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__()
方法,即使這樣做可能不安全。否則,將根據 eq 和 frozen 的設定來生成__hash__()
方法。預設值為False
。內建的
hash()
在物件被新增到雜湊集合(如字典和集合)時會使用__hash__()
。擁有__hash__()
意味著類的例項是不可變的。可變性是一個複雜的屬性,取決於程式設計師的意圖、__eq__()
的存在和行為,以及@dataclass
裝飾器中 eq 和 frozen 標誌的值。預設情況下,
@dataclass
不會隱式新增__hash__()
方法,除非這樣做是安全的。它也不會新增或更改已顯式定義的__hash__()
方法。如__hash__()
文件中所述,將類屬性__hash__ = None
設定為對 Python 具有特定含義。如果
__hash__()
沒有被顯式定義,或者它被設定為None
,那麼@dataclass
可能 會新增一個隱式的__hash__()
方法。雖然不推薦,但你可以使用unsafe_hash=True
來強制@dataclass
建立一個__hash__()
方法。這可能適用於你的類在邏輯上是不可變的,但仍然可以被修改的情況。這是一個特殊的用例,應仔細考慮。以下是隱式建立
__hash__()
方法的規則。請注意,你不能在資料類中既有顯式的__hash__()
方法,又設定unsafe_hash=True
;這將導致TypeError
。如果 eq 和 frozen 均為真值,預設情況下
@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=True
時,向基類的__init_subclass__()
傳遞引數將導致TypeError
。解決方法是使用不帶引數的__init_subclass__
或使用預設值。詳情見 gh-91126。在 3.10 版本加入。
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'
在此示例中,
a
和b
都將包含在新增的__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: 如果提供,它必須是一個零引數的可呼叫物件,當需要此欄位的預設值時將被呼叫。除了其他用途,這可以用來指定具有可變預設值的欄位,如下所述。同時指定 default 和 default_factory 是錯誤的。
init: 如果為真(預設值),此欄位將作為引數包含在生成的
__init__()
方法中。repr: 如果為真(預設值),此欄位將包含在生成的
__repr__()
方法返回的字串中。hash: 這可以是一個布林值或
None
。如果為真,此欄位將包含在生成的__hash__()
方法中。如果為假,此欄位將從生成的__hash__()
中排除。如果為None
(預設值),則使用 compare 的值:這通常是預期的行為,因為如果欄位用於比較,它就應該包含在雜湊中。不鼓勵將此值設定為除None
之外的任何值。將
hash=False
但compare=True
的一個可能原因是,如果某個欄位計算雜湊值成本高昂,而該欄位對於相等性測試是必需的,並且還有其他欄位對型別的雜湊值有貢獻。即使一個欄位被從雜湊中排除,它仍將用於比較。compare: 如果為真(預設值),此欄位將包含在生成的相等性和比較方法中(
__eq__()
,__gt__()
, 等)。metadata: 這可以是一個對映或
None
。None
被視為空字典。這個值被包裝在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.x
和C.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
。不返回ClassVar
或InitVar
型別的偽欄位。
- 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.Any
。init、repr、eq、order、unsafe_hash、frozen、match_args、kw_only、slots 和 weakref_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__()
引數。在此示例中,欄位
y
和z
將被標記為僅關鍵字欄位:@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)
凍結的例項¶
建立真正不可變的 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
, z
。x
的最終型別是 int
,如類 C
中所指定。
為 C
生成的 __init__()
方法將如下所示:
def __init__(self, x: int = 15, y: int = 0, z: int = 10):
__init__()
中僅限關鍵字引數的重新排序¶
在計算出 __init__()
所需的引數後,任何僅關鍵字引數都會被移動到所有常規(非僅關鍵字)引數之後。這是 Python 中實現僅關鍵字引數的要求:它們必須位於非僅關鍵字引數之後。
在此示例中,Base.y
、Base.w
和 D.t
是僅關鍵字欄位,而 Base.x
和 D.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
描述符型別的欄位¶
被賦予描述符物件作為其預設值的欄位具有以下特殊行為:
傳遞給資料類
__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
請注意,如果一個欄位被註解為描述符型別,但沒有被賦予一個描述符物件作為其預設值,該欄位將像一個普通欄位一樣工作。