具有数据类属性的必需位置参数

cor*_*vus 7 python properties python-dataclasses

似乎已经对此进行了相当多的讨论。我发现这篇文章特别有帮助,它似乎提供了最好的解决方案之一。

但推荐的解决方案存在问题。

嗯,一开始看起来效果很好。考虑一个没有属性的简单测试用例:

@dataclass
class Foo:
    x: int
Run Code Online (Sandbox Code Playgroud)
>>> # Instantiate the class
>>> f = Foo(2)
>>> # Nice, it works!
>>> f.x
2
Run Code Online (Sandbox Code Playgroud)

x现在尝试使用推荐的解决方案作为属性来实现:

@dataclass
class Foo:
    x: int
    _x: int = field(init=False, repr=False)
    
    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = value
Run Code Online (Sandbox Code Playgroud)
>>> # Instantiate while explicitly passing `x`
>>> f = Foo(2)
>>> # Still appears to work
>>> f.x
2
Run Code Online (Sandbox Code Playgroud)

可是等等...

>>> # Instantiate without any arguments
>>> f = Foo()
>>> # Oops...! Property `x` has never been initialized. Now we have a bug :(
>>> f.x
<property object at 0x10d2a8130>
Run Code Online (Sandbox Code Playgroud)

实际上,这里的预期行为是:

>>> # Instantiate without any arguments
>>> f = Foo()
TypeError: __init__() missing 1 required positional argument: 'x'
Run Code Online (Sandbox Code Playgroud)

似乎数据类字段已被属性覆盖...有没有想过如何解决这个问题?

有关的:

Jam*_*mes 8

在数据类中使用共享方法参数名称的属性__init__会产生有趣的副作用。当类在不带参数的情况下实例化时,该property对象将作为默认值传递。

作为解决方法,您可以使用检查xin的类型__post_init__

@dataclass
class Foo:
    x: int
    _x: int = field(init=False, repr=False)

    def __post_init__(self):
        if isinstance(self.x, property):
            raise TypeError("__init__() missing 1 required positional argument: 'x'")

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value
Run Code Online (Sandbox Code Playgroud)

现在,在实例化 时Foo,不传递任何参数会引发预期的异常。

f = Foo()
# raises TypeError

f = Foo(1)
f
# returns
Foo(x=1)
Run Code Online (Sandbox Code Playgroud)

这是使用多个属性时的更通用的解决方案。这用于InitVar将参数传递给__post_init__方法。它确实要求首先列出属性,并且它们各自的存储属性具有相同的名称并带有前导下划线。

这非常 hacky,并且属性不再显示在repr.

@dataclass
class Foo:
    x: InitVar[int]
    y: InitVar[int]
    _x: int = field(init=False, repr=False, default=None)
    _y: int = field(init=False, repr=False, default=None)

    def __post_init__(self, *args):
        if m := sum(isinstance(arg, property) for arg in args):
            s = 's' if m>1 else ''
            raise TypeError(f'__init__() missing {m} required positional argument{s}.')

        arg_names = inspect.getfullargspec(self.__class__).args[1:]
        for arg_name, val in zip(arg_names, args):
            self.__setattr__('_' + arg_name, val)

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = value
Run Code Online (Sandbox Code Playgroud)