解决您__getattr__()
/__setattr__()
无限递归问题的简单方法
实现这些魔术方法中的一种或另一种通常很容易。但是当同时覆盖它们时,它变得更加棘手。这篇文章的例子主要适用于这个更困难的情况。
__init__()
在实现这两种神奇的方法时,经常会陷入在类的构造函数中找出一种绕过递归的策略。这是因为需要为对象初始化变量,但每次读取或写入这些变量的尝试都会通过__get/set/attr__()
,这可能会在其中包含更多未设置的变量,从而导致更多徒劳的递归调用。
首先,要记住的一个关键点是,__getattr__()
只有在对象上找不到该属性时才会被运行时调用。问题是在不递归地触发这些函数的情况下定义属性。
另一点是__setattr__()
无论如何都会被调用。这是两个函数之间的一个重要区别,这就是实现这两个属性方法可能很棘手的原因。
这是解决问题的一种基本模式。
class AnObjectProxy:
_initialized = False # *Class* variable 'constant'.
def __init__(self):
self._any_var = "Able to access instance vars like usual."
self._initialized = True # *instance* variable.
def __getattr__(self, item):
if self._initialized:
pass # Provide the caller attributes in whatever ways interest you.
else:
try:
return self.__dict__[item] # Transparent access to instance vars.
except KeyError:
raise AttributeError(item)
def __setattr__(self, key, value):
if self._initialized:
pass # Provide caller ways to set attributes in whatever ways.
else:
self.__dict__[key] = value # Transparent access.
当类初始化并创建它的实例变量时,两个属性函数中的代码都允许通过__dict__
字典透明地访问对象的属性——您的代码__init__()
可以正常创建和访问实例属性。当调用属性方法时,它们只访问self.__dict__
已经定义的方法,从而避免递归调用。
在 的情况下self._any_var
,一旦被分配,__get/set/attr__()
就不会被调用再次找到它。
除去额外的代码,这是最重要的两部分。
... def __getattr__(self, item):
... try:
... return self.__dict__[item]
... except KeyError:
... raise AttributeError(item)
...
... def __setattr__(self, key, value):
... self.__dict__[key] = value
解决方案可以围绕这些行构建访问__dict__
字典。为了实现对象代理,实现了两种模式:在此之前的代码中的初始化和后初始化 - 下面是一个更详细的相同示例。
答案中还有其他示例,它们在处理递归的各个方面可能具有不同程度的有效性。一种有效的方法是__dict__
直接访问__init__()
需要及早访问实例变量的其他地方。这可行,但可能有点冗长。例如,
self.__dict__['_any_var'] = "Setting..."
会在__init__()
.
我的帖子往往会有点冗长.. 在这一点之后只是额外的。您应该已经对上面的示例有所了解。
使用 IDE 中的调试器可以看到其他一些方法的缺点。他们可能会过度使用自省,并在您逐步执行代码时产生警告和错误恢复消息。即使使用可以独立运行的解决方案,您也可以看到这种情况发生。当我说递归的所有方面时,这就是我所说的。
这篇文章中的示例仅使用单个类变量来支持 2 种操作模式,非常易于维护。
但请注意:代理类需要两种操作模式来设置和代理内部对象。您不必有两种操作模式。
您可以简单地合并代码以以__dict__
适合您的任何方式访问这些示例中的内容。
如果您的要求不包括两种操作模式,您可能根本不需要声明任何类变量。只需采用基本模式并对其进行自定义即可。
这是一个更接近真实世界(但绝不是完整)的示例,该示例遵循该模式的 2 模式代理:
>>> class AnObjectProxy:
... _initialized = False # This class var is important. It is always False.
... # The instances will override this with their own,
... # set to True.
... def __init__(self, obj):
... # Because __getattr__ and __setattr__ access __dict__, we can
... # Initialize instance vars without infinite recursion, and
... # refer to them normally.
... self._obj = obj
... self._foo = 123
... self._bar = 567
...
... # This instance var overrides the class var.
... self._initialized = True
...
... def __setattr__(self, key, value):
... if self._initialized:
... setattr(self._obj, key, value) # Proxying call to wrapped obj.
... else:
... # this block facilitates setting vars in __init__().
... self.__dict__[key] = value
...
... def __getattr__(self, item):
... if self._initialized:
... attr = getattr(self._obj, item) # Proxying.
... return attr
... else:
... try:
... # this block facilitates getting vars in __init__().
... return self.__dict__[item]
... except KeyError:
... raise AttributeError(item)
...
... def __call__(self, *args, **kwargs):
... return self._obj(*args, **kwargs)
...
... def __dir__(self):
... return dir(self._obj) + list(self.__dict__.keys())
2-mode proxy 只需要一点“引导”来在初始化时访问它自己范围内的 var,然后再设置它的任何 var。初始化后,代理没有理由为自己创建更多的变量,因此通过将所有属性调用推迟到它的包装对象,它会很好。
代理本身拥有的任何属性仍然可供其自身和其他调用者访问,因为只有在无法立即在对象上找到属性时才会调用魔术属性函数。
希望这种方法对任何喜欢直接解决__get/set/attr__()
__init__()
挫折的人都有好处。