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文档,输出应该是什么?)


以下是CPython的错误报告。

我怀疑这与查找优化有关。来自源代码:

 /* 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)

我发现效果如预期。

这是一个已知的(可能不是很好)记录在案的差异。PyPy不区分函数和内置函数。在CPython中,当存储在类中时,函数会被绑定为未绑定的方法(have__get__),而内置函数则不会(它们不同)。

然而,在PyPy下,内置函数与python函数完全相同,因此解释器无法区分它们,并将它们视为python级别的函数。我认为这被定义为实现细节,尽管在python开发中有一些关于消除这种特殊差异的讨论。

干杯,
fijal

注意以下内容:

>>> 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中有点松散地指定),并且属于语言中由实现"照原样"记录的部分(并且(相当)频繁地进行更改)。

Simple,处理子类并更正AttributeError,尽管它很小:

class DotDict(dict):
    def __init__(self, d: dict = {}):
        super().__init__()
        for key, value in d.items():
            self[key] = DotDict(value) if type(value) is dict else value
    
    def __getattr__(self, key):
        if key in self:
            return self[key]
        raise AttributeError(key) #Set proper exception, not KeyError
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

最新更新