Python抽象基类能强制函数签名吗?
假设我定义了一个抽象基类,像这样:
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 个回答
我觉得在基础的 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
仍然在不断完善中,但在这个情况下它是有效的。有一些特殊情况可能会漏掉某些签名变体,但总体来说,它在大多数实际应用中都能正常工作。
我建议你看看pylint这个工具。我把你的代码用它进行了一下静态分析,在你定义quack()方法的那一行,它给出了报告:
Argument number differs from overridden method (arguments-differ)
情况比你想的还要糟糕。抽象方法只通过名字来跟踪,所以你甚至不需要把 quack
定义成一个方法,就可以创建子类的实例。
class SurrealDuck(Quacker):
quack = 3
d = SurrealDuck()
print d.quack # Shows 3
系统里没有任何东西强制要求 quack
必须是一个可调用的对象,更不用说它的参数要和抽象方法的原始参数匹配了。最好的办法是你可以继承 ABCMeta
,然后自己添加代码来比较子类和父类中方法的参数类型,但这实现起来并不简单。
(目前,把某个东西标记为“抽象”,实际上只是把名字添加到父类的一个冻结集合属性中(Quacker.__abstractmethods__
)。让一个类可以实例化其实只需要把这个属性设置为空的可迭代对象,这在测试时很有用。)