如何在Python中实现虚拟方法?
我知道PHP或Java中的虚拟方法。
那么在Python中怎么实现它们呢?
或者我需要在一个抽象类中定义一个空的方法,然后再去重写它吗?
9 个回答
Python中的方法总是虚拟的。
x = 1
当前实现状态的总结,重点关注“如果方法没有实现则优雅地崩溃”的行为:
方法 | 异常 | mypy静态检查 | sphinx |
---|---|---|---|
raise NotImplementedError() |
在方法调用时 | 否 | 否 |
typing.Protocol |
否 | 是 | 否 |
@abc.abstractmethod |
在实例化时 | 是 | 是 |
raise NotImplementedError()
: 动态类型检查
这是推荐在“抽象”基类的“纯虚方法”中抛出的异常,用于那些没有实现的方法。
https://docs.python.org/3.5/library/exceptions.html#NotImplementedError中提到:
这个异常是从
RuntimeError
派生的。在用户定义的基类中,抽象方法应该在需要派生类重写该方法时抛出这个异常。
正如其他人所说,这主要是一种文档约定,并不是强制要求,但这样你得到的异常信息比缺少属性错误要有意义得多。
dynamic.py
class Base(object):
def virtualMethod(self):
raise NotImplementedError()
def usesVirtualMethod(self):
return self.virtualMethod() + 1
class Derived(Base):
def virtualMethod(self):
return 1
assert Derived().usesVirtualMethod() == 2
Base().usesVirtualMethod()
给出的结果是:
Traceback (most recent call last):
File "./dynamic.py", line 13, in <module>
Base().usesVirtualMethod()
File "./dynamic.py", line 6, in usesVirtualMethod
return self.virtualMethod() + 1
File "./dynamic.py", line 4, in virtualMethod
raise NotImplementedError()
NotImplementedError
typing.Protocol
: 静态类型检查(Python 3.8)
Python 3.8增加了typing.Protocol
,现在我们也可以静态检查一个虚方法是否在子类中实现。
protocol.py
from typing import Protocol
class Bird(Protocol):
def fly(self) -> str:
pass
def peck(self) -> str:
return 'Bird.peck'
class Pigeon(Bird):
def fly(self):
return 'Pigeon.fly'
def peck(self):
return 'Pigeon.peck'
class Parrot(Bird):
def fly(self):
return 'Parrot.fly'
class Dog(Bird):
pass
pigeon = Pigeon()
assert pigeon.fly() == 'Pigeon.fly'
assert pigeon.peck() == 'Pigeon.peck'
parrot = Parrot()
assert parrot.fly() == 'Parrot.fly'
assert parrot.peck() == 'Bird.peck'
# mypy error
dog = Dog()
assert dog.fly() is None
assert dog.peck() == 'Bird.peck'
如果我们运行这个文件,断言会通过,因为我们没有添加任何动态类型检查:
python protocol.py
但是如果我们用mypy
进行类型检查:
python -m pip install --user mypy
mypy protocol.py
我们会得到预期的错误:
rotocol.py:31: error: Cannot instantiate abstract class "Dog" with abstract attribute "fly" [abstract]
Found 1 error in 1 file (checked 1 source file)
不过有点遗憾的是,错误检查只在实例化时捕捉到错误,而不是在类定义时。
typing.Protocol
将方法视为抽象,当它们的主体是“空”的时候
我不太确定他们认为什么是空,但以下所有情况都被视为空:
pass
...
省略号对象raise NotImplementedError()
所以最好的可能性是:
protocol_empty.py
from typing import Protocol
class Bird(Protocol):
def fly(self) -> None:
raise NotImplementedError()
class Pigeon(Bird):
def fly(self):
return None
class Dog(Bird):
pass
Bird().fly()
Dog().fly()
这将按预期失败:
protocol_empty.py:14: error: Cannot instantiate protocol class "Bird" [misc]
protocol_empty.py:15: error: Cannot instantiate abstract class "Dog" with abstract attribute "fly" [abstract]
protocol_empty.py:15: note: "fly" is implicitly abstract because it has an empty function body. If it is not meant to be abstract, explicitly `return` or `return None`.
Found 2 errors in 1 file (checked 1 source file)
但是如果我们例如用一些随机的“非空”语句替换:
raise NotImplementedError()
那么mypy
就不会将它们视为虚方法,也不会给出错误。
@abc.abstractmethod
: 动态 + 静态 + 文档一次搞定
之前提到过:https://stackoverflow.com/a/19316077/895245,但metaclass
的语法在Python 3中变成了:
class C(metaclass=abc.ABCMeta):
而不是Python 2中的:
class C:
__metaclass__=abc.ABCMeta
所以现在使用@abc.abstractmethod
,需要:
abc_cheat.py
import abc
class C(metaclass=abc.ABCMeta):
@abc.abstractmethod
def m(self, i):
pass
try:
c = C()
except TypeError:
pass
else:
assert False
与raise NotImplementedError
和Protocol
相比:
- 缺点:更冗长
- 优点:完成所有动态检查、静态检查,并在文档中显示(见下文)
https://peps.python.org/pep-0544确实提到了这两种方法
例如:
abc_bad.py
#!/usr/bin/env python
import abc
class CanFly(metaclass=abc.ABCMeta):
'''
doc
'''
@abc.abstractmethod
def fly(self) -> str:
'''
doc
'''
pass
class Bird(CanFly):
'''
doc
'''
def fly(self):
'''
doc
'''
return 'Bird.fly'
class Dog(CanFly):
'''
doc
'''
pass
def send_mail(flyer: CanFly) -> str:
'''
doc
'''
return flyer.fly()
assert send_mail(Bird()) == 'Bird.fly'
assert send_mail(Dog()) == 'Dog.fly'
然后:
mypy abc_bad.py
按预期失败,结果是:
main.py:40: error: Cannot instantiate abstract class "Dog" with abstract attribute "fly"
Sphinx: 让它在文档中显示
在上述提到的方法中,只有一种会出现在sphinx文档输出中:@abc.abstractmethod
。
结束语
参考文献:
- https://peps.python.org/pep-0544
typing.Protocol
的PEP - 在Python中可以创建抽象类吗?
- 在Python中用什么替代接口/协议
在Python 3.10.7、mypy 0.982、Ubuntu 21.10上测试。
当然,你甚至不需要在基类中定义一个方法。在Python中,方法比虚拟方法更好,因为它们是完全动态的,Python使用的是一种叫做鸭子类型的类型系统。
class Dog:
def say(self):
print "hau"
class Cat:
def say(self):
print "meow"
pet = Dog()
pet.say() # prints "hau"
another_pet = Cat()
another_pet.say() # prints "meow"
my_pets = [pet, another_pet]
for a_pet in my_pets:
a_pet.say()
在Python中,Cat
(猫)和Dog
(狗)不需要从一个共同的基类派生出来就能实现这种行为——你可以免费获得这个特性。不过,有些程序员喜欢以更严格的方式定义他们的类层次结构,这样可以更好地记录它,并施加一些类型的严格性。这也是可以做到的——比如可以查看abc
标准模块。