Что мешает желаемым результатам, так это то, что вы выполняете итерации по классу '__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