1. 程式人生 > 其它 >(轉)python 資料類:dataclass

(轉)python 資料類:dataclass

原文:https://www.cnblogs.com/dan-baishucaizi/p/14786600.html

1、dataclass簡介

​ dataclass是python3.7開始帶有的新屬性(類裝飾器),dataclass是指”一個帶有預設值的可變namedtuple“,本質還是一個類,它的屬性非特殊情況可以直接訪問,類中有與屬性相關的類方法。簡單地說就是一個含有資料及其操作方法的類。

dataclass與普通類的區別

  • 與普通類相比,dataclass通常不包含私有屬性,這些屬性可以直接訪問(也可以私有);
  • repr() 函式將物件轉化為供直譯器讀取的形式;dataclass的repr方法通常有其固定格式,會列印類名、屬性名、屬性值;
  • dataclass有__eq____hash__這些魔法方法;
  • dataclass有著模式單一固定的構造方式,根據需要有時需要過載運算子,而普通class通常無需這些工作。

注:namedtuple是tuple的子類,它的元素是有命名的!


Top---Bottom

2、引入dataclass裝飾器

常見的類生成方式

class elfin:
    def __init__(self, name, age):
        self.name = name
        self.age = age

使用dataclass裝飾器

@dataclass
class elfin:
    name: str
    age: int

我們使用@dataclass就可以實現與普通類的效果,這樣程式碼更簡潔!

__post_init__方法

如果某個屬性需要在init後處理,就可以放置到__post_init__中!

@dataclass
class elfin:
    name: str
    age: int
    
    def __post_init__(self):
        if type(self.name) is str:
            self.identity = identity_dict[self.name]

測試上面的案例:

>>> from dataclasses import dataclass
>>> identity_dict = {
... "firstelfin": "boss",
... "secondelfin": "master",
... "thirdelfin": "captain"
... }
>>> @dataclass
... class Elfin:
...     name: str
...     age: int
...
...     def __post_init__(self):
...         if type(self.name) is str:
...             self.identity = identity_dict[self.name]
>>> print(Elfin)
... Out[1]: <class '__main__.Elfin'>
>>> elfin_ins = Elfin("firstelfin", 23)
>>> elfin_ins
... Out[2]: Elfin(name='firstelfin', age=23)
>>> elfin_ins.identity
... Out[3]: 'boss'

上面的案例向我們展示了即使init部分沒有生成identity屬性,例項也可以獲取到!

下面我們就分別展示dataclass裝飾器的一些知識點。


Top---Bottom

3、dataclass裝飾器選項

使用dataclass類裝飾器的選項,我們可以定製我們想要的資料類,預設選項為:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Elfin:
    pass

裝飾器的引數選項說明:

  • init控制是否生成__init__方法;
  • repr控制是否生成__repr__方法;
  • eq控制是否生成__eq__方法,它用於判斷例項是否相等;
  • order控制是否建立四種大小關係方法:__lt____le____gt____ge__;order為True,則eq不能為False,也不能自定義order方法。
  • unsafe_hash控制hash的生成方式。
    • 當unsafe_hash為False時,將根據eq、frozen引數來生成__hash__方法;
      1. eq、frozen都為True時,__hash__將會生成;
      2. eq為True,frozen為False,__hash__將被設定為None;
      3. eq為False,frozen為True,__hash__將使用object(超類)的同名屬性(通常就是物件id的hash)
    • 當unsafe_hash為True時,將會根據類的屬性生成__hash__。如其名,這是不安全的,因為屬性是可變的,這會導致hash的不一致。當然您能保證物件屬性不會變,你也可以設定為True。
  • frozen控制是否凍結對field賦值。設定為True時,物件將是不可變的,因為不可變,所以如果設定有__setattr____delattr__將會導致TypeError錯誤。

前兩個引數我們在上一章實際已經看了效果,下面我們檢視引數eqorder

>>> @dataclass(init=True, repr=True, eq=True, order=True)
... class Elfin:
...     name: str
...     age: int
...
...     def __post_init__(self):
...         if type(self.name) is str:
...             self.identity = identity_dict[self.name]
>>> elfin_ins1 = Elfin("thirdelfin", 18)
>>> elfin_ins2 = Elfin("secondelfin", 20)
>>> elfin_ins1 == elfin_ins2
... Out[4]: False
>>> elfin_ins1 >= elfin_ins2
... Out[5]: True
>>> 

可以發現我們可以在例項之間進行大小的比較了!同時我們知道普通類是不同進行大小比較的:

>>> class A:
... def __init__(self, age):
...     self.age = age
>>> a1 = A(20)
>>> a2 = A(30)
>>> a1 > a2
... TypeError                  Traceback (most recent call last)
... <ipython-input-24-854e76ddfa09> in <module>
... ----> 1 a1 > a2
...
... TypeError: '>' not supported between instances of 'A' and 'A'

上面我們提到了field,實際上,所有的資料類屬性,都是被field所控制,它代表一個數據的實體和它的元資訊,下面我們瞭解一下dataclasses.field


Top---Bottom

4、資料類的基石--dataclasses.field

field的定義如下:

def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
          hash=None, compare=True, metadata=None):
    if default is not MISSING and default_factory is not MISSING:
        raise ValueError('cannot specify both default and default_factory')
    return Field(default, default_factory, init, repr, hash, compare,
                 metadata)

一般情況下,我們無需直接使用,裝飾器會根據我們給出的型別註解自動生成field,但有時候也需要定製這個過程,所以dataclasses.field就特別重要了!

引數說明:

  • default:如果呼叫時沒有指定,則預設為None,它控制的是field的預設值;

  • default_factory:控制如何產生值,它接收一個無引數或者全是預設引數的callable物件,然後呼叫該物件field的初始值,再將default複製給callable物件。

  • init:控制是否在init中生成此引數。在前面章節的案例中,我們要生成self.identity屬性,但是不想在init中傳入,就可以使用field了。

    >>> @dataclass(init=True, repr=True, eq=True, order=True)
    ... class Elfin:
    ...     name: str
    ...     age: int
    ...    	identity: str = field(init=False)
    ...
    ...     def __post_init__(self):
    ...         if type(self.name) is str:
    ...             self.identity = identity_dict[self.name]
    >>> elfin_ins3 = Elfin("firstelfin", 20)
    >>> elfin_ins3
    ... Out[6]: Elfin(name='firstelfin', age=20, identity='boss')
    
  • repr:表示該field是否被包含進repr的輸出,預設要輸出,如上面的案例。

  • compare:是否參與比較和計算hash值。

  • hash:是否參與比較和計算hash值。

  • metadata不被dataclass自身使用,通常讓第三方元件從中獲取某些元資訊時才使用,所以我們不需要使用這一引數。

只能初始化呼叫的屬性

如果指定一個field的型別註解為dataclasses.InitVar,那麼這個field將只會在初始化過程中(__init____post_init__)可以被使用,當初始化完成後訪問該field會返回一個dataclasses.Field物件而不是field原本的值,也就是該field不再是一個可訪問的資料物件。

>>> from dataclasses import InitVar
>>> @dataclass(init=True, repr=True, eq=True, order=True)
... class Elfin:
...     name: str
...     age: int
...    	identity: InitVar[str] = None
...
...     def __post_init__(self, identity):
...         if type(self.name) is str:
...             self.identity = identity_dict[self.name]
>>> elfin_ins3 = Elfin("firstelfin", 20)
>>> elfin_ins3
... Out[7]: Elfin(name='firstelfin', age=20)
>>> elfin_ins3.identity
>>>

注意這裡elfin_ins3.identity說明都沒有返回,實際上應該是”boss“,但是我們訪問不到。


Top---Bottom

5、dataclass的常用函式

5.1 轉換資料為字典 dataclasses.asdict

>>> from dataclasses import asdict
>>> asdict(elfin_ins3)
... Out[8]: {'name': 'firstelfin', 'age': 20}

5.2 轉換資料為元組 dataclasses.astuple

>>> from dataclasses import astuple
>>> astuple(elfin_ins3)
... Out[9]: ('firstelfin', 20)

5.3 判斷是否是dataclass類

>>> from dataclasses import is_dataclass
>>> is_dataclass(Elfin)
... Out[10]: True
>>> is_dataclass(elfin_ins3)
... Out[11]: True

Top---Bottom

6、dataclass繼承

​ python3.7引入dataclass的一大原因就在於相比namedtuple,dataclass可以享受繼承帶來的便利。

dataclass裝飾器會檢查當前class的所有基類,如果發現一個dataclass,就會把它的屬性按順序新增進當前的class,隨後再處理當前class的field。所有生成的方法也將按照這一過程處理,因此如果子類中的field與基類同名,那麼子類將會無條件覆蓋基類。子類將會根據所有的field重新生成一個建構函式,並在其中初始化基類。

案例:

>>> @dataclass(init=True, repr=True, eq=True, order=True)
... class Elfin:
...     name: str = "firstelfin"
...     age: int = 20
...    	identity: InitVar[str] = None
...
...     def __post_init__(self, identity):
...         if type(self.name) is str:
...             self.identity = identity_dict[self.name]
>>> @dataclass
... class Wude(Elfin):
...     age: int = 68
>>> Wude()
... Out[11]: Wude(name='firstelfin', age=68)
>>> 

上述可見,Wude類繼承了Elfin類的name屬性,而例項中的age覆蓋了Elfin中的age定義。


Top---Bottom

7、小結

​ 合理使用dataclass將會大大減輕開發中的負擔,將我們從大量的重複勞動中解放出來,這既是dataclass的魅力,不過魅力的背後也總是有陷阱相伴,最後我想提幾點注意事項:

  • dataclass通常情況下是unhashable的,因為預設生成的__hash__None,所以不能用來做字典的key,如果有這種需求,那麼應該指定你的資料類為frozendataclass
  • 小心當你定義了和dataclass生成的同名方法時會引發的問題
  • 當使用可變型別(如list)時,應該考慮使用fielddefault_factory
  • 資料類的屬性都是公開的,如果你有屬性只需要初始化時使用而不需要在其他時候被訪問,請使用dataclasses.InitVar

​ 只要避開這些陷阱,dataclass一定能成為提高生產力的利器。

技術連結