Python: __setattr__ 函数定义的不一致性?

15 投票
4 回答
750 浏览
提问于 2025-04-16 19:18

考虑一下这段代码:

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)

我发现这样做效果正如预期。

撰写回答