对在后续 class 实例化中覆盖 属性 数据的自定义 "Typed Property" 模式进行故障排除

Troubleshooting a custom "Typed Property" pattern that overwrites property data on subsequent class instantiations

我正在尝试组装一个类似于 Python 的 dataclass 的实用程序,但它为我提供了一些我可以做的额外事情,例如对分配进行类型检查等。我' m 基本上遵循 O'Reilly Python Cookbook, 3rd Edition

中“9.21 避免重复 属性 方法”中描述的一般模式

我运行遇到一个问题,即 MyClass 的后续实例化会覆盖它的其他实例的数据。这是我正在做的事情的简化版本:

def typed_property(name, default=None):
    varname = "_" + name

    @property
    def prop(self):
        if not hasattr(self, varname):
            setattr(self, varname, default)
        return getattr(self, varname)

    @prop.setter
    def prop(self, value):
        setattr(self, varname, value)

    return prop

class MyClass(object):
    data = typed_property("data", default={})

如果我 运行 使用这样的东西:

obj1 = MyClass()
obj1.data["test"] = set()
obj1.data["test"].add(1)
print(">>> initial")
print("(1) id(obj1) =", id(obj1))
print("(2) obj1.data =", obj1.data)

print(">>> create obj2")
obj2 = MyClass()
obj2.data["test"] = set()

print("(3) obj1.data =", obj1.data) 
print("(4) obj2.data =", obj2.data)

print(">>> ID's:")
print("(5) id(obj1) =", id(obj1))
print("(6) id(obj2) =", id(obj2))
print("(7) id(obj1.data) =", id(obj1.data))
print("(8) id(obj2.data) =", id(obj2.data))

我得到这个输出:

>>> initial
(1) id(obj1) = 4429860720
(2) obj1.data = {'test': {1}}
>>> create obj2
(3) obj1.data = {'test': set()}
(4) obj2.data = {'test': set()}
>>> ID's:
(5) id(obj1) = 4429860720
(6) id(obj2) = 4428300544
(7) id(obj1.data) = 4430915136
(8) id(obj2.data) = 4430915136

这是不正确的。在这种情况下,当我创建 obj2 时,obj1.data 条目丢失了。我希望第 (2) 和 (3) 行的输出匹配,但 (3) 和 (4) 现在是相同的。我可以在 (7) 和 (8) 中看到 属性 从两个 类 引用相同的位置,所以我可以看到 obj2 的创建踩到了 obj1.

认为我知道发生了什么但我想确认一下。我认为问题出在 typed_property 我的 setattr(self, varname, default) 行中 default 值的赋值。 typed_propertydefault 参数实际上是对内存中单个对象的引用...所以当该分配发生时,真正发生的是对 default 的引用被分配给我的 属性的内部存储,对吧?

我可以通过将 setattr(self, varname, default) 更改为 setattr(self, varname, copy.deepcopy(default) 来解决问题,但这是最好的方法吗?

理想情况下,我只想使用 dataclass,但遗憾的是它无法处理我们需要涵盖的所有情况。

如果有人能用 default 函数参数阐明或 post 一个 link 一些信息来解释内存中发生的事情,这将有助于我的理解。参数总是固定引用还是由 Python 创建一次的默认参数?

确认我的想法或了解有关语言内部的更多信息会很好。

谢谢!

您是正确的,对默认值的引用在 MyClass 个实例之间共享,您的测试也证实了这一点。理解为什么会发生这种情况的一个重要信息是,除了例如。 __init__ 函数的主体,class 主体只被计算一次;关于 class 创建。不可能存在两个或多个不同的默认对象,因为在创建新的 MyClass 实例时不会执行与其相关的代码。

dataclasses 通过将 default 用于不可变默认值和 default_factory 用于可变默认值来解决在 class 主体中定义默认值的问题。我建议只为您的构造使用类似的模式,如果您确实想要在实例之间共享一个对象,那么创建副本的替代方案必然会产生问题:

def typed_property(name, default_factory=lambda: None):
    varname = "_" + name

    @property
    def prop(self):
        if not hasattr(self, varname):
            setattr(self, varname, default_factory())
        return getattr(self, varname)

    @prop.setter
    def setter(self, value):
        setattr(self, varname, value)

    return prop

class MyClass:
    data = typed_property("data", default_factory=dict)

通过将 dict 函数作为使用初始 settatr 调用的工厂传递,您可以为每个实例获得新的字典对象。如果要共享某个对象 o = MySharedObject(),只需将字段定义为 typed_property("shared_data", lambda: o)。或者一路走下去并定义 defaultdefault_factory 参数,但它会使 typed_property 实现变得有点复杂,检查是否只使用了一个或另一个以及什么没有。

并证明它现在有效:

>>> a = MyClass()
>>> id(a.data)
140412230286208
>>> b = MyClass()
>>> id(b.data)
140412230275328
>>> id(a.data)
140412230286208