URLhttps://learnscript.net/zh/python/classes/abstract-classes/
    复制链接转到说明  示例

    Python 抽象类,抽象方法介绍,以及定义和实现抽象类,抽象方法

    我被代码海扁署名-非商业-禁演绎
    阅读 14:34·字数 4373·更新 

    前提

    阅读本节的前提是对 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不在其中。

    abstract.py
    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类,语法形式如下,其中classnameblock的含义与之前相同。

    class <classname>(ABC):
        <block>

    abc 模块的 ABC 类

    abc模块的ABC类是一个辅助类(Helper Class),他将ABCMeta指定为元类,从ABC派生的类会成为抽象基类。你不能像使用ABCMeta类一样,通过metaclass关键字来使用ABC,那将导致一些问题。

    ABCMeta类不同,ABC会出现在抽象基类和其派生类的方法解析顺序中,ABC类是抽象基类的基类。

    下面,我们从ABC派生Accessible类,查看抽象基类Accessible的方法解析顺序和基类,发现ABC在其中。

    abstract.py
    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类定义了两个抽象方法countname,他们未被派生类Cat实现,但通过CatAnimal来调用却是可行的。

    abstract_methods.py
    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动态增加一个抽象方法showBlack类没有实现该方法,但创建其实例是可行的,因为ABCMeta并不知道新加入的抽象方法,直至通过update_abstractmethods函数更新相关的抽象类。

    abstract_update.py
    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,因此PlantFlower均不能创建实例,BigFlower类实现了Flower类的抽象类方法count,因此BigFlower可以创建实例。

    abstract_implement.py
    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.registerabstractclass是一个抽象类。

    抽象类不会出现在其虚派生类的方法解析顺序中

    虽然虚派生类可被issubclass函数判断为抽象类的派生类,虚派生类的实例可被isinstance函数判断为抽象类的实例,但抽象类和其所有的抽象基类,不会出现在其虚派生类的方法解析顺序中,这意味着你无法在虚派生类中使用super来访问抽象类中的方法或其他特性,因为,这些抽象类不会被搜索。

    如何查看虚派生类的缓存令牌?

    每当任意抽象类的register方法被调用时,包括作为修饰符被调用的情况,虚派生类的缓存令牌都会发生改变,这一点可通过abc模块的get_cache_token函数进行验证(需要 Python 3.4 以上版本)。

    下面,我们通过register方法将JerryTom注册为抽象基类Man的虚派生类,并使用函数get_cache_token查看缓存令牌的变化。

    在注册为虚拟派生类后,JerryTom可在实现或未实现抽象方法的情况下创建实例。不过,由于Man不在JerryTom的方法解析顺序中,因此,Tom类的语句super().run()将导致异常AttributeError

    abstract_register.py
    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函数,其返回值为TrueFalse时,issubclass函数亦将返回TrueFalse,其返回值为NotImplemented时,issubclass函数将根据自己原有的逻辑判断并返回结果。

    一旦为某个抽象类定义了类方法__subclasshook__,那么__subclasshook__将适用于该抽象类的所有派生类,如果他没有被某个派生类重写的话。

    __subclasshook__(subclass)

    subclass 参数

    subclass参数为需要判断是否为派生类的类。

    我们为下面的Unit类定义了类方法__subclasshook__,并返回False,这将使得所有类均不被认定为Unit的派生类,即便他们真的从Unit继承。

    abstract_subclasshook.py
    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

    源码

    src/zh/classes/abstract·codebeatme/python·GitHub