是否有任何设计模式可以实现抽象 class 的 child classes 方法的装饰?

Is there any design pattern for implementing decoration of methods of child classes of an abstract class?

情况是这样的,我有一个抽象 class 和几个 child classes 实现它。

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!')

我的目标是制作某种装饰器,它将用于 Parent class 的任何 child 的方法。我需要这个,因为每个方法在实际做某事之前都会进行某种准备,并且这种准备在 Parent class 的所有 child 的所有方法中都是完全相同的。喜欢:

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!')

我首先想到的是使用 Parent class 方法实现:只需删除“raise NotImplementedError()”并添加一些功能,然后在 child classes 例如,我会在每个方法的开头调用 super().first_method(self, *args, **kwargs) 。这很好,但我也想 return 来自 Parent 方法的一些数据,当 parent 方法和 child 方法 return 时看起来很奇怪声明中有所不同。更不用说我可能想在方法之后做一些 post-processing 工作,所以我需要 2 个不同的函数:开始和执行脚本之后。

我想出的下一件事是制作 MetaClass。 只需在创建 class 期间在新元类中实现所有方法的修饰,并将在 child 方法中使用的新生成数据传递给它们在 kwargs.

这是最接近我的目标的解决方案,但无论如何感觉不对。因为一些 kwargs 将被传递给 child 方法并不明确,如果你是这段代码的新手,那么你需要做一些研究来理解它是如何工作的。我觉得我过度设计了。

所以问题是:是否有任何模式或类似的东西可以实现此功能? 也许您可以为我的情况提供更好的建议? 非常感谢您!

所以,除了现有的模式:我不知道这是否有一个特定的名称,你需要什么,那将是一个“模式”是“插槽”的使用:也就是说 - 你记录特殊命名的方法这将作为另一个方法执行的一部分被调用。这个其他方法然后执行它的设置代码,检查是否有槽方法(通常可以通过名称识别),调用它们,用一个简单的方法调用,这将 运行 它的最专业版本,即使特殊调用槽的方法在基础 class 中,而你在一个大的 class-inheritance 层次结构中。

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

所以,如果这不是 Python,您所要做的就是对此进行规范,并记录这些方法不应直接调用,而应通过“入口点”方法调用 - 这可以简单地具有“ep_”前缀。这些必须在基础class上固定和硬编码,并且您需要为每个要prefix/postfix编码的方法使用一个。


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,更容易添加一些魔法来避免代码重复并保持简洁。

一个可能的事情是有一个特殊的 class ,当检测到子 class 中的方法时,应该将其作为包装方法的槽调用,如上所示,自动重命名that 方法:这样入口点方法可以具有与子方法相同的名称 - 更好的是,一个简单的装饰器可以标记意味着是“入口点”的方法,并且继承甚至对他们有用。

基本上,在构建新的 class 时,我们会检查所有方法:如果它们中的任何一个在调用层次结构中具有对应的部分并被标记为入口点,则会进行重命名。

如果任何入口点方法将作为第二个参数(第一个是self)作为要调用的开槽方法的参考,则更实用。

经过一些调整:好消息是不需要 custommetaclass - baseclass 中的 __init_subclass__ 特殊方法足以启用装饰器。

坏消息:由于 entry-point 中的 re-entry 次迭代是由最终方法上对“super()”的潜在调用触发的,在需要中间 classes。我还注意设置了一些 multi-threading 保护措施——尽管这不是 100% bullet-proof.

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)