如何强制子类遵循方法签名?

4 投票
6 回答
4007 浏览
提问于 2025-04-18 04:00

像C#和Java这样的编程语言有一个叫“方法重载”的特性,这意味着如果子类没有用完全相同的方法签名来实现父类的方法,就不会覆盖父类的方法。

那么在Python中,我们如何强制子类遵循父类的方法签名呢?下面的代码示例显示了子类用不同的方法签名覆盖了父类的方法:

>>> class A(object):
...   def m(self, p=None):
...     raise NotImplementedError('Not implemented')
... 
>>> class B(A):
...   def m(self, p2=None):
...     print p2
... 
>>> B().m('123')
123

虽然这并不是特别重要,或者说可能是Python的设计使然(比如使用*args和**kwargs),但我问这个问题是为了更清楚地了解是否有可能做到这一点。

请注意:

我已经尝试过使用@abstractmethodABC了。

6 个回答

0

之所以会被覆盖,是因为它们实际上有相同的方法签名。这里写的内容就像在Java中做的事情:

public class A
{
    public void m(String p)
    {
        throw new Exception("Not implemented");
    }
}

public class B extends A
{
    public void m(String p2)
    {
        System.out.println(p2);
    }
}

注意,虽然参数的名字不同,但它们的类型是相同的,因此它们有相同的签名。在像Java这样的强类型语言中,我们可以提前明确地说明参数的类型。

而在Python中,参数的类型是在运行时动态确定的,也就是说,当你使用这个方法时,Python会根据你传入的值来判断类型。这就导致Python解释器无法知道你实际上想调用哪个方法,比如当你说B().m('123')时。因为这两个方法的签名都没有指定它们期望的参数类型,只是说我需要一个参数。所以,调用最深层次的(也就是和你实际拥有的对象最相关的)方法是合理的,这里就是类B的方法,因为你是类B的实例。

如果你想在子类的方法中只处理某些类型,并把其他类型传递给父类,可以这样做:

class A(object):
    def m(self, p=None):
        raise NotImplementedError('Not implemented')

class B(A):
    def m(self, p2=None):
        if isinstance(p2, int):
            print p2
        else:
            super(B, self).m(p2)

然后使用b就能得到你想要的输出。也就是说,类b处理整数类型,并把其他任何类型传递给它的父类。

>>> b = B()
>>> b.m(2)
2
>>> b.m("hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in m
  File "<stdin>", line 3, in m
NotImplementedError: Not implemented
0

这个语言本身设计上不支持检查函数的参数类型和数量。如果你想了解更多,可以看看这个链接:

http://grokbase.com/t/python/python-ideas/109qtkrzsd/abc-what-about-the-method-arguments

在这个讨论中,听起来你可以写一个装饰器来检查函数的参数,比如用 abc.same_signature(method1, method2) 来比较两个函数的参数,但我自己从来没有尝试过。

1

mypy,以及我想其他静态类型检查工具,也会发出警告,如果你在子类中定义的方法和它们覆盖的父类方法的参数不一样。我的看法是,确保子类的方法参数和父类一致的最好办法就是使用mypy(或者其他类似的工具)。

3

这是对之前接受的答案的更新,目的是让它能在Python 3.5版本中正常工作。

import inspect
from types import FunctionType

class BadSignatureException(Exception):
    pass


class SignatureCheckerMeta(type):
    def __new__(cls, 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]

            if not isinstance(f, FunctionType):
                continue
            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 type(name, baseClasses, d)


def main():
    class A(object):
        def foo(self, x):
            pass

    try:
        class B(A, metaclass=SignatureCheckerMeta):
            def foo(self):
                """This override shouldn't work because the signature is wrong"""
                pass
    except BadSignatureException:
        print("Class B can't be constructed because of a bad method signature")
        print("This is as it should be :)")

    try:
        class C(A):
            __metaclass__ = SignatureCheckerMeta
            def foo(self, x):
                """This is ok because the signature matches A.foo"""
                pass
    except BadSignatureException:
        print("Class C couldn't be constructed. Something went wrong")


if __name__ == "__main__":
    main()
3

下面是一个完整的示例,展示了如何使用元类来确保子类的方法和父类的方法有相同的参数格式。请注意这里使用了inspect模块。用这种方式,它会确保参数格式是完全相同的,这可能不是你想要的效果。

import inspect

class BadSignatureException(Exception):
    pass


class SignatureCheckerMeta(type):
    def __new__(cls, 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).__func__
                    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 type(name, baseClasses, d)


def main():

    class A(object):
        def foo(self, x):
            pass

    try:
        class B(A):
            __metaclass__ = SignatureCheckerMeta
            def foo(self):
                """This override shouldn't work because the signature is wrong"""
                pass
    except BadSignatureException:
        print("Class B can't be constructed because of a bad method signature")
        print("This is as it should be :)")

    try:
        class C(A):
            __metaclass__ = SignatureCheckerMeta
            def foo(self, x):
                """This is ok because the signature matches A.foo"""
                pass
    except BadSignatureException:
        print("Class C couldn't be constructed. Something went wrong")


if __name__ == "__main__":
    main()

撰写回答