Python3.7中的Dataclasses

原文鏈接:https://medium.com/mindorks/understanding-python-dataclasses-part-2-660ecc11c9b8
參考:https://linux.cn/article-9974-1.html

介紹

Dataclasses 是 Python 的類(LCTT 譯註:更準確的說,它是一個模塊),適用於存儲數據對象。你可能會問什麼是數據對象?下面是定義數據對象的一個不太詳細的特性列表:

  • 它們存儲數據並代表某種數據類型。例如:一個數字。對於熟悉 ORM 的人來說,模型實例就是一個數據對象。它代表一種特定的實體。它包含那些定義或表示實體的屬性。
  • 它們可以與同一類型的其他對象進行比較。例如:一個數字可以是 greater than(大於)、less than(小於) 或equal(等於) 另一個數字。

Python 3.7 提供了一個裝飾器 dataclass,用於將類轉換爲 dataclass

你所要做的就是將類包在裝飾器中:

from dataclasses import dataclass

@dataclass
class A:
 ...

初始化

通常是這樣:

class Number:
    def __init__(self, val):
        self.val = val
>>> one = Number(1)
>>> one.val
>>> 1

dataclass 是這樣:

@dataclass
class Number:
    val:int 
>>> one = Number(1)
>>> one.val
>>> 1

以下是 dataclass 裝飾器帶來的變化:

  1. 無需定義__init__,然後將值賦給 self,dataclass 負責處理它;
  2. 們以更加易讀的方式預先定義了成員屬性,以及類型提示。我們現在立即能知道 valint 類型。這無疑比一般定義類成員的方式更具可讀性。

它也可以定義默認值:

@dataclass
class Number:
    val:int = 0

表示

對象表示指的是對象的一個有意義的字符串表示,它在調試時非常有用。
默認的 Python 對象表示不是很直觀:

class Number:
    def __init__(self, val = 0):
    self.val = val
>>> a = Number(1)
>>> a
>>> <__main__.Number object at 0x7ff395b2ccc0>

這讓我們無法知悉對象的作用,並且會導致糟糕的調試體驗。

一個有意義的表示可以通過在類中定義一個__repr__方法來實現。

def __repr__(self):
    return self.val

現在我們得到這個對象有意義的表示:

>>> a = Number(1)
>>> a
>>> 1

dataclass會自動添加一個 __repr__函數,這樣我們就不必手動實現它了。

@dataclass
class Number:
    val: int = 0
>>> a = Number(1)
>>> a
>>> Number(val = 1)

數據比較

通常,數據對象之間需要相互比較。
兩個對象 a 和 b 之間的比較通常包括以下操作:

class Number:
    def __init__( self, val = 0):
       self.val = val
    def __eq__(self, other):
        return self.val == other.val
    def __lt__(self, other):
        return self.val < other.val

使用dataclass

@dataclass(order = True)
class Number:
    val: int = 0

我們不需要定義__eq____lt__方法,因爲當 order = True 被調用時,dataclass裝飾器會自動將它們添加到我們的類定義中。

那麼,它是如何做到的呢?

當你使用 dataclass 時,它會在類定義中添加函數 eqlt 。我們已經知道這點了。那麼,這些函數是怎樣知道如何檢查相等並進行比較呢?

生成 __eq__函數的 dataclass類會比較兩個屬性構成的元組,一個由自己屬性構成的,另一個由同類的其他實例的屬性構成。在我們的例子中,自動生成的 __eq__函數相當於:

def __eq__(self, other):
    return (self.val,) == (other.val,)

讓我們來看一個更詳細的例子:

我們會編寫一個dataclassPerson來保存 nameage

@dataclass(order = True)
class Person:
    name: str
    age:int = 0

自動生成的__eq__方法等同於:

def __eq__(self, other):
    return (self.name, self.age) == ( other.name, other.age)

同樣,等效的__le__函數類似於:

def __le__(self, other):
    return (self.name, self.age) <= (other.name, other.age)

當你需要對數據對象列表進行排序時,通常會出現像__le__這樣的函數的定義。Python 內置的 sorted函數依賴於比較兩個對象。

>>> import random
>>> a = [Number(random.randint(1,10)) for _ in range(10)] #generate list of random numbers
>>> a
>>> [Number(val=2), Number(val=7), Number(val=6), Number(val=5), Number(val=10), Number(val=9), Number(val=1), Number(val=10), Number(val=1), Number(val=7)]
>>> sorted_a = sorted(a) #Sort Numbers in ascending order
>>> [Number(val=1), Number(val=1), Number(val=2), Number(val=5), Number(val=6), Number(val=7), Number(val=7), Number(val=9), Number(val=10), Number(val=10)]
>>> reverse_sorted_a = sorted(a, reverse = True) #Sort Numbers in descending order 
>>> reverse_sorted_a
>>> [Number(val=10), Number(val=10), Number(val=9), Number(val=7), Number(val=7), Number(val=6), Number(val=5), Number(val=2), Number(val=1), Number(val=1)]

dataclass作爲一個可調用的裝飾器

定義所有的dunder(LCTT 譯註:這是指雙下劃線方法,即魔法方法)方法並不總是值得的。你的用例可能只包括存儲值和檢查相等性。因此,你只需定義 __init____eq__方法。如果我們可以告訴裝飾器不生成其他方法,那麼它會減少一些開銷,並且我們將在數據對象上有正確的操作。

幸運的是,這可以通過將 dataclass裝飾器作爲可調用對象來實現。

官方文檔來看,裝飾器可以用作具有如下參數的可調用對象:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
  • init:默認將生成 __init__方法。如果傳入 False,那麼該類將不會有 __init__方法。
  • repr__repr__方法默認生成。如果傳入 False,那麼該類將不會有 __repr__方法。
  • eq:默認將生成__eq__方法。如果傳入 False,那麼__eq__方法將不會被 dataclass 添加,但默認爲 object.eq
  • order:默認將生成__gt____ge____lt____le__方法。如果傳入 False,則省略它們。

Frozen(不可變) 實例

Frozen 實例是在初始化對象後無法修改其屬性的對象。

以下是我們期望不可變對象能夠做到的:

>>> a = Number(10) #Assuming Number class is immutable
>>> a.val = 10 # Raises Error

有了 dataclass,就可以通過使用 dataclass 裝飾器作爲可調用對象配合參數 frozen=True 來定義一個 frozen 對象

當實例化一個 frozen對象時,任何企圖修改對象屬性的行爲都會引發 FrozenInstanceError

@dataclass(frozen = True)
class Number:
    val: int = 0
>>> a = Number(1)
>>> a.val
>>> 1
>>> a.val = 2
>>> Traceback (most recent call last):
 File “<stdin>, line 1, in <module>
 File “<string>, line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field ‘val’

因此,一個frozen 實例是一種很好方式來存儲:

  • 常數
  • 設置

這些通常不會在應用程序的生命週期內發生變化,任何企圖修改它們的行爲都應該被禁止。

後期初始化處理

有了 dataclass,需要定義一個 __init__方法來將變量賦給 self這種初始化操作已經得到了處理。但是我們失去了在變量被賦值之後立即需要的函數調用或處理的靈活性。

讓我們來討論一個用例,在這個用例中,我們定義一個 Float 類來包含浮點數,然後在初始化之後立即計算整數和小數部分。

在這裏插入代碼片
import math
class Float:
    def __init__(self, val = 0):
        self.val = val
        self.process()
    def process(self):
        self.decimal, self.integer = math.modf(self.val)
>>> a = Float( 2.2)
>>> a.decimal
>>> 0.2000
>>> a.integer
>>> 2.0

幸運的是,使用 __post_init__ 方法已經能夠處理後期初始化操作。
生成的 __init__ 方法在返回之前調用 __post_init__返回。因此,可以在函數中進行任何處理。

import math
@dataclass
class FloatNumber:
    val: float = 0.0
    def __post_init__(self):
        self.decimal, self.integer = math.modf(self.val)
>>> a = FloatNumber(2.2)
>>> a.val
>>> 2.2
>>> a.integer
>>> 2.0
>>> a.decimal
>>> 0.2

繼承

Dataclasses 支持繼承,就像普通的 Python 類一樣。
因此,父類中定義的屬性將在子類中可用。

@dataclass
class Person:
    age: int = 0
    name: str
@dataclass
class Student(Person):
    grade: int
>>> s = Student(20, "John Doe", 12)
>>> s.age
>>> 20
>>> s.name
>>> "John Doe"
>>> s.grade
>>> 12

請注意,Student的參數是在類中定義的字段的順序。

繼承過程中 __post_init__的行爲是怎樣的?

由於__post_init__只是另一個函數,因此必須以傳統方式調用它:

@dataclass
class A:
    a: int
    def __post_init__(self):
        print("A")
@dataclass
class B(A):
    b: int
    def __post_init__(self):
        print("B")
>>> a = B(1,2)
>>> B

在上面的例子中,只有 B 的 __post_init__被調用,那麼我們如何調用 A 的 __post_init__ 呢?

@dataclass
class B(A):
    b: int
    def __post_init__(self):
        super().__post_init__() # 調用 A 的 post init
        print("B")
>>> a = B(1,2)
>>> A
    B

Dataclass fields

我們已經知道 Dataclasses 會生成他們自身的__init__方法。它同時把初始化的值賦給這些字段。在上一面定義的內容:

• 變量名

• 數據類型

這些內容僅給我們有限的 dataclass 字段使用範圍。讓我們討論一下這些侷限性,以及它們如何通過 dataclass.field 被解決。

複合初始化

考慮以下情形:你想要初始化一個變量爲列表。你如何實現它呢?一種簡單的方式是使用__post_init__方法。

import random

from typing import List

def get_random_marks():
	return [random.randint(1,10) for _ in range(5)]

@dataclass
class Student:
	marks: List[int]
	
	def __post_init__(self):
    	self.marks = get_random_marks() #Assign random speeds


>>> a = Student()
>>> a.marks
>>> [1,4,2,6,9]

數據類 Student 產生了一個名爲 marks 的列表。我們不傳遞 marks 的值,而是使用__post_init__方法初始化。這是我們定義的單一屬性。此外,我們必須在__post_init__裏調用 get_random_marks 函數。這些工作是額外的。

辛運的是,Python 爲我們提供了一個解決方案。我們可以使用 dataclasses.field來定製化 dataclass 字段的行爲以及它們在 dataclass 的影響。

仍然是上述的使用情形,讓我們從__post_init__裏去除get_random_marks的調用。以下是使用dataclasses.field的情形:

from dataclasses import field

@dataclass
class Student:
	marks: List[int] = field(default_factory = get_random_marks)
	
>>> s = Student()
>>> s.marks
>>> [1,4,2,6,9]

dataclasses.field接受了一個名爲 default_factory的參數,它的作用是:如果在創建對象時沒有賦值,則使用該方法初始化該字段。
default_factory必須是一個可以調用的無參數方法(通常爲一個函數)。

這樣我們就可以使用複合形式初始化字段。現在,讓我們考慮另一個使用場景。
dataclass 能夠自動生成<, =, >, <=>=這些比較方法。但是這些比較方法的一個缺陷是,它們使用類中的所有字段進行比較,而這種情況往往不常見。更經常地,這種比較方法會給我們使用 dataclasses 造成麻煩。

考慮以下的使用情形:你有一個數據類用於存放用戶的信息。現在,它可能存在以下字段:
• 姓名
• 年齡
• 身高
• 體重

你僅想比較用戶對象的年齡、身高和體重。你不想比較姓名。這是後端開發者經常會遇到的使用情景。

@dataclass(order = True)
class User:
	name: str
	age: int
	height: float
	weight: float

自動生成的比較方法會比較一下的元組:

(self.name, self.age, self.height, self.weight)

這將會破壞我們的意圖。我們不想讓姓名(name)用於比較。那麼,如何使用 dataclasses.field 來實現我們的想法呢?

@dataclass(order = True)
class User:
	name:str = field(compare = False) # compare = False tells the dataclass to not use name for comparison methods
	age: int
	weight: float
	height: float
		

>>> user_1 = User("John Doe", 23, 70, 1.70)
>>> user_2 = User("Adam", 24, 65, 1.60)

>>> user_1 < user_2
>>> True

默認情況下,所用的字段都用於比較,因此我們僅僅需要指定哪些字段用於比較,而實現方法是直接把不需要的字段定義爲filed(compare=False)

一個更爲簡單的應用情形也可以被討論。讓我們定義一個數據類,它被用來存儲一個數字激起字符串表示。我們想讓比較僅僅發生在該數字的值,而不是他的字符串表示。

@dataclass(order = True)
class Number:
	string: str
	val: int
	
>>> a = Number("one",1)
>>> b = Number("eight", 8)
>>> b > a # Compares as ("eight",8) > ("one",1)
>>> False

#Now we shall only compare using the Number.val

@dataclass(order = True)
class Number:
	string: str: = field(compare = False) #Do not use Number.string for comparison
	val: int
	
>>> a = Number("one", 1)
>>> b = Number("eight", 8)
>>> b > a # Compares (8,) > (1,)
>>> True

現在,我們有更大的自由來控制 dataclasses 的行爲。

使用全部字段進行數據表示
自動生成的__repr__方法使用所有的字段用於表示。當然,這也不是大多數情形下的理想選擇,尤其是當你的數據類有大量的字段時。單個對象的表示會變得異常臃腫,對調試來說也不利。

@dataclass(order = True)
class User:
	name: str = field(compare = False)
	age: int
	height: float
	weight: float
	city: str = field(compare = False)
	country: str = field(compare = False)
	
>>> a = User("John Doe", 24, 1.7, 70, "Massachusetts" ,"United States of America")
>>> a
>>> User(name='John Doe', age=24, height=1.7, weight=70, city='Massachusetts', country='United States of America')  #Debugging Nightmare coming through

想象一下在你的日誌裏看到這樣的表示吧,然後還要寫一個正則表達式來搜索它。

當然,我們也能夠個性化這種行爲。考慮一個類似的使用場景,也許最合適的用於表示的屬性是姓名(name)。那麼對__repr__,我們僅使用它:

@dataclass(order=True)
class User:
	name: str = field(compare=False)
	age: int = field(repr=False) # This tells the dataclass to not show age in the representation
	height:float = field(repr=False)
	weight:float = field(repr=False)
	city:str = field(repr=False, compare=False) #Do not use city for representation and comparison
	country:str = field(repr=False, compare=False)
	
>>> a = User("John Doe", 24, 1.7, 70, "Massachusetts", "United States of America")
>>> b = User("Adam", 24, 1.6, 65, "San Jose", "United States of America")

>>> a
>>> User(name='John Doe')
>>> b
>>> User(name='Adam')
>>> b > a #Compares (24, 1.7, 70) > (23, 1.6, 65)
>>> True

這樣看起來就很棒了。調試很方便,比較也有意義!

從初始化中省略字段

目前爲止我們看到的所有例子,都有一個共同特點——即我們需要爲所有被聲明的字段傳遞值,除了有默認值之外。在那種情形下(指有默認值的情況下),我們可以選擇傳遞值,也可以不傳遞。

@dataclass
class Number:
  string: str
  val:int = 0
  
>>> a = Number("Zero") #Not passing value of the field with default value
>>> a
>>> Number(string='Zero', val=0)

>>> b = Number("One",1) #Passing the default value of the field which has default declared
>>> b
>>> Number(string='One', val=1)

但是,還有一種情形:我們可能不想在初始化時設定某個字段的值。這也是一種常見的使用場景。也許你在追蹤一個對象的狀態,並且希望它在初始化時一直被設爲 False。更一般地,這個值在初始化時不能夠被傳遞。

class User:
	def __init__(self, email = None):
		self.email = email
		self.verified = False 
		#This field is set during initialization, but its value cannot be set manually while creating object

那麼,我們如何實現上述想法呢?以下是具體內容:

@dataclass
class User:
  email: str = field(repr = True)
  verified: bool = field(repr = False, init = False, default = False)
	#Omit verified from representation as well as __init__
	
>>> a = User("[email protected]")
>>> a
>>> User(email='[email protected]')
>>> a.verified
>>> False

>>> b = User("[email protected]", True) #Let us try to pass the value of verified
>>> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes 2 positional arguments but 3 were given
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章