原文鏈接: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 裝飾器
帶來的變化:
- 無需定義
__init__
,然後將值賦給self
,dataclass 負責處理它; - 們以更加易讀的方式預先定義了成員屬性,以及類型提示。我們現在立即能知道
val
是int
類型。這無疑比一般定義類成員的方式更具可讀性。
它也可以定義默認值:
@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 之間的比較通常包括以下操作:
- a < b
- a > b
- a == b
- a >= b
- a <= b
參考:https://docs.python.org/3/reference/datamodel.html#object.__lt__
通常這樣寫:
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 時,它會在類定義中添加函數 eq 和 lt 。我們已經知道這點了。那麼,這些函數是怎樣知道如何檢查相等並進行比較呢?
生成 __eq__
函數的 dataclass
類會比較兩個屬性構成的元組,一個由自己屬性構成的,另一個由同類的其他實例的屬性構成。在我們的例子中,自動生成的 __eq__
函數相當於:
def __eq__(self, other):
return (self.val,) == (other.val,)
讓我們來看一個更詳細的例子:
我們會編寫一個dataclass
類 Person
來保存 name
和 age
。
@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