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

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

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

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

Пример:

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 ]

3 голосов
/ 12 января 2012

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

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. Это может вести себя не так, как надо, если атрибут, который вы хотите получить, является каким-то устаревшим дескриптором.

Редактировать : если вы хотите, чтобы код в обновлении вашего вопроса работал как нужно, вы можете использовать следующий код:

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
2 голосов
/ 17 января 2012

Отдельный вопрос, документация по 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'}

Невозможно сказать, какие из них были изначально установлены каким классом или были в последний раз установлены каким классом, или какой-либо другой формой собственности, которую вы хотите. Конечно, нет никакого способа получить "1031 *, находящегося в тени 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 в первой части моего поста; поиск атрибутов таким образом не даст вам никаких других результатов, чем поиск их непосредственно в экземпляре).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...