一起使用数据类和属性时的奇怪问题

Mic*_*ard 5 python properties python-3.x python-dataclasses

我在尝试将数据类与属性一起使用时遇到了一个奇怪的问题。

我把它降到最低限度来重现它:

import dataclasses

@dataclasses.dataclass
class FileObject:
    _uploaded_by: str = dataclasses.field(default=None, init=False)
    uploaded_by: str = None

    def save(self):
        print(self.uploaded_by)

    @property
    def uploaded_by(self):
        return self._uploaded_by

    @uploaded_by.setter
    def uploaded_by(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

p = FileObject()
p.save()
Run Code Online (Sandbox Code Playgroud)

这输出:

Setter Called with Value  <property object at 0x7faeb00150b0>
<property object at 0x7faeb00150b0>
Run Code Online (Sandbox Code Playgroud)

我希望得到 None 而不是

我在这里做错了什么还是偶然发现了一个错误?

在阅读@juanpa.arrivillaga 的回答后,我认为使 upload_by 和 InitVar 可能会解决问题,但它仍然返回一个属性对象。我想是因为这个,他说:

datalcas 机制将对类主体中的类型注释变量的任何赋值解释为 created 的默认值 __init__

我能找到的适用于默认值的唯一选项是从数据类定义中删除 uploadby 并编写一个实际的__init__. 这有一个不幸的副作用,即要求您__init__手动为数据类编写一个,这否定了使用数据类的一些价值。这是我所做的:

import dataclasses

@dataclasses.dataclass
class FileObject:
    _uploaded_by: str = dataclasses.field(default=None, init=False)
    uploaded_by: dataclasses.InitVar=None
    other_attrs: str = None

    def __init__(self, uploaded_by=None, other_attrs=None):
        self._uploaded_by = uploaded_by
        self.other_attrs = other_attrs

    def save(self):
        print("Uploaded by: ", self.uploaded_by)
        print("Other Attrs: ", self.other_attrs)

    @property
    def uploaded_by(self):
        if not self._uploaded_by:
            print("Doing expensive logic that should not be repeated")
        return self._uploaded_by

p = FileObject(other_attrs="More Data")
p.save()

p2 = FileObject(uploaded_by='Already Computed', other_attrs="More Data")
p2.save()
Run Code Online (Sandbox Code Playgroud)

哪些输出:

Doing expensive logic that should not be repeated
Uploaded by:  None
Other Attrs:  More Data
Uploaded by:  Already Computed
Other Attrs:  More Data
Run Code Online (Sandbox Code Playgroud)

这样做的负面影响:

  • 您必须编写样板__init__(我的实际用例大约有 20 个属性)
  • 您在 中丢失了__repr__upload_by,但它在 _uploaded_by 中
  • 对 asdict、astuple、dataclasses.replace 的调用未正确处理

所以这真的不是解决问题的方法

我在 Python Bug Tracker 上提交了一个错误:https : //bugs.python.org/issue39247

jua*_*aga 5

因此,不幸的是,@property语法总是被解释为对uploaded_by(因为,嗯,它)的赋值。机器dataclass将其解释为默认值,因此它传递属性对象!它相当于:

In [11]: import dataclasses
    ...:
    ...: @dataclasses.dataclass
    ...: class FileObject:
    ...:     uploaded_by: str
    ...:     _uploaded_by: str = dataclasses.field(repr=False, init=False)
    ...:     def save(self):
    ...:         print(self.uploaded_by)
    ...:
    ...:     def _get_uploaded_by(self):
    ...:         return self._uploaded_by
    ...:
    ...:     def _set_uploaded_by(self, uploaded_by):
    ...:         print('Setter Called with Value ', uploaded_by)
    ...:         self._uploaded_by = uploaded_by
    ...:     uploaded_by = property(_get_uploaded_by, _set_uploaded_by)
    ...: p = FileObject()
    ...: p.save()
Setter Called with Value  <property object at 0x10761e7d0>
<property object at 0x10761e7d0>
Run Code Online (Sandbox Code Playgroud)

其本质上是这样的:

In [13]: @dataclasses.dataclass
    ...: class Foo:
    ...:     bar:int = 1
    ...:     bar = 2
    ...:

In [14]: Foo()
Out[14]: Foo(bar=2)
Run Code Online (Sandbox Code Playgroud)

我不认为有一个干净的方法来解决这个问题,也许它可以被认为是一个错误,但实际上,不确定解决方案应该是什么,因为本质上,datalcas 机制解释对类型注释变量的任何赋值类主体作为创建的默认值__init__。您也许可以对@property语法进行特殊处理,或者只是property对象本身,因此至少@property和 的行为x = property(set_x, get_x)是一致的......

需要明确的是,以下类型的作品

In [22]: import dataclasses
    ...:
    ...: @dataclasses.dataclass
    ...: class FileObject:
    ...:     uploaded_by: str
    ...:     _uploaded_by: str = dataclasses.field(repr=False, init=False)
    ...:     @property
    ...:     def uploaded_by(self):
    ...:         return self._uploaded_by
    ...:     @uploaded_by.setter
    ...:     def uploaded_by(self, uploaded_by):
    ...:         print('Setter Called with Value ', uploaded_by)
    ...:         self._uploaded_by = uploaded_by
    ...:
    ...: p = FileObject(None)
    ...: print(p.uploaded_by)
Setter Called with Value  None
None

In [23]: FileObject()
Setter Called with Value  <property object at 0x1086debf0>
Out[23]: FileObject(uploaded_by=<property object at 0x1086debf0>)
Run Code Online (Sandbox Code Playgroud)

但请注意,您无法设置有用的默认值!它总是会占用该属性...更糟糕的是,IMO,如果您不想要默认值,它总是会创建一个!

编辑:找到了一个潜在的解决方法!

这应该是显而易见的,但您可以只在类上设置属性对象。

import dataclasses
import typing
@dataclasses.dataclass
class FileObject:
    uploaded_by:typing.Optional[str]=None

    def _uploaded_by_getter(self):
        return self._uploaded_by

    def _uploaded_by_setter(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

FileObject.uploaded_by = property(
    FileObject._uploaded_by_getter,
    FileObject._uploaded_by_setter
)
p = FileObject()
print(p)
print(p.uploaded_by)
Run Code Online (Sandbox Code Playgroud)