Как перебрать суперклассы в многоуровневом наследовании? - PullRequest
0 голосов
/ 03 мая 2020

Я проектирую метакласс для перезаписи функции класса __call__, предшествующей выполнению ее суперклассов __call__ рекурсивно. По идее можно получить следующий результат для кода ниже:

Abstract
Base
Super
Child
class Abstract(metaclass=Meta):
    def __call__(self):
        print("Abstract")

class Base(Abstract):
    def __call__(self):
        print("Base")

class Super(Abstract):
    def __call__(self):
        print("Super")

class Parent:
    def __call__(self):
        print("Parent")

class Child(Base, Super, Parent):
    def __call__(self):
        print("Child")

До сих пор я писал Meta.new следующим образом:

def __new__(meta, name, bases, attr):

    __call__ = attr['__call__']

    def recursive_call(self):
        for cls in [base for base in self.__class__.__bases__ if type(base) is Meta]:
            cls.__call__(super(cls, self))
        __call__(self)

    attr['__call__'] = recursive_call

    return super(Meta, meta).__new__(
        meta, name, bases, attr
    )

И на самом деле он работает для классов с одним уровнем наследования, но не для многоуровневых.

Как я могу исправить этот код для достижения моей цели? Или был бы более простой способ поразить его, чем метаклассы?

Ответы [ 2 ]

1 голос
/ 04 мая 2020

Что мешает желаемым результатам, так это то, что вы выполняете итерации по классу '__bases__ - в нем перечислены только непосредственные суперклассы. Если вы измените свой метакасс, чтобы перебрать линеаризованную последовательность __mro__, Python всех предков одного класса, он будет работать:


In [14]: class Abstract(metaclass=Meta): 
    ...:     def __call__(self): 
    ...:         print("Abstract") 
    ...:  
    ...: class Base(Abstract): 
    ...:     def __call__(self): 
    ...:         print("Base") 
    ...:  
    ...: class Super(Abstract): 
    ...:     def __call__(self): 
    ...:         print("Super") 
    ...:  
    ...: class Parent: 
    ...:     def __call__(self): 
    ...:         print("Parent") 
    ...:  
    ...: class Child(Parent): 
    ...:     def __call__(self): 
    ...:         print("Child") 
    ...:                                                                                                            

In [15]: Child.__mro__                                                                                              
Out[15]: (__main__.Child, __main__.Parent, object)

В любом случае, это окажется немного сложнее чем кажется на первый взгляд - есть угловые случаи - что если один из ваших подходящих классов не имеет, например, __call__? Что если один из методов do включает обычный вызов super ()? Хорошо, добавьте маркер, чтобы избежать нежелательного повторного входа в случаях, когда вы ставите «super ()» - что, если он работает в многопоточной среде и создается два экземпляра одновременно?

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

Также обратите внимание, что для __call__ это работает точно так же, как и для любого другого метода - поэтому я преобразовал имя "__call__" в константу, чтобы убедиться, что (и это можно сделать списком методов или всех методов, чье имя имеет определенный префикс и т. д.).


from functools import wraps
from threading import local as threading_local  

MARKER_METHOD = "_auto_super_original"
AUTO_SUPER = "__call__"

class Meta(type):
    def __new__(meta, name, bases, attr):

        original_call = attr.pop(AUTO_SUPER, None)

        avoid_rentrancy = threading_local()
        avoid_rentrancy.running = False

        @wraps(original_call)
        def recursive_call(self, *args, _wrap_call_mro=None, **kwargs):
            if getattr(avoid_rentrancy, "running", False):
                return
            avoid_rentrancy.running = True

            mro = _wrap_call_mro or self.__class__.__mro__

            try:
                for index, supercls in enumerate(mro[1:], 1):
                    if MARKER_METHOD in supercls.__dict__:
                        supercls.__call__(self, *args, _wrap_call_mro=mro[index:], **kwargs)
                        break
                getattr(mro[0], MARKER_METHOD)(self, *args, **kwargs)    
            finally:
                avoid_rentrancy.running = False

        if original_call:
            attr[MARKER_METHOD] = original_call
            attr[AUTO_SUPER] = recursive_call

        return super().__new__(
            meta, name, bases, attr
        )

И это работает на консоли - я добавил еще несколько промежуточных классов для покрытия угловых случаев:


class Abstract(metaclass=Meta):
    def __call__(self):
        print("Abstract")

class Base1(Abstract):
    def __call__(self):
        print("Base1")

class Base2(Abstract):
    def __call__(self):
        print("Base2")

class Super(Base1):
    def __call__(self):
        print("Super")

class NonColaborativeParent():
    def __call__(self):
        print("Parent")

class ForgotAndCalledSuper(Super):
    def __call__(self):
        super().__call__()
        print("Forgot and called super")

class NoCallParent(Super):
    pass

class Child(NoCallParent, ForgotAndCalledSuper, Parent, Base2):
    def __call__(self):
        print("Child")

Результат:

In [96]: Child()()                                                                                                  
Abstract
Base2
Base1
Super
Child
Forgot and called super
Child
0 голосов
/ 03 мая 2020

Ну, я мог бы найти решение, использующее преимущества порядка разрешения методов класса (то есть его атрибут __mro__), а также следуя , поддерживает предложение Моники (спасибо!).

Мой метакласс был таким:

class MetaComposition(type):
    def __new__(meta, name, bases, attr, __func__='__call__'):

        def __call__(self, *args, **kwargs):
            for cls in self.__class__.__compound__:
                cls.__run__(self, *args, **kwargs)

        attr['__run__'] = attr[__func__]
        attr[__func__] = __call__

        return super(MetaComposition, meta).__new__(meta, name, bases, attr)

    @property
    def __compound__(cls):
        return [
            element
            for element in
            cls.mro()[::-1]
            if type(element)
            is type(cls)
        ]

И таким образом достигается желаемое поведение

...