簡體   English   中英

如何使數據類更好地與 __slots__ 配合使用?

[英]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則與插槽沖突的默認字段值也可能由數據類機制創建。

正如答案中已經指出的那樣,數據類中的數據類不能生成槽,原因很簡單,必須在創建類之前定義槽。

事實上, 數據類PEP明確提到了這一點:

至少對於初始版本,將不支持__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=Truedataclass一起使用,以提高內存效率:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM