python: super () - подобный прокси-объект, который запускает поиск MRO в указанном классе


Согласно документам, super(cls, obj) возвращает

Прокси-объект, делегирующий вызовы метода родителю или родному брату класс типа cls

Я понимаю, почему super() предлагает эту функциональность, но мне нужно что-то немного другое: мне нужно создать прокси-объект, который делегирует вызовы методов (и поиск атрибутов) самому классу cls; и как в super, если cls не реализует метод / атрибут, мой прокси должен продолжать искать в порядке MRO новый Не исходный класс). Есть ли какая-нибудь функция, которую я могу написать, чтобы достичь этого?

Пример:

class X:
  def act():
    #...

class Y:
  def act():
    #...

class A(X, Y):
  def act():
    #...

class B(X, Y):
  def act():
    #...

class C(A, B):
  def act():
    #...

c = C()
b = some_magic_function(B, c)
# `b` needs to delegate calls to `act` to B, and look up attribute `s` in B
# I will pass `b` somewhere else, and have no control over it
Конечно, я мог бы сделать b = super(A, c), но это зависит от знания точной иерархии классов и того факта, что B следует за A в MRO. Он молча сломается, если в будущем изменится какое-либо из этих двух предположений. (Обратите внимание, что super не делает никаких таких предположений!) Если бы мне просто нужно было позвонить b.act(), я мог бы использовать B.act(c). Но это так. передаю b кому-то другому, и понятия не имею, что они с этим сделают. Мне нужно убедиться, что он не предаст меня и не начнет действовать как экземпляр class C в какой-то момент.

Отдельный вопрос, документация для super() (в Python 3.2) говорит только о делегировании метода и не уточняет, что поиск атрибутов для прокси также выполняется таким же образом. Может быть, это случайное упущение?

EDIT

Обновленный Делегатский подход работает следующим образом пример также:

class A:
    def f(self):
        print('A.f')
    def h(self):
        print('A.h')
        self.f()

class B(A):
    def g(self):
        self.f()
        print('B.g')
    def f(self):
        print('B.f')
    def t(self):
        super().h()


a_true = A()
# instance of A ends up executing A.f
a_true.h()

b = B()
a_proxy = Delegate(A, b)
# *unlike* super(), the updated `Delegate` implementation would call A.f, not B.f
a_proxy.h()

Обратите внимание, что обновленный class Delegate ближе к тому, что я хочу, чем super() по двум причинам:

  1. super() только проксирование для первого вызова; последующие вызовы будут происходить как обычно, так как к тому времени используется объект, а не его прокси.

  2. super() не разрешает доступ к атрибутам.

Таким образом, мой заданный вопрос имеет (почти) идеальный ответ на языке Python.

Оказывается, что на более высоком уровне я был пытаясь сделать что-то, что я не должен (смотрите мои комментарии здесь).

2 4

2 ответа:

Этот класс должен охватывать наиболее распространенные случаи:

class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self._delegate_obj)
        return x

Используйте его так:

b = Delegate(B, c)

(с именами из вашего примера кода.)

Ограничения:

  1. Вы не можете получить некоторые специальные атрибуты, такие как __class__ и т.д. из класса, который вы передаете в конструктор через этот прокси. (Эти рестиции также применимы к super.)

  2. Это может вести себя weired, если атрибут, который вы хотите получить, является каким-то weired дескриптор.

Edit: Если вы хотите, чтобы код в обновлении к вашему вопросу работал по желанию, вы можете использовать код foloowing:

class Delegate:
    def __init__(self, cls):
        self._delegate_cls = cls
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self)
        return x

Это передает прокси-объект в качестве параметра self любому вызываемому методу, и ему вообще не нужен исходный объект, поэтому я удалил его из конструктора.

Если вы также хотите, чтобы атрибуты экземпляра были доступны, вы можете использовать эту версию:

class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        if name in vars(self._delegate_obj):
            return getattr(self._delegate_obj, name)
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self)
        return x

Отдельный вопрос, документация для super () (в Python 3.2) только говорит о своем методе делегирования, и не уточняет, что поиск атрибутов для прокси-сервера также выполняется таким же образом. Это случайное упущение?

Нет, это не случайно. super() ничего не делает для поиска атрибутов. Причина в том, что атрибуты экземпляра не связаны с определенным классом, они просто существуют. Рассмотрим следующее:

class A:
    def __init__(self):
        self.foo = 'foo set from A'

class B(A):
    def __init__(self):
        super().__init__()
        self.bar = 'bar set from B'

class C(B):
    def method(self):
        self.baz = 'baz set from C'

class D(C):
    def __init__(self):
        super().__init__()
        self.foo = 'foo set from D'
        self.baz = 'baz set from D'

instance = D()
instance.method()
instance.bar = 'not set from a class at all'

Какому классу "принадлежит" foo, bar, и baz?

Если я хочу просмотреть instance как экземпляр C, должен ли он иметь атрибут baz перед вызовом method? А что будет потом?

Если я рассматриваю instance как экземпляр A, какое значение должно иметь foo? Должен ли bar быть невидимым, потому что был добавлен только в B, или видимым, потому что он был установлен в значение вне класса?

Все эти вопросы являются нонсенсом в Python. Нет никакой возможности способ проектирования системы с семантикой Python, которая могла бы дать разумные ответы на них. __init__ даже не является специальным с точки зрения добавления атрибутов к экземплярам класса; это просто совершенно обычный метод, который случайно вызывается как часть протокола создания экземпляра. Любой метод (или даже код из другого класса вообще, или не из любого класса вообще) может создавать атрибуты в любом экземпляре, на который он ссылается.

На самом деле, все атрибуты instance хранятся там же:

>>> instance.__dict__
{'baz': 'baz set from C', 'foo': 'foo set from D', 'bar': 'not set from a class at all'}
Невозможно сказать, какие из них были первоначально установлены каким классом, или были последними установлены каким классом, или какой бы мерой собственности вы ни хотели обладать. Конечно, нет никакого способа добраться до" A.foo, затененного D.foo", как можно было бы ожидать от C++; они являются одним и тем же атрибутом, и любая запись в него одним классом (или из другого места) ударит по значению, оставленному в нем другим классом.

Следствием этого является то, что super() не выполняет поиск атрибутов точно так же, как и поиск методов; он не может, и ни один код, который вы пишете, не может.


На самом деле, от запуска некоторых экспериментов, ни super, ни Свен Delegate фактически не поддерживают прямой поиск атрибутов вообще!

class A:
    def __init__(self):
        self.spoon = 1
        self.fork = 2

    def foo(self):
        print('A.foo')

class B(A):
    def foo(self):
        print('B.foo')

b = B()

d = Delegate(A, b)
s = super(B, b)

Тогда оба работают, как и ожидалось для методов:

>>> d.foo()
A.foo
>>> s.foo()
A.foo

Но:

>>> d.fork
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    d.fork
  File "/tmp/foo.py", line 6, in __getattr__
    x = getattr(self._delegate_cls, name)
AttributeError: type object 'A' has no attribute 'fork'
>>> s.spoon
Traceback (most recent call last):
  File "<pyshell#45>", line 1, in <module>
    s.spoon
AttributeError: 'super' object has no attribute 'spoon'
Таким образом, они оба действительно работают только для вызова некоторых методов, а не для передачи произвольному стороннему коду, чтобы притвориться экземпляром класса вы хотите делегировать кому-то. К сожалению, они не ведут себя одинаково при наличии множественного наследования. Дано:
class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self._delegate_obj)
        return x


class A:
    def foo(self):
        print('A.foo')

class B:
    pass

class C(B, A):
    def foo(self):
        print('C.foo')

c = C()

d = Delegate(B, c)
s = super(C, c)

Затем:

>>> d.foo()
Traceback (most recent call last):
  File "<pyshell#50>", line 1, in <module>
    d.foo()
  File "/tmp/foo.py", line 6, in __getattr__
    x = getattr(self._delegate_cls, name)
AttributeError: type object 'B' has no attribute 'foo'
>>> s.foo()
A.foo

Потому что Delegate игнорирует полный MRO любого класса _delegate_obj, являющегося экземпляром, ТОЛЬКО используя MRO _delegate_cls. В то время как super делает то, что вы спросили в вопросе, но поведение кажется довольно странным: это не обертывание экземпляра C, чтобы притвориться, что это экземпляр B, потому что прямые экземпляры B не имеют foo определенный.

Вот моя попытка:

class MROSkipper:
    def __init__(self, cls, obj):
        self.__cls = cls
        self.__obj = obj

    def __getattr__(self, name):
        mro = self.__obj.__class__.__mro__
        i = mro.index(self.__cls)
        if i == 0:
            # It's at the front anyway, just behave as getattr
            return getattr(self.__obj, name)
        else:
            # Check __dict__ not getattr, otherwise we'd find methods
            # on classes we're trying to skip
            try:
                return self.__obj.__dict__[name]
            except KeyError:
                return getattr(super(mro[i - 1], self.__obj), name)

Я полагаюсь на атрибут __mro__ классов, чтобы правильно понять, с чего начать, а затем я просто использую super. Вы могли бы сами пройти цепочку MRO от этой точки, проверяя класс __dict__ s на наличие методов, если странность возврата на один шаг назад для использования super слишком велика.

Я не делал попыток обрабатывать необычные атрибуты; те, которые реализованы с помощью дескрипторов (включая свойства), или те магические методы, которые были найдены позади сцены на Python, которые часто начинаются с класса, а не непосредственно с экземпляра. Но это ведет себя так, как вы просили умеренно хорошо (с оговоркой, изложенной на ad nauseum в первой части моего поста; поиск атрибутов таким образом не даст вам никаких других результатов, чем поиск их непосредственно в данном случае).