Python: несоответствие в том, как вы определяете функцию __setattr__?

StackOverflow https://stackoverflow.com/questions/6305267

  •  26-10-2019
  •  | 
  •  

Вопрос

Рассмотрим этот код:

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 применяются как несвязанные методы при хранении в классе (HAS __get__), в то время как встроенные функции не (они разные).

Однако под PYPY встроенные функции точно такие же, как и функции Python, поэтому интерпретатор не может сказать их отдельно и рассматривать их как функции на уровне Python. Я думаю, что это было определено как детали реализации, хотя в Python-Dev было некоторое обсуждение об удалении этого конкретного различия.

Ваше здоровье,
Фиял

Обратите внимание на следующее:

>>> 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. орг) и относится к части языка, которая документируется «как есть» реализацией (и подвергается (скорее) частым изменениям).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top