[英]How can dataclasses be made to work better with __slots__?
決定從 Python 3.7 的數據類中刪除對__slots__
直接支持。
盡管如此, __slots__
仍然可以與數據類一起使用:
from dataclasses import dataclass
@dataclass
class C():
__slots__ = "x"
x: int
但是,由於__slots__
工作方式,無法為數據類字段分配默認值:
from dataclasses import dataclass
@dataclass
class C():
__slots__ = "x"
x: int = 1
這會導致錯誤:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 'x' in __slots__ conflicts with class variable
如何使__slots__
和默認dataclass
字段一起工作?
2021 更新:對__slots__
直接支持已添加到 python 3.10。 我將這個答案留給后人,不會更新。
這個問題並不是數據類獨有的。 任何沖突的類屬性都會在一個插槽上踩踏:
>>> class Failure:
... __slots__ = tuple("xyz")
... x=1
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 'x' in __slots__ conflicts with class variable
這就是插槽的工作方式。 發生錯誤是因為__slots__
為每個插槽名稱創建了一個類級描述符對象:
>>> class Success:
... __slots__ = tuple("xyz")
...
>>>
>>> type(Success.x)
<class 'member_descriptor'>
為了防止這個變量名沖突的錯誤,在類對象被實例化之前必須改變類命名空間,這樣在類中不會有兩個對象競爭同一個成員名:
出於這個原因,父類上的__init_subclass__
方法是不夠的,類裝飾器也是不夠的,因為在這兩種情況下,當這些函數接收到類來改變它時,類對象已經被創建。
在更改槽機制以提供更大的靈活性之前,或者語言本身提供了在類對象實例化之前更改類命名空間的機會之前,我們唯一的選擇是使用元類。
為解決此問題而編寫的任何元類必須至少:
__dict__
類中(以便dataclass
機器可以找到它們)dataclass
裝飾器__dict__
插槽該怎么辦)至少可以說,這是一項極其復雜的工作。 像下面這樣定義類會更容易 - 沒有默認值,以便根本不會發生沖突 - 然后添加一個默認值。
未更改的數據類如下所示:
@dataclass
class C:
__slots__ = "x"
x: int
更改很簡單。 更改__init__
簽名以反映所需的默認值,然后更改__dataclass_fields__
以反映默認值的存在。
from functools import wraps
def change_init_signature(init):
@wraps(init)
def __init__(self, x=1):
init(self,x)
return __init__
C.__init__ = change_init_signature(C.__init__)
C.__dataclass_fields__["x"].default = 1
測試:
>>> C()
C(x=1)
>>> C(2)
C(x=2)
>>> C.x
<member 'x' of 'C' objects>
>>> vars(C())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute
有用!
setmember
裝飾器通過一些努力,可以使用所謂的setmember
裝飾器以上述方式自動更改類。 這將需要偏離 dataclasses API,以便在類主體內部以外的位置定義默認值,可能類似於:
@setmember(x=field(default=1))
@dataclass
class C:
__slots__="x"
x: int
同樣的事情也可以通過父類上的__init_subclass__
方法來完成:
class SlottedDataclass:
def __init_subclass__(cls, **kwargs):
cls.__init_subclass__()
# make the class changes here
class C(SlottedDataclass, x=field(default=1)):
__slots__ = "x"
x: int
如上所述,另一種可能性是 Python 語言改變插槽機制以提供更大的靈活性。 這樣做的一種方法可能是在類定義時更改槽描述符本身以存儲類級別的數據。
這可能可以通過提供一個dict
作為__slots__
參數來完成(見下文)。 類級別的數據(x 為 1,y 為 2)可以只存儲在描述符本身上供以后檢索:
class C:
__slots__ = {"x": 1, "y": 2}
assert C.x.value == 1
assert C.y.value == y
一個困難:可能只希望在某些插槽上存在slot_member.value
在其他插槽上不存在。 這可以通過從新的slottools
庫中導入一個空槽工廠來實現:
from slottools import nullslot
class C:
__slots__ = {"x": 1, "y": 2, "z": nullslot()}
assert not hasattr(C.z, "value")
上面建議的代碼風格與 dataclasses API 有所不同。 然而,插槽機制本身甚至可以改變以允許這種風格的代碼,特別考慮到數據類 API 的適應:
class C:
__slots__ = "x", "y", "z"
x = 1 # 1 is stored on C.x.value
y = 2 # 2 is stored on C.y.value
assert C.x.value == 1
assert C.y.value == y
assert not hasattr(C.z, "value")
另一種可能性是改變/准備(與元類的__prepare__
方法同義)類命名空間。
目前,沒有機會(除了編寫元類)在類對象被實例化之前編寫更改類命名空間的代碼,並且插槽機制開始工作。 這可以通過創建一個用於預先准備類名稱空間的鈎子來改變,並使其僅在運行該鈎子后才產生抱怨名稱沖突的錯誤。
這個所謂的__prepare_slots__
鈎子看起來像這樣,我認為還不錯:
from dataclasses import dataclass, prepare_slots
@dataclass
class C:
__slots__ = ('x',)
__prepare_slots__ = prepare_slots
x: int = field(default=1)
dataclasses.prepare_slots
函數只是一個函數——類似於__prepare__
方法——它接收類命名空間並在創建類之前更改它。 特別是對於這種情況,默認數據類字段值將存儲在其他一些方便的位置,以便在創建槽描述符對象后可以檢索它們。
* 請注意,如果正在使用dataclasses.field
則與插槽沖突的默認字段值也可能由數據類機制創建。
正如答案中已經指出的那樣,數據類中的數據類不能生成槽,原因很簡單,必須在創建類之前定義槽。
至少對於初始版本,將不支持
__slots__
。__slots__
需要在創建類時添加。 在創建類之后調用數據類裝飾器,因此為了添加__slots__
裝飾器必須創建一個新類,設置__slots__
並返回它。 因為這種行為有點令人驚訝,數據類的初始版本將不支持自動設置__slots__
。
我想使用插槽,因為我需要在另一個項目中初始化很多很多數據類實例。 我最終編寫了自己的數據類替代實現,它支持這一點,還有一些額外的功能: dataclassy 。
dataclassy 使用元類方法,它具有許多優點——它支持裝飾器繼承,大大降低了代碼復雜性,當然還有槽的生成。 使用 dataclassy 可以實現以下功能:
from dataclassy import dataclass
@dataclass(slots=True)
class Pet:
name: str
age: int
species: str
fluffy: bool = True
打印Pet.__slots__
輸出預期的{'name', 'age', 'species', 'fluffy'}
,實例沒有__dict__
屬性,因此對象的整體內存占用較低。 這些觀察結果表明__slots__
已成功生成並且有效。 另外,正如所證明的,默認值工作得很好。
我為這個問題找到的最少涉及的解決方案是使用object.__setattr__
指定一個自定義__init__
來分配值。
@dataclass(init=False, frozen=True)
class MyDataClass(object):
__slots__ = (
"required",
"defaulted",
)
required: object
defaulted: Optional[object]
def __init__(
self,
required: object,
defaulted: Optional[object] = None,
) -> None:
super().__init__()
object.__setattr__(self, "required", required)
object.__setattr__(self, "defaulted", defaulted)
按照Rick slotted_dataclass
的建議,我創建了一個slotted_dataclass
裝飾器。 在關鍵字參數中,它可以采用您在[field]: [type] =
之后指定的任何內容,而沒有__slots__
的數據類 - fields 和field(...)
默認值。 指定應該去舊@dataclass
構造函數的參數也是可能的,但在字典對象中作為第一個位置參數。 所以這:
@dataclass(frozen=True)
class Test:
a: dict = field(repr=False)
b: int = 42
c: list = field(default_factory=list)
會成為:
@slotted_dataclass({'frozen': True}, a=field(repr=False), b=42, c=field(default_factory=list))
class Test:
__slots__ = ('a', 'b', 'c')
a: dict
b: int
c: list
這是這個新裝飾器的源代碼:
def slotted_dataclass(dataclass_arguments=None, **kwargs):
if dataclass_arguments is None:
dataclass_arguments = {}
def decorator(cls):
old_attrs = {}
for key, value in kwargs.items():
old_attrs[key] = getattr(cls, key)
setattr(cls, key, value)
cls = dataclass(cls, **dataclass_arguments)
for key, value in old_attrs.items():
setattr(cls, key, value)
return cls
return decorator
上面的代碼利用了dataclasses
模塊通過在類上調用getattr
來獲取默認字段值的事實。 這使得可以通過替換類的__dict__
中的適當字段來提供我們的默認值(這是通過使用setattr
函數在代碼中完成的)。 由@dataclass
裝飾器生成的類將與通過在=
之后指定那些生成的類完全相同,就像如果類不包含__slots__
。
但是由於帶有__slots__
的類的__dict__
包含member_descriptor
對象:
>>> class C:
... __slots__ = ('a', 'b', 'c')
...
>>> C.__dict__['a']
<member 'a' of 'C' objects>
>>> type(C.__dict__['a'])
<class 'member_descriptor'>
一件好事是備份這些對象並在@dataclass
裝飾器完成其工作后恢復它們,這是通過使用old_attrs
字典在代碼中完成的。
另一種解決方案是在類主體內從類型化注釋生成 slot 參數。 這看起來像:
@dataclass
class Client:
first: str
last: str
age_of_signup: int
__slots__ = slots(__annotations__)
其中slots
函數是:
def slots(anotes: Dict[str, object]) -> FrozenSet[str]:
return frozenset(anotes.keys())
運行將生成一個插槽參數,如下所示: frozenset({'first', 'last', 'age_of_signup})
這需要它上面的注釋並生成一組指定的名稱。 這里的限制是您必須為每個類重新鍵入__slots__ = slots(__annotations__)
行,並且它必須位於所有注釋下方,並且它不適用於具有默認參數的注釋。 這還有一個優點,即插槽參數永遠不會與指定的注釋沖突,因此您可以隨意添加或刪除成員,而不必擔心維護單獨的列表。
在 Python 3.10+ 中,您可以將slots=True
與dataclass
一起使用,以提高內存效率:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
x: int = 0
y: int = 0
這樣您也可以設置默認字段值。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.