Python抽象基类能强制函数签名吗?

51 投票
3 回答
14123 浏览
提问于 2025-04-18 16:23

假设我定义了一个抽象基类,像这样:

from abc import abstractmethod, ABCMeta

class Quacker(object):
  __metaclass__ = ABCMeta

  @abstractmethod
  def quack(self):
    return "Quack!"

这就确保了任何从Quacker类派生的类都必须实现quack这个方法。但是如果我这样定义:

class PoliteDuck(Quacker):
   def quack(self, name):
     return "Quack quack %s!" % name

d = PoliteDuck()  # no error

我可以创建这个类的实例,因为我提供了quack方法,但函数的签名不匹配。我能理解在某些情况下这可能有用,但我更想确保我能确实调用这些抽象方法。如果函数签名不同,这可能会出错!

所以:我该如何强制要求函数签名匹配呢?如果签名不匹配,我希望在创建对象时能报错,就像我根本没有定义这个方法一样。

知道这并不是常规做法,而且如果我想要这样的保证,Python其实并不是合适的语言,但这不是重点——这可能吗?

3 个回答

5

我觉得在基础的 python 语言中,这个问题没有变化,不过我找到了一种可能有用的解决方法。mypy 这个包似乎会强制要求抽象基类和它们的具体实现遵循相同的签名。所以基本上,如果你在抽象基类上定义了一个签名,所有具体类都必须严格按照这个签名来写。

这里有一个例子,在 mypy 中会出错。这个代码来自 mypy网站,我为了这个回答做了一些调整。

第一个例子是可以通过的代码。注意 eat 方法的签名是一样的,mypy 不会报错。

from abc import ABCMeta, abstractmethod

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def eat(self, food: str) -> None: pass

    @property
    @abstractmethod
    def can_walk(self) -> bool: pass

class Cat(Animal):
    def eat(self, food: str) -> None:
        pass  # Body omitted

    @property
    def can_walk(self) -> bool:
        return True


y = Cat()     # OK

但是我们稍微改一下这个代码,结果 mypy 就会报错了:

from abc import ABCMeta, abstractmethod

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def eat(self, food: str) -> None: pass

    @property
    @abstractmethod
    def can_walk(self) -> bool: pass

class Cat(Animal):
    def eat(self, food: str, drink: str) -> None:
        pass  # Body omitted

    @property
    def can_walk(self) -> bool:
        return True


y = Cat()     # Error

Mypy 仍然在不断完善中,但在这个情况下它是有效的。有一些特殊情况可能会漏掉某些签名变体,但总体来说,它在大多数实际应用中都能正常工作。

8

我建议你看看pylint这个工具。我把你的代码用它进行了一下静态分析,在你定义quack()方法的那一行,它给出了报告:

Argument number differs from overridden method (arguments-differ)

(https://en.wikipedia.org/wiki/Pylint)

42

情况比你想的还要糟糕。抽象方法只通过名字来跟踪,所以你甚至不需要把 quack 定义成一个方法,就可以创建子类的实例。

class SurrealDuck(Quacker):
    quack = 3

d = SurrealDuck()
print d.quack   # Shows 3

系统里没有任何东西强制要求 quack 必须是一个可调用的对象,更不用说它的参数要和抽象方法的原始参数匹配了。最好的办法是你可以继承 ABCMeta,然后自己添加代码来比较子类和父类中方法的参数类型,但这实现起来并不简单。

(目前,把某个东西标记为“抽象”,实际上只是把名字添加到父类的一个冻结集合属性中(Quacker.__abstractmethods__)。让一个类可以实例化其实只需要把这个属性设置为空的可迭代对象,这在测试时很有用。)

撰写回答