如何在Python中实现虚方法?

122

我了解PHP或Java中的虚方法。

在Python中如何实现虚方法?

还是说我需要在抽象类中定义一个空方法并重写它?

8个回答

130

当然,你甚至不必在基类中定义一个方法。在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中,即使CatDog没有从共同的基类派生,它们也可以实现相同的行为 - 这个功能是默认提供的。尽管如此,一些程序员更喜欢以更严格的方式定义他们的类层次结构,以便更好地记录并强制执行某些类型的严格性。这也是可能的 - 例如,可以查看abc标准模块

56
+1 作为一个例子。顺便问一下,狗用什么语言说“汪汪”? - JeremyP
7
@JeremyP:嗯,说得好 :-) 我想在那些将“h”发音理解为“hippy”的第一个字母或者在西班牙语中发音类似于“Javier”的语言中。 - Eli Bendersky
7
@Eli:抱歉,但我非常想知道这个问题的答案。英语中它们说“woof”,好吧他们不这样说,但那是我们用来类比猫咪的“meow”和奶牛的“moo”的词。那么,“hau”是西班牙语吗? - JeremyP
15
@JeremyP 我确定那就是波兰狗说的 ;) - j_kubik
3
@JeremyP 是的,我确认在西班牙语中狗会说“Jau”,而用英文写作则是“Hau” :) 希望对你有帮助。 - SkyWalker
显示剩余6条评论

113

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

print Derived().usesVirtualMethod()
Base().usesVirtualMethod()
给出:
2
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 CanFly(Protocol):
    def fly(self) -> str:
        pass

    def fly_fast(self) -> str:
        return 'CanFly.fly_fast'

class Bird(CanFly):
    def fly(self):
        return 'Bird.fly'

    def fly_fast(self):
        return 'Bird.fly_fast'

class FakeBird(CanFly):
    pass

assert Bird().fly() == 'Bird.fly'
assert Bird().fly_fast() == 'Bird.fly_fast'
# mypy error
assert FakeBird().fly() is None
# mypy error
assert FakeBird().fly_fast() == 'CanFly.fly_fast'

如果我们运行此文件,断言将通过,因为我们没有添加任何动态类型检查:

python protocol.py

但是如果我们对mypy进行类型检查:

python -m pip install --user mypy
mypy protocol.py

我们得到了预期的错误:

protocol.py:22: error: Cannot instantiate abstract class "FakeBird" with abstract attribute "fly"
protocol.py:24: error: Cannot instantiate abstract class "FakeBird" with abstract attribute "fly"

有些不幸的是,错误检查仅在实例化时捕获错误,而不是在类定义时。

typing.Protocol 将方法视为抽象方法,当它们的主体是“空”时

我不确定他们认为什么是空的,但是以下所有内容都被认为是空的:

  • pass
  • ... 省略对象
  • raise NotImplementedError()

因此,最好的可能性很可能是:

protocol_empty.py

from typing import Protocol

class CanFly(Protocol):
    def fly(self) -> None:
        raise NotImplementedError()

class Bird(CanFly):
    def fly(self):
        return None

class FakeBird(CanFly):
    pass

Bird().fly()
FakeBird().fly()

失败是预期的:

protocol_empty.py:15: error: Cannot instantiate abstract class "FakeBird" with abstract attribute "fly"
protocol_empty.py:15: note: The following method was marked implicitly abstract because it has an empty function body: "fly". If it is not meant to be abstract, explicitly return None.

但是,如果我们替换:

raise NotImplementedError()

例如,随意添加一个“非空”的陈述:

x = 1

如果 mypy 没有将它们视为虚拟的,就不会产生错误。

@abc.abstractmethod:动态 + 静态 + 文档同时实现

之前在这里提到过:https://dev59.com/pG445IYBdhLWcg3wwcyg#19316077 ,但在 Python 3 中 metaclass 语法已经改变:

class C(metaclass=abc.ABCMeta):

不再使用Python 2:

class C:
    __metaclass__=abc.ABCMeta

现在要使用之前在https://dev59.com/pG445IYBdhLWcg3wwcyg#19316077提到的@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 Bat(CanFly):
    '''
    doc
    '''
    pass

def send_mail(flyer: CanFly) -> str:
    '''
    doc
    '''
    return flyer.fly()

assert send_mail(Bird()) == 'Bird.fly'
assert send_mail(Bat()) == 'Bat.fly'

那么:

mypy abc_bad.py

失败,正是预期的结果:

main.py:40: error: Cannot instantiate abstract class "Bat" with abstract attribute "fly"

Sphinx:如何在文档中显示

请参见:如何在Sphinx文档中将成员注释为抽象?

在上述提到的方法中,只有一个会在Sphinx文档输出中显示:@abc.abstractmethod

enter image description here

结语

参考文献:

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


59

Python的方法始终是虚拟的。


1
除了双下划线方法。 - Konstantin
3
这个回答并没有很好地帮助实现接口类的目标,而这正是使用虚方法的主要原因之一。 - Jean-Marc Volle

25

实际上,在Python 2.6版本中提供了称为抽象基类的东西,您可以像这样显式设置虚拟方法:

from abc import ABCMeta
from abc import abstractmethod
...
class C:
    __metaclass__ = ABCMeta
    @abstractmethod
    def my_abstract_method(self, ...):

只要这个类没有继承已经使用元类的类,它就可以非常好地工作。

来源:http://docs.python.org/2/library/abc.html


1
这个指令有没有 Python 3 的等效版本? - locke14
@locke14,Python 3中元类语法已更改,现在您需要编写class C(metaclass=abc.ABCMeta):https://dev59.com/pG445IYBdhLWcg3wwcyg#38717503 - Ciro Santilli OurBigBook.com

11

Python的方法始终是虚拟的

就像Ignacio所说的,使用类继承可能是实现您想要的功能的更好方法。

class Animal:
    def __init__(self,name,legs):
        self.name = name
        self.legs = legs

    def getLegs(self):
        return "{0} has {1} legs".format(self.name, self.legs)

    def says(self):
        return "I am an unknown animal"

class Dog(Animal): # <Dog inherits from Animal here (all methods as well)

    def says(self): # <Called instead of Animal says method
        return "I am a dog named {0}".format(self.name)

    def somethingOnlyADogCanDo(self):
        return "be loyal"

formless = Animal("Animal", 0)
rover = Dog("Rover", 4) #<calls initialization method from animal

print(formless.says()) # <calls animal say method

print(rover.says()) #<calls Dog says method
print(rover.getLegs()) #<calls getLegs method from animal class

结果应该是:

I am an unknown animal
I am a dog named Rover
Rover has 4 legs

9
在C++中,类的派生类通过引用或指向基类的指针调用方法实现,这种方法称为虚方法。但在Python中,由于其没有类型,类似于虚方法的概念并不存在。(不过我不知道Java和PHP中的虚方法是如何工作的)。
但是,如果你所说的“虚方法”是指在继承层次结构中调用最底层的实现方法,那么在Python中你总是会得到这个结果,正如几个答案所指出的那样。
嗯,几乎总是...
正如dplamp指出的,不是所有的Python方法都是这样的。Dunder方法不是这样的。我认为这是一个不太为人所知的特性。
考虑下面这个人造的例子。
class A:
    def prop_a(self):
        return 1
    def prop_b(self):
        return 10 * self.prop_a()

class B(A):
    def prop_a(self):
        return 2

现在
>>> B().prop_b()
20
>>> A().prob_b()
10

然而,考虑这一个。
class A:
    def __prop_a(self):
        return 1
    def prop_b(self):
        return 10 * self.__prop_a()

class B(A):
    def __prop_a(self):
        return 2

现在
>>> B().prop_b()
10
>>> A().prob_b()
10

我们唯一改变的是将prop_a()变成了dunder方法。
第一种行为的问题在于,如果您不影响prop_b()的行为,就无法在派生类中更改prop_a()的行为。 Raymond Hettinger的这个非常好的演讲给出了一个使用案例,说明这种情况是不方便的。

0
虚拟并不等同于在继承子类中实现抽象。
虚拟应该与接口合同一起使用,这样你可以给代码一些额外的灵活性,并且仍然能够获得低代码重复的好处,因为父类可以实现适用于9/10的类的基本函数实现,通过虚拟,你可以覆盖1/10的函数而无需在子类x、y或n中复制函数,因为抽象定义会要求你这样做*。
不幸的是,我怀疑没有任何强大的解决方案来支持Python中的虚拟,因为即使接口也还不是内置模块的一部分(abc模块,即使经常用于模拟接口,也不能真正被视为适当的接口支持)。
*从软件设计质量的角度来说,这是一种将代码向低耦合、高内聚质量目标移动的技术,这将导致反意大利面和最终的代码生成和完全通用的模块化编码哲学,它是作为一个生成引擎编写的,而不是自定义实现。有许多面向对象编程员声称面向对象编程由于其“本质”而无法从头开始生成,实际上面向对象编程不应该是别的东西,特别是如果你在运行时之前完成所有这些(在其中生成整个运行时代码),那就更好了。
在实例化新对象的情况下,您可以像图形渲染库中那样启用高性能。生成一个对象传输器,以处理突然对对象的需求,并在顶层库中处理此类实时性能关键需求的管理。此外,根据您的使用情况,您可以将它们存储在高性能数据结构中,例如堆栈,或者堆栈的列表/字典,以便进行分组。

0

Python 3.6 引入了__init_subclass__,这使得你可以轻松地执行以下操作:

class A:

    def method(self):
        '''method needs to be overwritten'''
        return NotImplemented

    def __init_subclass__(cls):
        if cls.method is A.method:
            raise NotImplementedError(
                'Subclass has not overwritten method {method}!')

这个解决方案的好处在于避免了abc元类,直接给用户一个命令来正确地执行它。除此之外,还有另一个答案在调用该方法时会引发NotImplementedError的情况。这个解决方案在运行时进行检查,而不仅仅是如果用户调用该方法才进行检查。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接