Python 抽象类,抽象方法介绍,以及定义和实现抽象类,抽象方法
关注 1421
前提
阅读本节的前提是对 Python 类继承以及类的多态性有所掌握,你可以查看Python 类继承介绍,以及实现类继承,多重继承,方法重写,编程教程的类的多态性,方法重写,方法重载,方法隐藏介绍来了解相关信息。
Python 中的接口
你不能像在其他语言(比如,C#)中一样,使用类似于interface
这样的关键字,为 Python 定义接口(Interface)。Python 中的“接口”仅是概念性词汇,用于表示一种功能实现上的约定。
接口
想要了解更多关于接口的信息,你可以查看编程教程的接口,隐式实现,显式实现介绍一节。
定义 Python 抽象类
虽然无法定义接口,但抽象类可以达到与接口近似的效果,他能够约束其派生类必须通过重写(Override)来实现某些方法或特性。
同样的,Python 没有提供类似于abstract
这样的关键字,用于声明一个抽象类或抽象基类(Abstract Base Classe,ABC),Python 抽象基类的定义需要通过指定元类(Meta Class)来完成,任何指定abc
模块的ABCMeta
类为元类的类,均可被称为抽象基类。指定元类需要使用关键字metaclass
,因此,定义抽象基类的语法如下。
class <classname>(<metaclass=ABCMeta>):
<block>
- classname 部分
classname
为抽象基类的名称,他需要符合 Python 的标识符规范,不能使用关键字或保留关键字。- block 部分
block
为抽象基类的主体代码,需要使用某种空白字符进行缩进,以表示其归属于抽象基类。
abc 模块的 ABCMeta 类
abc
模块的ABCMeta
类继承自type
,可以作为其他类的元类,来实现抽象基类的效果。元类通常用于在实例的创建过程中施加更多控制,ABCMeta
类的作用之一便是检查是否所有的抽象方法或特性均被重写,如果没有,则实例无法被创建。除此之外,ABCMeta
并不会过多的在其他方面影响类,你可以在抽象类中进行一些常规操作,比如,定义并使用非抽象的实例方法,类方法等。
需要指出,ABCMeta
类不会出现在抽象基类和其派生类的方法解析顺序(Method Resolution Order,MRO)中,ABCMeta
类不是抽象基类的基类。
下面,我们将ABCMeta
指定为类Movable
的元类,查看抽象基类Movable
的方法解析顺序和基类,发现ABCMeta
不在其中。
from abc import ABCMeta
# 表示可移动的抽象基类
class Movable(metaclass=ABCMeta):
pass
# 显示 Movable 的方法解析顺序和基类
print(Movable.__mro__)
print(Movable.__bases__)
(<class '__main__.Movable'>, <class 'object'>)
(<class 'object'>,)
当然,使用关键字metaclass
可能会让人感觉怪怪的,Python 提供了另一种定义抽象基类的方式,直接继承abc
模块的ABC
类,语法形式如下,其中classname
和block
的含义与之前相同。
class <classname>(ABC):
<block>
abc 模块的 ABC 类
abc
模块的ABC
类是一个辅助类(Helper Class),他将ABCMeta
指定为元类,从ABC
派生的类会成为抽象基类。你不能像使用ABCMeta
类一样,通过metaclass
关键字来使用ABC
,那将导致一些问题。
与ABCMeta
类不同,ABC
会出现在抽象基类和其派生类的方法解析顺序中,ABC
类是抽象基类的基类。
下面,我们从ABC
派生Accessible
类,查看抽象基类Accessible
的方法解析顺序和基类,发现ABC
在其中。
from abc import ABC
# 表示可访问的抽象基类
class Accessible(ABC):
pass
# 显示 Accessible 的方法解析顺序和基类
print(Accessible.__mro__)
print(Accessible.__bases__)
(<class '__main__.Accessible'>, <class 'abc.ABC'>, <class 'object'>)
(<class 'abc.ABC'>,)
为 Python 抽象类定义抽象方法
在抽象基类或其派生类中,你可以使用abc
模块提供的@abstractmethod
修饰符,来定义抽象方法或本质上是方法的抽象特性。@abstractmethod
修饰符可与其他修饰符(@classmethod
,@staticmethod
,@property
等)一起使用,来构成更为复杂的方法定义,比如,抽象的类方法,抽象的静态方法,抽象属性等。
Python 3.2 曾经加入了一些其他关于抽象的修饰符,但在 Python 3.3 中被弃用,他们是@abstractclassmethod
,@abstractstaticmethod
,@abstractproperty
。
ABCMeta
会检查从抽象基类派生的类,是否重写了继承链中所有已定义的抽象方法或特性,当该类的实例被创建时。
abc 模块的 @abstractmethod 修饰符
@abstractmethod
修饰符的主要目的是标记抽象方法,以让ABCMeta
类对这些方法进行检查,他并不影响被标记方法的其他方面。比如,被标记为抽象方法的方法可拥有自己的主体代码,并且可通过super
在派生类中访问。
@abstractmethod
修饰符应书写在相关修饰符(@classmethod
,@staticmethod
,@property
等)之后,以避免引发异常AttributeError
。
抽象类方法,抽象静态方法可在未实现的状态下被调用
由于ABCMeta
仅在创建实例时,对抽象方法或特性的实现情况进行检查,因此,抽象类方法和抽象静态方法,或本质上是方法的抽象类特性,可以在未被实现的情况下直接调用,只要此过程不创建相关的实例。
在下面的代码中,Animal
类定义了两个抽象方法count
和name
,他们未被派生类Cat
实现,但通过Cat
或Animal
来调用却是可行的。
from abc import ABCMeta, abstractmethod
class Animal(metaclass=ABCMeta):
# 抽象类方法 count
@classmethod
@abstractmethod
def count(cls):
pass
# 抽象静态方法 name
@staticmethod
@abstractmethod
def name():
print('名字到底是什么啊?')
class Cat(Animal):
pass
# 调用未实现的静态方法 name 和类方法 count
Cat.name()
Animal.count()
# ERROR Cat 并未实现抽象基类 Animal
Cat().name()
名字到底是什么啊?
…
TypeError: Can't instantiate abstract class Cat without an implementation for abstract methods 'count', 'name'
使用 update_abstractmethods 函数更新抽象类
abc
模块的update_abstractmethods
函数(支持作为类的修饰符来使用),可用于更新一个抽象类,当该类动态添加抽象特性,或抽象特性的状态发生变化时。
需要指出,update_abstractmethods
函数只更新目标抽象类,并不更新目标抽象类的基类或派生类。
update_abstractmethods(cls)
- cls 参数
cls
参数为需要更新的抽象类。
接下来,我们为抽象基类Color
动态增加一个抽象方法show
,Black
类没有实现该方法,但创建其实例是可行的,因为ABCMeta
并不知道新加入的抽象方法,直至通过update_abstractmethods
函数更新相关的抽象类。
from abc import ABC, abstractmethod, update_abstractmethods
# 定义抽象基类 Color 和派生类 Black
class Color(ABC):
pass
class Black(Color):
pass
# 动态的为抽象基类增加一个抽象方法
@abstractmethod
def show(self):
pass
Color.show = show
print('创建第一个 Black 实例')
# 创建实例没有问题
Black()
# 更新抽象类 Color 和 Black
update_abstractmethods(Color)
update_abstractmethods(Black)
print('创建第二个 Black 实例')
# ERROR 不能创建实例,因为 Black 没有实现抽象方法 show
Black()
创建第一个 Black 实例
创建第二个 Black 实例
…
TypeError: Can't instantiate abstract class Black without an implementation for abstract method 'show'
实现 Python 抽象类,抽象方法
正如上面所描述的,对已经定义的抽象方法或本质为方法的抽象特性进行重写,意味着实现该抽象方法或特性。当抽象类中的所有抽象方法和特性均被实现时,即表示该抽象类被实现。
重写抽象方法或特性不能使用修饰符 @abstractmethod
在重写抽象方法或特性时,不能为其再次书写修饰符@abstractmethod
,否则该方法或特性将被认定为抽象定义,而不是对抽象的实现。
在下面的示例中,Flower
类实现了Plant
类的抽象方法grow
,并定义了自己的抽象类方法count
,因此Plant
和Flower
均不能创建实例,BigFlower
类实现了Flower
类的抽象类方法count
,因此BigFlower
可以创建实例。
from abc import ABCMeta, abstractmethod
class Plant(metaclass=ABCMeta):
# 定义抽象方法 grow
@abstractmethod
def grow(self):
print('Plant 生长吧!')
class Flower(Plant):
# 重写抽象方法 grow,以实现他
@abstractmethod
def grow(self):
super().grow()
# 定义抽象类方法 count
@classmethod
@abstractmethod
def count(self):
pass
class BigFlower(Flower):
# 重写抽象类方法 count,以实现他
@classmethod
def count(self):
print('一共有多少个哪?')
# 不能创建 Plant 和 Flower 的实例
BigFlower().grow()
Plant 生长吧!
将 Python 类注册为不需要实现抽象类的虚派生类
任何抽象基类或其派生类,都拥有一个名称为register
的方法,使用该方法可将一个不相关的类,注册为抽象类的虚派生类(Virtual Subclass)。与从抽象类直接派生的类不同,虚派生类可以被实例化,无论其是否已经实现了抽象类中的所有抽象方法。
register(subclass)
- subclass 参数
subclass
参数为需要注册为虚派生类的类。
此外,在 Python 3.3 或更高的版本中,register
方法可以作为类的修饰符来使用,以将类注册为某个抽象类的虚派生类。其语法格式为@abstractclass.register
,abstractclass
是一个抽象类。
抽象类不会出现在其虚派生类的方法解析顺序中
虽然虚派生类可被issubclass
函数判断为抽象类的派生类,虚派生类的实例可被isinstance
函数判断为抽象类的实例,但抽象类和其所有的抽象基类,不会出现在其虚派生类的方法解析顺序中,这意味着你无法在虚派生类中使用super
来访问抽象类中的方法或其他特性,因为,这些抽象类不会被搜索。
如何查看虚派生类的缓存令牌?
每当任意抽象类的register
方法被调用时,包括作为修饰符被调用的情况,虚派生类的缓存令牌都会发生改变,这一点可通过abc
模块的get_cache_token
函数进行验证(需要 Python 3.4 以上版本)。
下面,我们通过register
方法将Jerry
,Tom
注册为抽象基类Man
的虚派生类,并使用函数get_cache_token
查看缓存令牌的变化。
在注册为虚拟派生类后,Jerry
和Tom
可在实现或未实现抽象方法的情况下创建实例。不过,由于Man
不在Jerry
和Tom
的方法解析顺序中,因此,Tom
类的语句super().run()
将导致异常AttributeError
。
from abc import ABC, abstractmethod, get_cache_token
# 定义抽象基类 Man
class Man(ABC):
@abstractmethod
def run(self):
pass
# 将类 Jerry 和 Tom 注册为虚派生类,并查看令牌的变化
print(f'令牌为 {get_cache_token()}')
@Man.register
class Jerry():
pass
print(f'令牌为 {get_cache_token()}')
class Tom():
def run(self):
# ERROR Man 不在 MRO 中
super().run()
Man.register(Tom)
print(f'令牌为 {get_cache_token()}')
# Jerry 和 Tom 被认为是派生类
print(issubclass(Jerry, Man))
print(issubclass(Tom, Man))
# 可以正常创建实例
print(isinstance(Jerry(), Man))
print(isinstance(Tom(), Man))
Tom().run()
令牌为 24
令牌为 25
令牌为 26
True
True
True
True
…
AttributeError: 'super' object has no attribute 'run'
使用类方法 __subclasshook__ 决定抽象类的派生类
你可以为抽象类定义类方法__subclasshook__
,来决定某个类是否为抽象类的派生类。__subclasshook__
方法将影响issubclass
函数,其返回值为True
或False
时,issubclass
函数亦将返回True
或False
,其返回值为NotImplemented
时,issubclass
函数将根据自己原有的逻辑判断并返回结果。
一旦为某个抽象类定义了类方法__subclasshook__
,那么__subclasshook__
将适用于该抽象类的所有派生类,如果他没有被某个派生类重写的话。
__subclasshook__(subclass)
- subclass 参数
subclass
参数为需要判断是否为派生类的类。
我们为下面的Unit
类定义了类方法__subclasshook__
,并返回False
,这将使得所有类均不被认定为Unit
的派生类,即便他们真的从Unit
继承。
from abc import ABC
class Unit(ABC):
# 所有的类都不是 Unit 的派生类
@classmethod
def __subclasshook__(cls, subclass):
print(f'{subclass} 是 Unit 的派生类吗?')
return False
class Player(Unit):
pass
# 查看 Player 与 Unit 之间的关系
print(issubclass(Player, Unit))
print(isinstance(Player(), Unit))
<class '__main__.Player'> 是 Unit 的派生类吗?
False
False