这种情况下,我有一个抽象类和几个实现它的子类.

class Parent(metaclass=ABCMeta):

    @abstract_method
    def first_method(self, *args, **kwargs):
        raise NotImplementedError()

    @abstract_method
    def second_method(self, *args, **kwargs):
        raise NotImplementedError()


class Child(Parent):

    def first_method(self, *args, **kwargs):
        print('First method of the child class called!')

    def second_method(self, *args, **kwargs):
        print('Second method of the child class called!')

我的目标是制作某种decorator ,它将用于父类的任何子类的方法.我需要这个,因 for each 方法在实际做某事之前都会做一些准备,而这种准备在父类的所有子类的所有方法中都是完全相同的.比如:

class Child(Parent):

    def first_method(self, *args, **kwargs):
        print('Preparation!')
        print('First method of the child class called!')

    def second_method(self, *args, **kwargs):
        print('Preparation!')
        print('Second method of the child class called!')

我想到的第一件事是使用父类方法实现:只需删除"raise NotImplementedError()"并添加一些功能,然后在子类中调用,例如super().每个方法开头的第一个方法(self、*args、**kwargs).这很好,但我也想从父方法返回一些数据,当父方法和子方法在声明中返回不同的内容时,这看起来会很奇怪.更不用说我可能想在方法之后做一些后处理工作,所以我需要两个不同的函数:用于开始和执行脚本之后.

接下来我想到的是制作元类.

这是最接近我目标的解决方案,但不管怎样,它感觉是错的.因为一些KWARG将被传递给子方法这一点并不明确,如果您对这段代码不熟悉,那么您需要做一些研究来了解它是如何工作的.我觉得我有点过度工程化了.

所以问题是:有没有任何模式或类似的东西来实现这个功能?

推荐答案

所以,除了现有的模式:我不知道这是否有一个特定的名称,你需要的是,一个"模式"是"插槽"的使用:也就是说,你记录了特殊命名的方法,这些方法将作为另一个方法执行的一部分被调用.然后,另一个方法执行其设置代码,判断slotted方法(通常可通过名称识别)是否存在,用简单的方法调用它们,这将运行它的最专门版本,即使调用slots的特殊方法在基类中,并且您处于大类继承层次 struct 中.

这种模式的一个简单例子是Python实例化对象的方式:使用与函数调用(MyClass())相同的语法调用类时,实际调用的是该类的class(其元类)__call__方法.(通常为type.__call__).在Python的type.__call__代码中,调用类'__new__方法,然后调用类'__init__方法,最后返回第一次调用返回到__new__的值.自定义元类可以修改__call__,以便在这两个调用之前、之间或之后运行它想要的任何代码.

因此,如果这不是Python,那么您只需要对其进行详细说明,并记录这些方法不应该直接调用,而是通过一个"入口点"方法来调用——该方法可以简单地使用一个"ep_2;"前缀.这些必须在基类上进行固定和硬编码,并且每个方法都需要一个前缀/后缀代码.


class Base(ABC):
    def ep_first_method(self, *args, **kw);
        # prefix code...
        ret_val = self.first_method(*args, **kw)
        # postfix code...
        return ret_val

    @abstractmethod
    def first_method(self):
        pass
    
class Child(Base):
    def first_method(self, ...):
        ...

由于是Python,所以更容易添加一些魔法来避免代码重复并保持简洁.

一种可能的方法是使用一个特殊的类,当在一个子类中检测到一个应该作为包装器方法的插槽调用的方法时,如上图所示,自动重命名that方法:这样入口点方法可以与子方法具有相同的名称——更妙的是,一个简单的修饰符可以标记打算作为"入口点"的方法,遗产甚至对他们有用.

基本上,在构建一个新类时,我们会判断所有方法:如果其中任何一个方法在调用层次 struct 中有一个对应的部分标记为入口点,则会进行重命名.

如果任何入口点方法都将作为第二个参数(第一个参数为self),则更实用,这是要调用的时隙方法的参考.

经过一些修改:好消息是不需要custommetaclass——基类中的__init_subclass__个特殊方法足以启用decorator .

坏消息是:由于在入口点中由对最终方法的"super()"的潜在调用触发的重新进入迭代,因此需要一种在中间类中调用原始方法的复杂启发式方法.我还注意了一些多线程保护——尽管这不是100%防弹的.

import sys
import threading
from functools import wraps


def entrypoint(func):
    name = func.__name__
    slotted_name = f"_slotted_{name}"
    recursion_control = threading.local()
    recursion_control.depth = 0
    lock = threading.Lock()
    @wraps(func)
    def wrapper(self, *args, **kw):
        slotted_method = getattr(self, slotted_name, None)
        if slotted_method is None:
            # this check in place of abstractmethod errors. It is only raised when the method is called, though
            raise TypeError("Child class {type(self).__name__} did not implement mandatory method {func.__name__}")

        # recursion control logic: also handle when the slotted method calls "super",
        # not just straightforward recursion
        with lock:
            recursion_control.depth += 1
            if recursion_control.depth == 1:
                normal_course = True
            else:
                normal_course = False
        try:
            if normal_course:
                # runs through entrypoint
                result = func(self, slotted_method, *args, **kw)
            else:
                # we are within a "super()" call - the only way to get the renamed method
                # in the correct subclass is to recreate the callee's super, by fetching its
                # implicit "__class__" variable.
                try:
                    callee_super = super(sys._getframe(1).f_locals["__class__"], self)
                except KeyError:
                    # callee did not make a "super" call, rather it likely is a recursive function "for real"
                    callee_super = type(self)
                slotted_method = getattr(callee_super, slotted_name)
                result = slotted_method(*args, **kw)

        finally:
            recursion_control.depth -= 1
        return result

    wrapper.__entrypoint__ = True
    return wrapper


class SlottedBase:
    def __init_subclass__(cls, *args, **kw):
        super().__init_subclass__(*args, **kw)
        for name, child_method in tuple(cls.__dict__.items()):
            #breakpoint()
            if not callable(child_method) or getattr(child_method, "__entrypoint__", None):
                continue
            for ancestor_cls in cls.__mro__[1:]:
                parent_method = getattr(ancestor_cls, name, None)
                if parent_method is None:
                    break
                if not getattr(parent_method, "__entrypoint__", False):
                    continue
                # if the code reaches here, this is a method that
                # at some point up has been marked as having an entrypoint method: we rename it.
                delattr (cls, name)
                setattr(cls, f"_slotted_{name}", child_method)
                break
        # the chaeegs above are inplace, no need to return anything


class Parent(SlottedBase):
    @entrypoint
    def meth1(self, slotted, a, b):
        print(f"at meth 1 entry, with {a=} and {b=}")
        result = slotted(a, b)
        print("exiting meth1\n")
        return result

class Child(Parent):
    def meth1(self, a, b):
        print(f"at meth 1 on Child, with {a=} and {b=}")

class GrandChild(Child):
    def meth1(self, a, b):
        print(f"at meth 1 on grandchild, with {a=} and {b=}")
        super().meth1(a,b)

class GrandGrandChild(GrandChild):
    def meth1(self, a, b):
        print(f"at meth 1 on grandgrandchild, with {a=} and {b=}")
        super().meth1(a,b)

c = Child()
c.meth1(2, 3)


d = GrandChild()
d.meth1(2, 3)

e = GrandGrandChild()
e.meth1(2, 3)

Python相关问答推荐

Odoo -无法比较使用@api.depends设置计算字段的日期

运行终端命令时出现问题:pip start anonymous"

在Polars(Python库)中将二进制转换为具有非UTF-8字符的字符串变量

通过pandas向每个非空单元格添加子字符串

如何在给定的条件下使numpy数组的计算速度最快?

将输入聚合到统一词典中

将scipy. sparse矩阵直接保存为常规txt文件

使用BeautifulSoup抓取所有链接

在Python中计算连续天数

Polars将相同的自定义函数应用于组中的多个列,

ruamel.yaml dump:如何阻止map标量值被移动到一个新的缩进行?

为什么调用函数的值和次数不同,递归在代码中是如何工作的?

我可以不带视频系统的pygame,只用于游戏手柄输入吗?''

Python日志(log)库如何有效地获取lineno和funcName?

从列表中分离数据的最佳方式

Pandas 删除只有一种类型的值的行,重复或不重复

我怎样才能让深度测试在OpenGL中使用Python和PyGame呢?

某些值的数值幂和**之间的差异

奇怪的Base64 Python解码

为什么在更新Pandas 2.x中的列时,数据类型不会更改,而在Pandas 1.x中会更改?