我怎样才能要求从元类的抽象方法中调用 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?
所以,我的问题是:
- 是否可以要求 一些(不是全部)抽象方法从 child class 显式调用
super()
?
- 如果是,那看起来怎么样?如果不是,怎么会?
我不知道有什么方法可以连接到 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
我有一个带有抽象方法的中央元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?
所以,我的问题是:
- 是否可以要求 一些(不是全部)抽象方法从 child class 显式调用
super()
? - 如果是,那看起来怎么样?如果不是,怎么会?
我不知道有什么方法可以连接到 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