python 3.7 dataclass 淺析
上文我們簡單的聊了一下dataclass
的一些用法,今天我們一起進一步熟悉它。
field
field
在dataclasses
裡面是比較重要的功能, 用於初處理定義的引數非常有用
在PEP 557
中是這樣描述field
的
Field objects describe each defined field. These objects are created internally, and are returned by the fields() module-level method (see below). Users should never instantiate a Field object directly.
大致意思就是 Field 物件是用於描述定義的欄位的,這些物件是內部定義好了的。然後由 field() 方法返回,使用者不用直接例項化 Field。
我們先看看field
是如何使用的
from dataclasses import dataclass, field @dataclass class A: a: str = field(default="123")
可以用於設立預設值,和a: str = "123"
一個效果,那為什麼我們還需要field
呢?
因為field
的功能遠不止這一個設定預設值,他還有很多有用的功能
-
設定是否載入到
__init__
裡面去
@dataclass class A: a: int b: int = field(default=10, init=False) a = A(1) # 注意,例項化 A 的時候只需要一個引數,賦給 a 的
等價於:
class A: b = 10 def __init__(self, a: int): self.a = a
-
設定是否成為
__repr__
返回引數我們在之前例項化
A
的時候,把例項化物件打印出來的話,是這樣的:A(a=1, b=10)
那如果我們不想把特定的物件打印出來,可以這樣寫:
@dataclass class A: a: int b: int = field(default=1, repr=False) a = A(1) print(a)
這時候,列印的結果為A(a=1)
-
設定是否計算
hash
的物件之一
a: int = field(hash=False)
-
設定是否成為和其他類進行對比的值之一
a: int = field(compare=False)
-
定義
field
資訊
from dataclasses import field, dataclass, fields @dataclass class A: a: int = field(metadata={"name": "a"}) # metadata 需要接受一個對映物件,也就是 python 的字典 metadata = fields(A) print(metadata)
列印的結果是
(Field(name='a',type=<class 'int'>,default=<dataclasses._MISSING_TYPE object at 0x10f2fe748>,default_factory=<dataclasses._MISSING_TYPE object at 0x10f2fe748>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'name': 'a'}),_field_type=_FIELD),)
是一個tuple
,第一個即是a
欄位的field
定義
可以通過metadata[0].metadata["name"]
獲取值
-
自定義處理定義的引數
有些欄位需要我們進行一些預處理,不用傳遞初始值,由其他函式返回
我們可以這麼寫
def value(): return "123" @dataclass class A: a: str = field(default_factory=value) print(A().a) # 例項化 A 的時候已經可以不傳遞值了
列印的結果是'123'
使用dataclass
設定初始方法
使用裝飾器dataclass
的時候,設定一些引數,即可選擇是否需要這些初始方法
-
__init__
@dataclass(init=False) class A: a: int = 1 print(A())
列印結果
['__module__', '__annotations__', 'a', '__dict__', '__weakref__', '__doc__', '__dataclass_params__', '__dataclass_fields__', '__repr__', '__eq__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__ne__', '__gt__', '__ge__', '__init__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']
的確是沒有__init__
的
-
__repr__
field
可以設定哪個引數不加入類返回值,設定
@dataclass(repr=False)
即可 -
__hash__
設定是否需要對類進行hash
,可以結合a: int = field(hash=True)
一起設定 -
__eq__
這是類之間比較使用的方法,
同樣可以結合a: int = field(compare=True)
一起設定
原始碼剖析
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
例項。
注意default
和default_factory
缺一不可,都是作為定義初始值的。
然後我們來看看Field
的原始碼:
class Field: __slots__ = ('name', 'type', 'default', 'default_factory', 'repr', 'hash', 'init', 'compare', 'metadata', '_field_type', ) def __init__(self, default, default_factory, init, repr, hash, compare, metadata): self.name = None self.type = None self.default = default self.default_factory = default_factory self.init = init self.repr = repr self.hash = hash self.compare = compare self.metadata = (_EMPTY_METADATA if metadata is None or len(metadata) == 0 else types.MappingProxyType(metadata)) self._field_type = None def __repr__(self): return ('Field(' f'name={self.name!r},' f'type={self.type!r},' f'default={self.default!r},' f'default_factory={self.default_factory!r},' f'init={self.init!r},' f'repr={self.repr!r},' f'hash={self.hash!r},' f'compare={self.compare!r},' f'metadata={self.metadata!r},' f'_field_type={self._field_type}' ')') def __set_name__(self, owner, name): func = getattr(type(self.default), '__set_name__', None) if func: # There is a __set_name__ method on the descriptor, call # it. func(self.default, owner, name)
基本沒有什麼可以說的,就是簡單的類,功能也就一個__set_name__
我們注意一下__repr__
裡面的有個細節:
f'name={self.name!r},'
, 比如self.name
為"name"
, 這裡會返回"name='name',"
dataclass
原始碼剖析
接下來我們來看dataclass
的原始碼
def dataclass(_cls=None, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False): def wrap(cls): return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen) if _cls is None: return wrap return wrap(_cls)
這是一個很常見的裝飾器
當我們定義類的時候,把類本身作為_cls
引數傳遞進去,這時候返回一個_process_class
函式的值
例項化類的時候,這時候_cls
為None
, 返回wrap
物件
接著我們來看_process_class
原始碼
這段程式碼比較長,我們刪減部分(不影響核心功能),刪除的是生成初始化函式的部分,有興趣的讀者可以自己檢視一下。
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): fields = {} setattr(cls, _PARAMS, _DataclassParams(init, repr, eq, order, unsafe_hash, frozen)) any_frozen_base = False has_dataclass_bases = False for b in cls.__mro__[-1:0:-1]: base_fields = getattr(b, _FIELDS, None) if base_fields: has_dataclass_bases = True for f in base_fields.values(): fields[f.name] = f if getattr(b, _PARAMS).frozen: any_frozen_base = True cls_annotations = cls.__dict__.get('__annotations__', {}) cls_fields = [_get_field(cls, name, type) for name, type in cls_annotations.items()] for f in cls_fields: fields[f.name] = f if isinstance(getattr(cls, f.name, None), Field): if f.default is MISSING: delattr(cls, f.name) else: setattr(cls, f.name, f.default) setattr(cls, _FIELDS, fields) if init: has_post_init = hasattr(cls, _POST_INIT_NAME) flds = [f for f in fields.values() if f._field_type in (_FIELD, _FIELD_INITVAR)] _set_new_attribute(cls, '__init__', _init_fn(flds, frozen, has_post_init, '__dataclass_self__' if 'self' in fields else 'self', )) return cls
這段程式碼,最後將傳進來的cls
返回出去,也就是返回的是類本身(初始化類的時候)
我們來看第一句程式碼:
setattr(cls, _PARAMS, _DataclassParams(init, repr, eq, order, unsafe_hash, frozen))
_PARAMS
為前面定義的變數,值為__dataclass_params__
_DataclassParams
是一個類
這句話就是把_DataclassParams
例項作為值,__dataclass_params__
作為屬性賦給cls
所以,我們在檢視定義的類的所有屬性的時候,會有一個__dataclass_params__
屬性,然後我們列印看看:
_DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False)
即是_DataclassParams
例項
第二段程式碼
fields = {} any_frozen_base = False has_dataclass_bases = False for b in cls.__mro__[-1:0:-1]: base_fields = getattr(b, _FIELDS, None) if base_fields: has_dataclass_bases = True for f in base_fields.values(): fields[f.name] = f if getattr(b, _PARAMS).frozen: any_frozen_base = True
前兩行都是定義變數,直接從第三行開始。
cls.__mro__[-1:0:-1]
這代表取cls
本身和繼承的類,按照新式類的順序從子類到父類排序
(詳情見: mro )
然後不要第一個(即自己本身),剩下的進行倒序排列,這時候,所有類的順序已經變成了父類到子類,這時候第一個為object
_FIELDS
為前面定義的變數,為__dataclass_fields__
輪詢排好序的類,如果由__dataclass_fields__
屬性,則進行前面的定義的變數操作,把所有的取到的值加入fields
。
只有用@dataclass
生成的類才會有這個屬性。
第三段程式碼
cls_annotations = cls.__dict__.get('__annotations__', {}) cls_fields = [_get_field(cls, name, type) for name, type in cls_annotations.items()] for f in cls_fields: fields[f.name] = f if isinstance(getattr(cls, f.name, None), Field): if f.default is MISSING: delattr(cls, f.name) else: setattr(cls, f.name, f.default)
cls_annotations = cls.__dict__.get('__annotations__', {})
這句話就是為了取出我們定義的所有欄位
只要我們定義欄位是
a: int b: str
這樣的,就會自動有__annotations__
屬性
可以參看PEP 526
然後賦予cls
屬性操作
這步操作就是我們能夠進行類取值的關鍵
第四段程式碼
setattr(cls, _FIELDS, fields)
將fields
(最早定義的一個字典)作為值,賦給cls
的屬性__dataclass_fields__
第五段程式碼
if init: has_post_init = hasattr(cls, _POST_INIT_NAME) flds = [f for f in fields.values() if f._field_type in (_FIELD, _FIELD_INITVAR)] _set_new_attribute(cls, '__init__', _init_fn(flds, frozen, has_post_init, '__dataclass_self__' if 'self' in fields else 'self', ))
這段程式碼表示,一旦設定__init__=True
,會在類裡面加上這個方法。
def _set_new_attribute(cls, name, value): if name in cls.__dict__: return True setattr(cls, name, value) return False
_set_new_attribute
是一個為類賦予屬性的方法
至此,dataclass
原始碼剖析完畢
make_dataclass
原始碼剖析
原始碼為:
def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False): if namespace is None: namespace = {} else: namespace = namespace.copy() seen = set() anns = {} for item in fields: if isinstance(item, str): name = item tp = 'typing.Any' elif len(item) == 2: name, tp, = item elif len(item) == 3: name, tp, spec = item namespace[name] = spec else: raise TypeError(f'Invalid field: {item!r}') if not isinstance(name, str) or not name.isidentifier(): raise TypeError(f'Field names must be valid identifers: {name!r}') if keyword.iskeyword(name): raise TypeError(f'Field names must not be keywords: {name!r}') if name in seen: raise TypeError(f'Field name duplicated: {name!r}') seen.add(name) anns[name] = tp namespace['__annotations__'] = anns cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace)) return dataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
流程很詳細,就是解析我們定義的fields
,然後賦予__annotations__
屬性,最後使用dataclass
生成一個類。
從其中的流程判斷來看,fields
裡面最長只允許我們設定三個值,第一個名字,第二個型別,第三個是fields
物件。
原始碼剖析至此結束
尾聲
從功能上來看,dataclass
為我們帶來了比較好優化類方案,提供的各類方法也足夠用,可以在之後的專案裡面逐漸使用起來。
從原始碼上來看,原始碼整體比較簡潔,使用了比較少見的__annotations__
,技巧足夠,程式碼簡單易學。
建議新手可以從此入手,即可學習裝飾器也可學習優秀程式碼。