对在后续 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_property
的 default
参数实际上是对内存中单个对象的引用...所以当该分配发生时,真正发生的是对 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)
。或者一路走下去并定义 default
和 default_factory
参数,但它会使 typed_property
实现变得有点复杂,检查是否只使用了一个或另一个以及什么没有。
并证明它现在有效:
>>> a = MyClass()
>>> id(a.data)
140412230286208
>>> b = MyClass()
>>> id(b.data)
140412230275328
>>> id(a.data)
140412230286208
我正在尝试组装一个类似于 Python 的 dataclass
的实用程序,但它为我提供了一些我可以做的额外事情,例如对分配进行类型检查等。我' m 基本上遵循 O'Reilly Python Cookbook, 3rd Edition
我运行遇到一个问题,即 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_property
的 default
参数实际上是对内存中单个对象的引用...所以当该分配发生时,真正发生的是对 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)
。或者一路走下去并定义 default
和 default_factory
参数,但它会使 typed_property
实现变得有点复杂,检查是否只使用了一个或另一个以及什么没有。
并证明它现在有效:
>>> a = MyClass()
>>> id(a.data)
140412230286208
>>> b = MyClass()
>>> id(b.data)
140412230275328
>>> id(a.data)
140412230286208