我怎样才能要求从元类的抽象方法中调用 super() ?

How can I make it a requirement to call super() from within an abstract method of a metaclass?

我有一个带有抽象方法的中央元class,其中一些我不仅想强制它们出现在 child classes 中,而且还想强制 child classes 从其中一些抽象方法中显式调用 super()

举个简单的例子:

import requests
import abc

class ParentCls(metaclass=abc.ABCMeta):
    # ...
    @abc.abstractmethod
    def login(self, username, password):
        """Override with logic for logging into the service"""

    @abc.abstractmethod
    def download_file(self, resp: requests.Response) -> None:
        # repeated logic goes here
        # basically, save the streaming download either to the cloud 
        # or locally depending on other class variables
        pass

class ChildCls(ParentCls):
    def login(self, username, password):
        # perform the login steps, no need to call `super()` in this method
        pass

    def download_file(self, link):
        # download request will be different per child class
        dl_resp = requests.get(link, stream=True)
        super().download_file(dl_resp)  # how can I make an explicit super() call required?

所以,我的问题是:

  1. 是否可以要求 一些(不是全部)抽象方法从 child class 显式调用 super()
  2. 如果是,那看起来怎么样?如果不是,怎么会?

我不知道有什么方法可以连接到 Python 协议,以便强制执行某些方法。实施用户端 class(例如 child)的人负责方法的正确实施。文档是您最好的朋友。

不过,如果您对解决方法感兴趣,请在下面找到实现您的逻辑的替代可能性。

import requests
import abc

class ParentCls(metaclass=abc.ABCMeta):
    # ...
    @abc.abstractmethod
    def login(self, username, password):
        """Override with logic for logging into the service"""

    def download_file(self, link) -> None:

        dl_resp = self._get_download_response(link)

        # repeated logic goes here
        # basically, save the streaming download either to the cloud 
        # or locally depending on other class variables
        print('repeated logic')

    @abc.abstractmethod
    def _get_download_response(self, link):
        pass
        
class ChildCls(ParentCls):
    def login(self, username, password):
        # perform the login steps, no need to call `super()` in this method
        pass

    def _get_download_response(self, link):
        # do whatever is child-specific here
        print('construct download request for child')
        return requests.get(link, stream=True)

您可以在各自的 class 中自定义您的 child-specific 逻辑,但您调用的方法仅在 parent class.[=11 中实现=]

包含对 super() 调用的方法与不包含调用的方法的一个区别是第一个方法将具有 __class__ 非局部变量。可以通过查看代码对象来检查:

In [18]: class A: 
    ...:     def a(self): 
    ...:         super().a() 
    ...:     def b(self): 
    ...:         pass 
    ...:                                                                                                                                                                                     

In [19]: A.a.__code__.co_freevars                                                                                                                                                            
Out[19]: ('__class__',)

In [20]: A.b.__code__.co_freevars                                                                                                                                                            
Out[20]: ()

这是在 运行 调用元 class 之前注入的,作为 type.__new__ 能够在 class 时插入引用的一种方式是为 class 本身创建的。它们是由对 super.

的无参数调用自动使用的

问题是这不能确保 super() 的存在,或者它实际上是 运行,也不能确保它用于调用所需的超级方法(可以使用它检查一个属性,或调用 super-class 中的另一个方法,尽管这很不寻常。

但是,仅此一项检查就可以捕获大多数情况,并防止意外解雇。

如果你真的需要严格检查超级方法调用,那可以在 运行 时间内完成,当实际方法被调用时,通过一个更复杂的机制:

在伪代码中:

  • 使用元class用装饰器包装被调用方法的最叶覆盖(我们称之为“客户端装饰器”)
  • 用另一个装饰器(例如“checker-decorator”)装饰必须 运行 的基本方法
  • “client-decorator”的功能是翻转一个应该是 由“检查器装饰器”取消翻转。
  • 在方法退出时,“客户端装饰器”可以知道是否调用了基本方法,如果没有调用则引发错误。

尝试使用传统的装饰器只包含子classes 中的最叶方法是复杂的(如果您在标记为 metaclass 的问题上寻找我的答案,我已经发布至少一次的示例代码,并且有很多极端情况)。但是,如果您将方法动态包装在 class __getattribute__ - 当从实例中检索方法时,这样做会更容易。

在这个例子中,“client-decorator”和“checker-decorator”都必须协调,访问“base_method_had_run”标志。一种机制是 checker-decorator 将“client-decorator”附加到修饰的方法,然后 metaclass __new__ 方法会建立一个注册表,其中包含方法名称和每个 class 的客户端装饰器。然后,此注册表可用于 __getattribute__

的动态包装

写完这些段落后,我已经很清楚了,我可以举一个工作示例:


from functools import wraps
import abc

def force_super_call(method):
    # If the instance is ever used in parallel code, like in multiple threads
    # or async-tasks, the flag bellow should use a contextvars.ContectVar
    # (or threading.local)
    base_method_called = False
    @wraps(method)
    def checker_wrapper(*args, **kwargs):
        nonlocal base_method_called
        try:
            result = method(*args, **kwargs)
        finally:
            base_method_called = True
        return result

    # This will be used dinamically on each method call:
    def client_decorator(leaf_method):
        @wraps(leaf_method)
        def client_wrapper(*args, **kwargs):
            nonlocal base_method_called
            base_method_called = False
            try:
                result = leaf_method(*args, **kwargs)
            finally:
                if not base_method_called:
                    raise RuntimeError(f"Overriden method '{method.__name__}' did not cause the base method to be called")

                base_method_called = False

            return result
        return client_wrapper

    # attach the client-wrapper to the decorated base method, so that the mechanism
    # in the metaclass can retrieve it:
    checker_wrapper.client_decorator = client_decorator

    # ordinary decorator return
    return checker_wrapper


def forcecall__getattribute__(self, name):

    cls = type(self)

    method = object.__getattribute__(self, name)
    registry = type(cls).forcecall_registry

    for superclass in cls.__mro__[1:]:
        if superclass in registry and name in registry[superclass]:
            # Apply the decorator with ordinary, function-call syntax:
            method = registry[superclass][name](method)
            break
    return method


class ForceBaseCallMeta(abc.ABCMeta):
    forcecall_registry = {}

    def __new__(mcls, name, bases, namespace, **kwargs):
        cls = super().__new__(mcls, name, bases, namespace, **kwargs)
        mcls.forcecall_registry[cls] = {}
        for name, method in cls.__dict__.items():
            if hasattr(method, "client_decorator"):
                mcls.forcecall_registry[cls][name] = method.client_decorator
        cls.__getattribute__ = forcecall__getattribute__
        return cls

这有效:


In [3]: class A(metaclass=ForceBaseCallMeta): 
   ...:     @abc.abstractmethod 
   ...:     @force_super_call 
   ...:     def a(self): 
   ...:         pass 
   ...:          
   ...:     @abc.abstractmethod 
   ...:     @force_super_call 
   ...:     def b(self): 
   ...:         pass 
   ...:                                                                                                                                                                                      

In [4]: class B(A): 
   ...:     def a(self): 
   ...:         return super().a() 
   ...:     def b(self): 
   ...:         return None 
   ...:                                                                                                                                                                                      

In [5]: b = B()                                                                                                                                                                              

In [6]: b.a()                                                                                                                                                                                

In [7]: b.b()                                                                                                                                                                                
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
...
RuntimeError: Overriden method 'b' did not cause the base method to be called