繁体   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