如何在Python中实现虚拟方法?

123 投票
9 回答
150360 浏览
提问于 2025-04-16 10:07

我知道PHP或Java中的虚拟方法。

那么在Python中怎么实现它们呢?

或者我需要在一个抽象类中定义一个空的方法,然后再去重写它吗?

9 个回答

59

Python中的方法总是虚拟的。

115
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 NotImplementedErrorProtocol相比:

  • 缺点:更冗长
  • 优点:完成所有动态检查、静态检查,并在文档中显示(见下文)

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文档中标注一个成员为抽象?

在上述提到的方法中,只有一种会出现在sphinx文档输出中:@abc.abstractmethod

在此输入图像描述

结束语

参考文献:

在Python 3.10.7、mypy 0.982、Ubuntu 21.10上测试。

132

当然,你甚至不需要在基类中定义一个方法。在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标准模块

撰写回答