dataclasses --- 資料類

原始碼: Lib/dataclasses.py


此模組提供了一個裝飾器和一些函式,用於自動為使用者自定義的類新增諸如 __init__()__repr__() 等生成的特殊方法。它最初在 PEP 557 中被描述。

要在這些生成的方法中使用成員變數,需要使用 PEP 526 的型別註解進行定義。例如,以下程式碼:

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

將會新增一個類似於下面的 __init__() 方法,以及其他方法:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

注意,此方法是自動新增到類中的:它沒有在上面顯示的 InventoryItem 定義中直接指定。

在 3.7 版本加入。

模組內容

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)

這個函式是一個裝飾器,用於為類新增生成的特殊方法,如下所述。

@dataclass 裝飾器檢查類以尋找 field。一個 field 被定義為具有型別註解的類變數。除了下面描述的兩個例外,@dataclass 不會檢查變數註解中指定的型別。

所有生成方法中的欄位順序,就是它們在類定義中出現的順序。

@dataclass 裝飾器會向類中新增各種“雙下劃線”方法,如下所述。如果類中已存在任何被新增的方法,其行為取決於引數,具體如下文所述。裝飾器返回其被呼叫的同一個類;不會建立新的類。

如果 @dataclass 僅作為一個不帶引數的簡單裝飾器使用,它的行為就像使用了此簽名中記錄的預設值一樣。也就是說,@dataclass 的這三種用法是等價的:

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False,
           match_args=True, kw_only=False, slots=False, weakref_slot=False)
class C:
    ...

@dataclass 的引數有:

  • init: 若為真值 (預設),將生成一個 __init__() 方法。

    如果該類已經定義了 __init__(),則此形參將被忽略。

  • repr: 若為真值 (預設),將生成一個 __repr__() 方法。生成的 repr 字串將帶有類名以及每個欄位的名稱和 repr,順序與它們在類中的定義順序相同。被標記為不包含在 repr 中的欄位將不會被包括。例如: InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)

    如果該類已經定義了 __repr__(),則此形參將被忽略。

  • eq: 若為真值 (預設),將生成一個 __eq__() 方法。此方法會按順序將類例項作為其欄位的元組來進行比較。參與比較的兩個例項必須為相同的型別。

    如果該類已經定義了 __eq__(),則此形參將被忽略。

  • order:如果為真值(預設為 False),將會生成 __lt__()__le__()__gt__()__ge__() 方法。這些方法會按順序將類例項作為其欄位的元組來進行比較。參與比較的兩個例項必須為相同的型別。如果 order 為真值而 eq 為假值,則會引發 ValueError

    如果該類已定義了 __lt__()__le__()__gt__()__ge__() 中的任何一個,則會引發 TypeError

  • unsafe_hash: 如果為 true,則強制 dataclasses 建立一個 __hash__() 方法,即使這樣做可能不安全。否則,將根據 eqfrozen 的設定來生成 __hash__() 方法。預設值為 False

    內建的 hash() 在物件被新增到雜湊集合(如字典和集合)時會使用 __hash__()。擁有 __hash__() 意味著類的例項是不可變的。可變性是一個複雜的屬性,取決於程式設計師的意圖、__eq__() 的存在和行為,以及 @dataclass 裝飾器中 eqfrozen 標誌的值。

    預設情況下,@dataclass 不會隱式新增 __hash__() 方法,除非這樣做是安全的。它也不會新增或更改已顯式定義的 __hash__() 方法。如 __hash__() 文件中所述,將類屬性 __hash__ = None 設定為對 Python 具有特定含義。

    如果 __hash__() 沒有被顯式定義,或者它被設定為 None,那麼 @dataclass 可能 會新增一個隱式的 __hash__() 方法。雖然不推薦,但你可以使用 unsafe_hash=True 來強制 @dataclass 建立一個 __hash__() 方法。這可能適用於你的類在邏輯上是不可變的,但仍然可以被修改的情況。這是一個特殊的用例,應仔細考慮。

    以下是隱式建立 __hash__() 方法的規則。請注意,你不能在資料類中既有顯式的 __hash__() 方法,又設定 unsafe_hash=True;這將導致 TypeError

    如果 eqfrozen 均為真值,預設情況下 @dataclass 將為你生成一個 __hash__() 方法。如果 eq 為真值而 frozen 為假值,__hash__() 將被設為 None,標記其為不可雜湊的(因為它確實是可變的)。如果 eq 為假值,__hash__() 將保持不變,意味著將使用超類的 __hash__() 方法(如果超類是 object,則會回退為基於 id 的雜湊)。

  • frozen: 如果為真(預設為 False),對欄位進行賦值會產生一個異常。這模擬了只讀的凍結例項。請參閱下面的討論

    如果在類中定義了 __setattr__()__delattr__() 並且 frozen 為真,則會引發 TypeError

  • match_args: 如果為真(預設為 True),則會從生成的 __init__() 方法的非僅關鍵字引數列表中建立 __match_args__ 元組(即使不生成 __init__(),見上文)。如果為假,或者如果類中已定義了 __match_args__,則不會生成 __match_args__

在 3.10 版本加入。

  • kw_only: 若為真值(預設值為 False),則所有欄位都將被標記為僅限關鍵字。如果一個欄位被標記為僅限關鍵字,其唯一效果是,在呼叫 __init__() 時,必須使用關鍵字來指定由該欄位生成的 __init__() 引數。詳見形參術語條目。另見 KW_ONLY 部分。

    僅關鍵字欄位不包含在 __match_args__ 中。

在 3.10 版本加入。

  • slots: 若為真值(預設值為 False),則會生成 __slots__ 屬性,並返回一個新類而不是原始類。如果類中已經定義了 __slots__,則會引發 TypeError

警告

當使用 slots=True 時,向基類的 __init_subclass__() 傳遞引數將導致 TypeError。解決方法是使用不帶引數的 __init_subclass__ 或使用預設值。詳情見 gh-91126

在 3.10 版本加入。

在 3.11 版本發生變更: 如果欄位名已包含在基類的 __slots__ 中,它將不會包含在生成的 __slots__ 中,以防止覆蓋它們。因此,不要使用 __slots__ 來檢索資料類的欄位名。請改用 fields()。為了能夠確定繼承的槽位,基類的 __slots__ 可以是任何可迭代物件,但*不能*是迭代器。

  • weakref_slot: 若為真值(預設值為 False),則新增一個名為 “__weakref__” 的槽,這是使例項可弱引用所必需的。在未指定 slots=True 的情況下指定 weakref_slot=True 是一個錯誤。

在 3.11 版本中新增。

field 可以選擇性地指定一個預設值,使用正常的 Python 語法:

@dataclass
class C:
    a: int       # 'a' has no default value
    b: int = 0   # assign a default value for 'b'

在此示例中,ab 都將包含在新增的 __init__() 方法中,該方法將被定義為:

def __init__(self, a: int, b: int = 0):

如果一個沒有預設值的欄位跟在一個有預設值的欄位後面,將會引發 TypeError。無論這發生在單個類中,還是作為類繼承的結果,都是如此。

dataclasses.field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None)

對於常見和簡單的用例,不需要其他功能。然而,有些資料類特性需要額外的逐欄位資訊。為了滿足這種對額外資訊的需求,你可以用對提供的 field() 函式的呼叫來替換預設欄位值。例如:

@dataclass
class C:
    mylist: list[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

如上所示,MISSING 值是一個哨兵物件,用於檢測使用者是否提供了某些引數。使用這個哨兵是因為 None 對於某些具有不同含義的引數是一個有效值。任何程式碼都不應直接使用 MISSING 值。

field() 的引數有:

  • default: 如果提供,這將是該欄位的預設值。這是必需的,因為 field() 呼叫本身取代了預設值的正常位置。

  • default_factory: 如果提供,它必須是一個零引數的可呼叫物件,當需要此欄位的預設值時將被呼叫。除了其他用途,這可以用來指定具有可變預設值的欄位,如下所述。同時指定 defaultdefault_factory 是錯誤的。

  • init: 如果為真(預設值),此欄位將作為引數包含在生成的 __init__() 方法中。

  • repr: 如果為真(預設值),此欄位將包含在生成的 __repr__() 方法返回的字串中。

  • hash: 這可以是一個布林值或 None。如果為真,此欄位將包含在生成的 __hash__() 方法中。如果為假,此欄位將從生成的 __hash__() 中排除。如果為 None(預設值),則使用 compare 的值:這通常是預期的行為,因為如果欄位用於比較,它就應該包含在雜湊中。不鼓勵將此值設定為除 None 之外的任何值。

    hash=Falsecompare=True 的一個可能原因是,如果某個欄位計算雜湊值成本高昂,而該欄位對於相等性測試是必需的,並且還有其他欄位對型別的雜湊值有貢獻。即使一個欄位被從雜湊中排除,它仍將用於比較。

  • compare: 如果為真(預設值),此欄位將包含在生成的相等性和比較方法中(__eq__(), __gt__(), 等)。

  • metadata: 這可以是一個對映或 NoneNone 被視為空字典。這個值被包裝在 MappingProxyType() 中以使其只讀,並暴露在 Field 物件上。資料類完全不使用它,而是作為第三方擴充套件機制提供。多個第三方可以各自擁有自己的鍵,作為元資料中的名稱空間。

  • kw_only: 如果為真,此欄位將被標記為僅關鍵字。這在計算生成的 __init__() 方法的引數時使用。

    僅關鍵字欄位也不包含在 __match_args__ 中。

在 3.10 版本加入。

  • doc: 該欄位的可選文件字串。

在 3.14 版本加入。

如果欄位的預設值是透過呼叫 field() 指定的,那麼該欄位的類屬性將被指定的 default 值替換。如果未提供 default,則該類屬性將被刪除。其意圖是,在 @dataclass 裝飾器執行後,類屬性將全部包含欄位的預設值,就像直接指定了預設值本身一樣。例如,在

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20

類屬性 C.z 將是 10,類屬性 C.t 將是 20,而類屬性 C.xC.y 將不會被設定。

class dataclasses.Field

Field 物件描述每個已定義的欄位。這些物件在內部建立,並由模組級方法 fields() 返回(見下文)。使用者不應直接例項化 Field 物件。其文件化的屬性有:

  • name: 欄位的名稱。

  • type: 欄位的型別。

  • default, default_factory, init, repr, hash, compare, metadata, 和 kw_only 的含義和值與它們在 field() 函式中的含義和值相同。

可能存在其他屬性,但它們是私有的,不應被檢查或依賴。

class dataclasses.InitVar

InitVar[T] 型別註解描述了僅限初始化的變數。用 InitVar 註解的欄位被認為是偽欄位,因此既不會被 fields() 函式返回,也不會以任何方式使用,除了將它們作為引數新增到 __init__() 和可選的 __post_init__()

dataclasses.fields(class_or_instance)

返回一個 Field 物件元組,該元組定義了此資料類的欄位。接受資料類或資料類的例項。如果傳入的不是資料類或其例項,則引發 TypeError。不返回 ClassVarInitVar 型別的偽欄位。

dataclasses.asdict(obj, *, dict_factory=dict)

將資料類 obj 轉換為字典(透過使用工廠函式 dict_factory)。每個資料類例項都會被轉換為一個其欄位構成的字典,形式為 name: value 鍵值對。資料類、字典、列表和元組會被遞迴地轉換。其他物件則透過 copy.deepcopy() 進行復制。

在巢狀資料類上使用 asdict() 的示例:

@dataclass
class Point:
     x: int
     y: int

@dataclass
class C:
     mylist: list[Point]

p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}

c = C([Point(0, 0), Point(10, 4)])
assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

要建立淺複製,可以使用以下變通方法:

{field.name: getattr(obj, field.name) for field in fields(obj)}

如果 obj 不是資料類的例項,asdict() 會引發 TypeError

dataclasses.astuple(obj, *, tuple_factory=tuple)

將資料類 obj 轉換為元組(透過使用工廠函式 tuple_factory)。每個資料類例項都會被轉換為一個其欄位值構成的元組。資料類、字典、列表和元組會被遞迴地轉換。其他物件則透過 copy.deepcopy() 進行復制。

接上一個例子:

assert astuple(p) == (10, 20)
assert astuple(c) == ([(0, 0), (10, 4)],)

要建立淺複製,可以使用以下變通方法:

tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))

如果 obj 不是資料類的例項,astuple() 會引發 TypeError

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False, module=None, decorator=dataclass)

建立一個新的資料類,其名稱為 cls_name,欄位定義在 fields 中,基類在 bases 中給出,並使用 namespace 中給出的名稱空間進行初始化。fields 是一個可迭代物件,其元素可以是 name(name, type)(name, type, Field)。如果只提供了 name,則 type 會使用 typing.Anyinitrepreqorderunsafe_hashfrozenmatch_argskw_onlyslotsweakref_slot 的值與它們在 @dataclass 中的含義相同。

如果定義了 module,資料類的 __module__ 屬性將被設定為該值。預設情況下,它被設定為呼叫者的模組名。

decorator 引數是一個可呼叫物件,將用於建立資料類。它應將類物件作為第一個引數,並接受與 @dataclass 相同的關鍵字引數。預設情況下,使用 @dataclass 函式。

這個函式並非嚴格必需,因為任何建立帶有 __annotations__ 的新類的 Python 機制,都可以隨後應用 @dataclass 函式將該類轉換為資料類。提供此函式是為了方便。例如:

C = make_dataclass('C',
                   [('x', int),
                     'y',
                    ('z', int, field(default=5))],
                   namespace={'add_one': lambda self: self.x + 1})

等價於:

@dataclass
class C:
    x: int
    y: 'typing.Any'
    z: int = 5

    def add_one(self):
        return self.x + 1

3.14 版新增: 增加了 decorator 引數。

dataclasses.replace(obj, /, **changes)

建立一個與 obj 型別相同的新物件,用 changes 中的值替換欄位。如果 obj 不是資料類,則引發 TypeError。如果 changes 中的鍵不是給定資料類的欄位名,則引發 TypeError

新返回的物件是透過呼叫資料類的 __init__() 方法建立的。這確保瞭如果存在 __post_init__(),它也會被呼叫。

如果存在任何沒有預設值的僅初始化變數,必須在對 replace() 的呼叫中指定它們,以便可以將它們傳遞給 __init__()__post_init__()

changes 中包含任何定義為 init=False 的欄位是錯誤的。在這種情況下會引發 ValueError

請注意 init=False 欄位在呼叫 replace() 期間的工作方式。它們不會從源物件複製,而是在 __post_init__() 中初始化,如果它們被初始化的話。預計 init=False 欄位將很少且謹慎地使用。如果使用它們,明智的做法可能是擁有備用的類建構函式,或者一個處理例項複製的自定義 replace()(或類似名稱的)方法。

資料類例項也受通用函式 copy.replace() 支援。

dataclasses.is_dataclass(obj)

如果其引數是資料類(包括資料類的子類)或其一個例項,則返回 True,否則返回 False

如果你需要知道一個類是否是資料類的例項(而不是資料類本身),那麼需要再增加一個 not isinstance(obj, type) 的檢查。

def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)
dataclasses.MISSING

一個哨兵值,表示缺少 default 或 default_factory。

dataclasses.KW_ONLY

一個用作型別註解的哨兵值。在型別為 KW_ONLY 的偽欄位之後的任何欄位都將被標記為僅關鍵字欄位。請注意,型別為 KW_ONLY 的偽欄位在其他方面被完全忽略。這包括該欄位的名稱。按照慣例,KW_ONLY 欄位的名稱使用 _。僅關鍵字欄位表示在例項化類時必須作為關鍵字指定的 __init__() 引數。

在此示例中,欄位 yz 將被標記為僅關鍵字欄位:

@dataclass
class Point:
    x: float
    _: KW_ONLY
    y: float
    z: float

p = Point(0, y=1.5, z=2.0)

在單個數據類中,指定多個型別為 KW_ONLY 的欄位是錯誤的。

在 3.10 版本加入。

exception dataclasses.FrozenInstanceError

在用 frozen=True 定義的資料類上呼叫隱式定義的 __setattr__()__delattr__() 時引發。它是 AttributeError 的子類。

初始化後處理

dataclasses.__post_init__()

當在類上定義時,它將被生成的 __init__() 呼叫,通常為 self.__post_init__()。但是,如果定義了任何 InitVar 欄位,它們也將按照在類中定義的順序傳遞給 __post_init__()。如果沒有生成 __init__() 方法,則 __post_init__() 將不會被自動呼叫。

除了其他用途外,這允許初始化依賴於一個或多個其他欄位的欄位值。例如:

@dataclass
class C:
    a: float
    b: float
    c: float = field(init=False)

    def __post_init__(self):
        self.c = self.a + self.b

@dataclass 生成的 __init__() 方法不會呼叫基類的 __init__() 方法。如果基類有一個必須被呼叫的 __init__() 方法,通常在 __post_init__() 方法中呼叫此方法:

class Rectangle:
    def __init__(self, height, width):
        self.height = height
        self.width = width

@dataclass
class Square(Rectangle):
    side: float

    def __post_init__(self):
        super().__init__(self.side, self.side)

但是請注意,通常情況下,由資料類生成的 __init__() 方法不需要被呼叫,因為派生的資料類會負責初始化任何本身是資料類的基類的所有欄位。

有關如何向 __post_init__() 傳遞引數的方法,請參閱下面關於僅初始化變數的部分。另請參閱關於 replace() 如何處理 init=False 欄位的警告。

類變數

@dataclass 實際檢查欄位型別的少數地方之一是確定一個欄位是否為 PEP 526 中定義的類變數。它透過檢查欄位的型別是否為 typing.ClassVar 來做到這一點。如果一個欄位是 ClassVar,它將被排除在欄位考慮範圍之外,並被資料類機制忽略。這樣的 ClassVar 偽欄位不會被模組級的 fields() 函式返回。

僅限初始化的變數

@dataclass 檢查型別註解的另一個地方是確定一個欄位是否是僅初始化變數。它透過檢視欄位的型別是否為 InitVar 型別來實現。如果一個欄位是 InitVar,它被認為是一個稱為僅初始化欄位的偽欄位。由於它不是一個真正的欄位,它不會被模組級的 fields() 函式返回。僅初始化欄位被新增為生成的 __init__() 方法的引數,並傳遞給可選的 __post_init__() 方法。它們在其他方面不被資料類使用。

例如,假設一個欄位將從資料庫初始化,如果在建立類時沒有提供值:

@dataclass
class C:
    i: int
    j: int | None = None
    database: InitVar[DatabaseType | None] = None

    def __post_init__(self, database):
        if self.j is None and database is not None:
            self.j = database.lookup('j')

c = C(10, database=my_database)

在這種情況下,fields() 將為 ij 返回 Field 物件,但不會為 database 返回。

凍結的例項

建立真正不可變的 Python 物件是不可能的。但是,透過向 @dataclass 裝飾器傳遞 frozen=True,你可以模擬不可變性。在這種情況下,資料類會向類中新增 __setattr__()__delattr__() 方法。這些方法在被呼叫時會引發 FrozenInstanceError

使用 frozen=True 會有微小的效能損失:__init__() 不能使用簡單的賦值來初始化欄位,而必須使用 object.__setattr__()

繼承

當資料類由 @dataclass 裝飾器建立時,它會按反向 MRO 順序(即從 object 開始)遍歷類的所有基類,對於它找到的每個資料類,將其欄位新增到欄位的有序對映中。在添加了所有基類的欄位後,它再將自己的欄位新增到有序對映中。所有生成的方法都將使用這個組合的、計算出的有序欄位對映。因為欄位是按插入順序排列的,所以派生類會覆蓋基類。一個例子:

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

最終的欄位列表按順序為 x, y, zx 的最終型別是 int,如類 C 中所指定。

C 生成的 __init__() 方法將如下所示:

def __init__(self, x: int = 15, y: int = 0, z: int = 10):

__init__() 中僅限關鍵字引數的重新排序

在計算出 __init__() 所需的引數後,任何僅關鍵字引數都會被移動到所有常規(非僅關鍵字)引數之後。這是 Python 中實現僅關鍵字引數的要求:它們必須位於非僅關鍵字引數之後。

在此示例中,Base.yBase.wD.t 是僅關鍵字欄位,而 Base.xD.z 是常規欄位:

@dataclass
class Base:
    x: Any = 15.0
    _: KW_ONLY
    y: int = 0
    w: int = 1

@dataclass
class D(Base):
    z: int = 10
    t: int = field(kw_only=True, default=0)

D 生成的 __init__() 方法將如下所示:

def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):

請注意,引數已從它們在欄位列表中出現的順序重新排序:派生自常規欄位的引數後面跟著派生自僅關鍵字欄位的引數。

僅關鍵字引數的相對順序在重新排序的 __init__() 引數列表中得以保持。

預設工廠函式

如果一個 field() 指定了一個 default_factory,當需要該欄位的預設值時,它將被以零個引數呼叫。例如,要建立一個列表的新例項,使用:

mylist: list = field(default_factory=list)

如果一個欄位從 __init__() 中排除了(使用 init=False),並且該欄位還指定了 default_factory,那麼預設工廠函式將總是從生成的 __init__() 函式中被呼叫。這是因為沒有其他方法可以給該欄位一個初始值。

可變的預設值

Python 將預設成員變數值儲存在類屬性中。考慮這個不使用資料類的例子:

class C:
    x = []
    def add(self, element):
        self.x.append(element)

o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x

請注意,類 C 的兩個例項共享同一個類變數 x,正如預期的那樣。

使用資料類,如果 這段程式碼是有效的:

@dataclass
class D:
    x: list = []      # This code raises ValueError
    def add(self, element):
        self.x.append(element)

它會生成類似這樣的程式碼:

class D:
    x = []
    def __init__(self, x=x):
        self.x = x
    def add(self, element):
        self.x.append(element)

assert D().x is D().x

這與使用類 C 的原始示例有相同的問題。也就是說,在建立類例項時未指定 x 值的兩個類 D 的例項將共享同一份 x 的副本。因為資料類僅使用正常的 Python 類建立,它們也共享此行為。資料類沒有通用的方法來檢測這種情況。相反,如果 @dataclass 裝飾器檢測到不可雜湊的預設引數,它將引發 ValueError。其假設是,如果一個值是不可雜湊的,那麼它是可變的。這是一個部分解決方案,但它確實可以防止許多常見的錯誤。

使用預設工廠函式是一種為欄位建立可變型別新例項作為預設值的方法:

@dataclass
class D:
    x: list = field(default_factory=list)

assert D().x is not D().x

在 3.11 版本發生變更: 現在不再是查詢並禁止 list, dictset 型別的物件,而是不允許不可雜湊物件作為預設值。不可雜湊性被用來近似可變性。

描述符型別的欄位

被賦予描述符物件作為其預設值的欄位具有以下特殊行為:

  • 傳遞給資料類 __init__() 方法的欄位值將被傳遞給描述符的 __set__() 方法,而不是覆蓋描述符物件。

  • 類似地,當獲取或設定欄位時,會呼叫描述符的 __get__()__set__() 方法,而不是返回或覆蓋描述符物件。

  • 為了確定一個欄位是否包含預設值,@dataclass 將以其類訪問形式呼叫描述符的 __get__() 方法:descriptor.__get__(obj=None, type=cls)。如果描述符在這種情況下返回一個值,它將被用作欄位的預設值。另一方面,如果描述符在這種情況下引發 AttributeError,則該欄位將沒有預設值。

class IntConversionDescriptor:
    def __init__(self, *, default):
        self._default = default

    def __set_name__(self, owner, name):
        self._name = "_" + name

    def __get__(self, obj, type):
        if obj is None:
            return self._default

        return getattr(obj, self._name, self._default)

    def __set__(self, obj, value):
        setattr(obj, self._name, int(value))

@dataclass
class InventoryItem:
    quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)

i = InventoryItem()
print(i.quantity_on_hand)   # 100
i.quantity_on_hand = 2.5    # calls __set__ with 2.5
print(i.quantity_on_hand)   # 2

請注意,如果一個欄位被註解為描述符型別,但沒有被賦予一個描述符物件作為其預設值,該欄位將像一個普通欄位一樣工作。