Python - 为子类强制执行特定的方法签名?

Posted

技术标签:

【中文标题】Python - 为子类强制执行特定的方法签名?【英文标题】:Python - Enforce specific method signature for subclasses? 【发布时间】:2019-08-14 00:59:12 【问题描述】:

我想创建一个定义特定接口的类,然后要求所有子类都符合这个接口。比如我想定义一个类

class Interface:
    def __init__(self, arg1):
       pass

    def foo(self, bar):
       pass

然后请放心,如果我持有任何类型为 A 的元素 aInterface 的子类),那么我可以调用 a.foo(2) 它会起作用。

看起来this question 几乎解决了这个问题,但在这种情况下,由 子类 来明确更改它的元类。

理想情况下,我正在寻找类似于 Rust 中的 Traits 和 Impls 的东西,我可以在其中指定特定的 Trait 和 trait 需要定义的方法列表,然后我可以确保任何具有该 Trait 的对象定义了这些方法。

有没有办法在 Python 中做到这一点?

【问题讨论】:

可能有办法,但为什么呢? Python 并不是这种模式的真正语言(我们在这里都是同意的成年人)。 @gmds 我想你是对的。就我而言,我正在编写一个库,其目的是让库的用户编写我的基类型的子类,然后,只要他们正确实现了基类的方法(并支持相同的方法签名) 然后他们可以将该对象传递给库中的各种函数,并且它将“正常工作”。当它解析他们的实现时显示一个错误会更好,警告他们“嘿,你在这里忘记了一个参数”,而不是让它在某些代码中显示 6 层或做一些不可预测的事情。 您可以遵循pyspark 模式(例如,请参阅我的答案) 我认为链接问题的基于元类的方法实际上也适用于使用元类的基类,这将使子类自动使用它。 @ShadowRanger 这是我最初的想法,但元类不会被继承。 【参考方案1】:

所以,首先,明确一点 - Python 有一个内置机制来测试派生类中方法和 属性存在性 - 它就是这样做的不检查他们的签名。

其次,一个不错的包是zope.interface。除了 zope 命名空间,它是一个完整的独立包,它允许拥有可以公开多个接口的对象的非常简洁的方法,但只是在需要时——然后释放命名空间。它肯定需要一些学习才能习惯它,但它可能非常强大,并为大型项目提供了非常好的模式。

它是为 Python 2 设计的,当时 Python 的功能比现在少得多——而且我认为它不执行自动接口检查(必须手动调用一个方法来确定一个类是否兼容)——但是尽管如此,自动执行此调用会很容易。

第三,How to enforce method signature for child classes? 的链接接受答案几乎可以工作,只需进行一次更改就足够了。该示例的问题在于它硬编码了对type 的调用以创建新类,并且不传递有关元类本身的type.__new__ 信息。换行:

return type(name, baseClasses, d)

为:

return super().__new__(cls, name, baseClasses, d)

然后,使基类(定义您所需方法的基类使用元类)将被任何子类正常继承。 (只需使用 Python 的 3 语法来指定元类)。

抱歉 - 该示例是 Python 2 - 它也需要在另一行进行更改,我最好重新发布:

from types import FunctionType

# from https://***.com/a/23257774/108205
class SignatureCheckerMeta(type):
    def __new__(mcls, name, baseClasses, d):
        #For each method in d, check to see if any base class already
        #defined a method with that name. If so, make sure the
        #signatures are the same.
        for methodName in d:
            f = d[methodName]
            for baseClass in baseClasses:
                try:
                    fBase = getattr(baseClass, methodName)

                    if not inspect.getargspec(f) == inspect.getargspec(fBase):
                        raise BadSignatureException(str(methodName))
                except AttributeError:
                    #This method was not defined in this base class,
                    #So just go to the next base class.
                    continue

        return super().__new__(mcls, name, baseClasses, d)

回顾一下,我发现其中没有任何机制可以强制实际实施方法。 IE。如果派生类中存在具有相同名称的方法,则强制执行其签名,但如果派生类中根本不存在该方法,则上面的代码将不会发现它(并且超类上的方法将是调用 - 这可能是一种期望的行为)。

答案:

第四 - 虽然这会起作用,但它可能有点粗糙 - 因为它的 any 方法覆盖任何超类中的另一个方法必须符合其签名。甚至兼容的签名也会中断。也许建立在ABCMeta@abstractmethod 现有机制的基础上会很好,因为这些机制已经适用于所有极端情况。但是请注意,此示例基于上面的代码,并在 class 创建时检查签名,而 Python 中的 abstractclass 机制使其在类被实例化时检查。保持不变将使您能够使用大型类层次结构,这可能会在中间类中保留一些抽象方法,而最终的具体类必须实现所有方法。 只需使用它而不是ABCMeta 作为接口类的元类,并像往常一样将要检查接口的方法标记为@abstractmethod

class M(ABCMeta):
    def __init__(cls, name, bases, attrs):
        errors = []
        for base_cls in bases:
            for meth_name in getattr(base_cls, "__abstractmethods__", ()):
                orig_argspec = inspect.getfullargspec(getattr(base_cls, meth_name))
                target_argspec = inspect.getfullargspec(getattr(cls, meth_name))
                if orig_argspec != target_argspec:
                    errors.append(f"Abstract method meth_name!r  not implemented with correct signature in cls.__name__!r. Expected orig_argspec.")
        if errors: 
            raise TypeError("\n".join(errors))
        super().__init__(name, bases, attrs)

【讨论】:

完美!我注意到有关方法存在的相同缺陷,但不知道如何将@abstractmethod 与元类方法集成。您的最终答案为我勾选了所有方框,谢谢! 这怎么只有 1 票?真的很有用,而且非常重要。【参考方案2】:

您可以遵循pyspark 模式,其中基类的方法执行(可选)参数有效性检查,然后调用子类的“非公共”方法,例如:

class Regressor():

    def fit(self, X, y):
        self._check_arguments(X, y)
        self._fit(X, y)

    def _check_arguments(self, X, y):
        if True:
             pass

        else:
            raise ValueError('Invalid arguments.')

class LinearRegressor(Regressor):

    def _fit(self, X, y):
        # code here

【讨论】:

我觉得对于大多数用例来说,这是一个很好的解决方案,开发人员只是希望对方法签名进行一些简单的检查,而不需要太深入地了解 Python 元功能。

以上是关于Python - 为子类强制执行特定的方法签名?的主要内容,如果未能解决你的问题,请参考以下文章

Python 覆盖子类中方法返回的类型提示,无需重新定义方法签名

Java:在超类方法签名中返回子类

python 强制子类实现父类方法

UML图标含义及记忆方法

python 抽象基类(abc)

流畅的python第十一章接口学习记录