Python: __setattr__ 函数定义的不一致性?
考虑一下这段代码:
class Foo1(dict):
def __getattr__(self, key): return self[key]
def __setattr__(self, key, value): self[key] = value
class Foo2(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
o1 = Foo1()
o1.x = 42
print(o1, o1.x)
o2 = Foo2()
o2.x = 42
print(o2, o2.x)
我本以为输出结果会是一样的。然而,在 CPython 2.5 和 2.6(在 3.2 中也是类似的)我得到的是:
({'x': 42}, 42)
({}, 42)
而在 PyPy 1.5.0 中,我得到了预期的输出:
({'x': 42}, 42)
({'x': 42}, 42)
那么,哪个输出是“正确”的呢?(或者根据 Python 文档,应该是什么输出呢?)
4 个回答
1
请注意以下几点:
>>> dict.__getitem__ # it's a 'method'
<method '__getitem__' of 'dict' objects>
>>> dict.__setitem__ # it's a 'slot wrapper'
<slot wrapper '__setitem__' of 'dict' objects>
>>> id(dict.__dict__['__getitem__']) == id(dict.__getitem__) # no bounding here
True
>>> id(dict.__dict__['__setitem__']) == id(dict.__setitem__) # or here either
True
>>> d = {}
>>> dict.__setitem__(d, 1, 2) # can be called directly (since not bound)
>>> dict.__getitem__(d, 1) # same with this
2
现在我们可以把它们包裹起来(而且__getattr__
即使不这样做也能正常工作):
class Foo1(dict):
def __getattr__(self, key): return self[key]
def __setattr__(self, key, value): self[key] = value
class Foo2(dict):
"""
It seems, 'slot wrappers' are not bound when present in the __dict__
of a class and retrieved from it via instance (or class either).
But 'methods' are, hence simple assignment works with __setitem__
in your original example.
"""
__setattr__ = lambda *args: dict.__setitem__(*args)
__getattr__ = lambda *args: dict.__getitem__(*args) # for uniformity, or
#__getattr__ = dict.__getitem__ # this way, i.e. directly
o1 = Foo1()
o1.x = 42
print(o1, o1.x)
o2 = Foo2()
o2.x = 42
print(o2, o2.x)
这样就得到了:
>>>
({'x': 42}, 42)
({'x': 42}, 42)
这里面涉及的行为机制(可能,我不是专家)超出了Python的“干净”部分(这个部分在一些详细的书籍,比如《学习Python》或《Python简明教程》中有介绍,也在python.org上有些松散的说明),而是属于语言的实现部分,这部分是按照实际情况记录的(而且会有相对频繁的变化)。
3
这是一个众所周知的(可能文档上没写得很清楚)区别。PyPy 不区分普通函数和内置函数。在 CPython 中,当函数存储在类里时,它们会被当作未绑定的方法(它们有 __get__),而内置函数则不是(它们是不同的)。
但是在 PyPy 中,内置函数和普通的 Python 函数是完全一样的,所以解释器无法区分它们,都会被当作 Python 级别的函数来处理。我觉得这个区别是作为实现细节定义的,虽然在 python-dev 上曾经讨论过要去掉这个特定的区别。
祝好,
fijal
7
我猜这和查找优化有关。从源代码来看:
/* speed hack: we could use lookup_maybe, but that would resolve the
method fully for each attribute lookup for classes with
__getattr__, even when the attribute is present. So we use
_PyType_Lookup and create the method only when needed, with
call_attribute. */
getattr = _PyType_Lookup(tp, getattr_str);
if (getattr == NULL) {
/* No __getattr__ hook: use a simpler dispatcher */
tp->tp_getattro = slot_tp_getattro;
return slot_tp_getattro(self, name);
}
快速路径并不会在类字典中查找。
所以,想要实现想要的功能,最好的办法是在类里面放一个重写的方法。
class AttrDict(dict):
"""A dictionary with attribute-style access. It maps attribute access to
the real dictionary. """
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self))
def __setitem__(self, key, value):
return super(AttrDict, self).__setitem__(key, value)
def __getitem__(self, name):
return super(AttrDict, self).__getitem__(name)
def __delitem__(self, name):
return super(AttrDict, self).__delitem__(name)
__getattr__ = __getitem__
__setattr__ = __setitem__
def copy(self):
return AttrDict(self)
我发现这样做效果正如预期。