Python: кооперативный суперкол __getattr__

Я работаю с somethign, похожим на этот код:

class BaseClass(object):
    def __getattr__(self, attr):
        return lambda:'1'

class SubClass(BaseClass):
    def foo(self):
        suffix = '2'
        return super(SubClass, self).foo() + suffix

class SubClass2(SubClass):
    def foo(self):
        suffix = '3'
        return super(SubClass2, self).foo() + suffix

o = SubClass2()
print o.foo()

Я ожидаю увидеть вывод '123', но вместо этого получаю сообщение об ошибке AttributeError: 'super' object has no attribute 'foo'. Python даже не пытается использовать базовый класс __getattr__.

Без изменения базового класса и слияния двух супер-вызовов я не смогу получить вывод, который я хочу. Есть ли какая-нибудь совместная модель суперкалла, которая будет работать для меня здесь?

Я понимаю, что super() переопределяет getattr каким-то образом, чтобы делать то, что ему нужно сделать, но я спрашиваю, есть ли разумное обходное решение, позволяющее вызывать подкласс __getattr__, когда необходимо.

Ответ 1

А, это отличный вопрос!

Короче говоря, здесь происходит то, что внутренности CPython иногда принимают ярлыки, когда они выполняют поиск атрибутов, и такой удивительный поведение является одним из последствий (другое - улучшенная производительность).

Чтобы точно понять, что происходит в этой ситуации, нам нужно рискнуть в определение super: http://hg.python.org/cpython/file/c24941251473/Objects/typeobject.c#l6689

Обратите внимание, что он не определяет tp_getattr (aka __getattr__), но определяет tp_getattro (aka __getattribute__):

PyTypeObject PySuper_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "super",                                    /* tp_name */
    ...
    0,                                          /* tp_getattr */
    ...
    super_getattro,                             /* tp_getattro */
    ...
};

(напомним, что __getattribute__ вызывается каждый раз, когда атрибут запрошен, в отличие от __getattr__, который вызывается только в том случае, если атрибут не существует на объекте (грубо: если атрибут не находится в объекте __dict__)).

Далее, рассмотрев определение super_getattro (aka super.__getattribute__), мы видим, что реализация приблизительно:

class super(object):
    def __init__(self, obj_type, obj):
        self.obj_type = obj_type
        self.obj = obj

    def __getattribute__(self, attr):
        i = self.obj_type.__mro__.find(self.obj_type)
        i += 1
        while i < len(obj_type.__mro__):
            cur_type = self.obj_type.__mro__[i]
            cur_dict = cur_type.__dict___
            res = cur_dict.get(attr)
            if res is not None:
                return res
            i += 1
        return object.__getattribute__(self, attr)

Что делает очевидным, почему super плохо работает с __getattr__ - super проверяет только атрибуты в родительском классе __dict__!

Замечание: похоже, что pypy (начиная с 2.1.0) ведет себя одинаково:

$ pypy super.py 
Traceback (most recent call last):
  File "app_main.py", line 72, in run_toplevel
  File "super.py", line 16, in <module>
    print o.foo()
  File "super.py", line 13, in foo
    return super(SubClass2, self).foo() + suffix
  File "super.py", line 8, in foo
    return super(SubClass, self).foo() + suffix
AttributeError: 'super' object has no attribute 'foo'

Ответ 2

Кажется, это работает правильно. В настоящее время я не вижу, почему стандартный суперкласс не делает этого.

class super2(super):
    def __getattr__(self, attr):
        return self.__self__.__getattr__(attr)

class BaseClass(object):
    def __getattr__(self, attr):
        return lambda:'1'

class SubClass(BaseClass):
    def foo(self):
        suffix = '2'
        return super2(SubClass, self).foo() + suffix

class SubClass2(SubClass):
    def foo(self):
        suffix = '3'
        return super2(SubClass2, self).foo() + suffix

o = SubClass2()
print o.foo()