簡體   English   中英

在 Python 數據類中使用描述符作為字段的正確方法是什么?

[英]What is the proper way to use descriptors as fields in Python dataclasses?

我一直在玩弄 python 數據類並且想知道:制作一個或某些字段描述符的最優雅或最 pythonic 的方法是什么?

在下面的示例中,我定義了一個 Vector2D class,它應該根據其長度進行比較。

from dataclasses import dataclass, field
from math import sqrt

@dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)
    length: int = field(init=False)
    
    def __post_init__(self):
        type(self).length = property(lambda s: sqrt(s.x**2+s.y**2))

Vector2D(3,4) > Vector2D(4,1) # True

雖然這段代碼有效,但每次創建實例時它都會觸及 class ,是否有更易讀/更少 hacky/更有意的方式來一起使用數據類和描述符?

僅將長度作為屬性而不是字段就可以了,但這意味着我必須編寫__lt__等。 我自己。

我發現的另一個解決方案同樣沒有吸引力:

@dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)
    length: int = field(init=False)
    
    @property
    def length(self):
        return sqrt(self.x**2+self.y**2)
    
    @length.setter
    def length(self, value):
        pass

引入一個無操作設置器是必要的,因為顯然數據類創建的 init 方法試圖分配給length即使沒有默認值並且它顯式設置init=False ...

當然必須有更好的方法嗎?

可能不會回答您的確切問題,但您提到您不想將長度作為屬性和非字段的原因是因為您必須

自己寫__lt__

雖然你必須自己實現__lt__ ,但你實際上可以擺脫實現

from functools import total_ordering
from dataclasses import dataclass, field
from math import sqrt

@total_ordering
@dataclass
class Vector2D:
    x: int
    y: int

    @property
    def length(self):
        return sqrt(self.x ** 2 + self.y ** 2)

    def __lt__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented

        return self.length < other.length

    def __eq__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented

        return self.length == other.length


print(Vector2D(3, 4) > Vector2D(4, 1))

這樣做的原因是因為total_ordering只是添加了基於__eq____lt__所有其他相等方法

我不認為您提供的示例是您嘗試做的事情的好用例。 盡管如此,為了完整起見,以下是您問題的可能解決方案:

@dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)
    length: int = field(default=property(lambda s: sqrt(s.x**2+s.y**2)), init=False)

這是有效的,因為除非值是列表、字典或集合,否則dataclass將默認值設置為類屬性的值。

盡管您可以手動實現@property和其他方法,但這會使您失去其他理想的功能,例如在這種情況下使用hash=False如果您想在dict中使用Vector2D 此外,讓它為您實現雙下划線方法可以讓您的代碼更不容易出錯,例如您不能忘記return NotImplemented ,這是一個常見的錯誤。

缺點是實現正確的類型提示並不容易,並且可能會有一些小的注意事項,但是一旦實現了類型提示,它就可以在任何地方輕松使用。

屬性(描述符)類型提示:

import sys
from typing import Any, Optional, Protocol, TypeVar, overload

if sys.version_info < (3, 9):
    from typing import Type
else:
    from builtins import type as Type

IT = TypeVar("IT", contravariant=True)
CT = TypeVar("CT", covariant=True)
GT = TypeVar("GT", covariant=True)
ST = TypeVar("ST", contravariant=True)


class Property(Protocol[CT, GT, ST]):

    # Get default attribute from a class.
    @overload
    def __get__(self, instance: None, owner: Type[Any]) -> CT:
        ...

    # Get attribute from an instance.
    def __get__(self, instance: IT, owner: Optional[Type[IT]] = ...) -> GT:
        ...

    def __get__(self, instance, owner=None):
        ...

    def __set__(self, instance: Any, value: ST) -> None:
        ...

從這里開始,我們現在可以在使用數據類時對property dataclass進行類型提示。 如果您需要使用field(...)中的其他選項,請使用field(default=property(...)) ) 。

import sys
import typing
from dataclasses import dataclass, field
from math import hypot

# Use for read-only property.
if sys.version_info < (3, 11):
    from typing import NoReturn as Never
else:
    from typing import Never


@dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)

    # Properties return themselves as their default class variable.
    # Read-only properties never allow setting a value.
    
    # If init=True, then it would assign self.length = Vector2D.length for the
    # default factory.
    
    # Setting repr=False for consistency with init=False.
    length: Property[property, float, Never] = field(
        default=property(lambda v: hypot(v.x, v.y)),
        init=False,
        repr=False,
    )


v1 = Vector2D(3, 4)
v2 = Vector2D(6, 8)

if typing.TYPE_CHECKING:
    reveal_type(Vector2D.length)  # builtins.property
    reveal_type(v1.length)        # builtins.float

assert v1.length == 5.0
assert v2.length == 10.0
assert v1 < v2

mypy Playground上試試。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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