📜  Python中的抽象基类(abc)

📅  最后修改于: 2022-05-13 01:55:10.147000             🧑  作者: Mango

Python中的抽象基类(abc)

你有没有想过检查你使用的对象是否符合特定的规范?有必要验证一个对象是否实现了给定的方法或属性,尤其是在创建其他开发人员使用它的库时。开发人员可以使用 hasattr 或 isinstance 方法来检查输入是否符合特定身份。但有时使用这些方法来检查无数不同的属性和方法并不方便。

为了解决这个不便, Python引入了一个称为抽象基类 (abc)的概念。在本节中,我们将讨论抽象基类及其重要性。

  • 抽象基类
  • 声明一个抽象基类
  • 为什么要声明一个抽象基类?
  • 抽象属性
  • 内置抽象类

抽象基类

抽象基类的主要目标是提供一种标准化的方法来测试对象是否符合给定的规范。它还可以防止任何尝试实例化不覆盖超类中特定方法的子类。最后,使用抽象类,一个类可以从另一个类派生身份,而无需任何对象继承。

声明一个抽象基类

Python有一个名为 abc(抽象基类)的模块,它提供了制作抽象基类的必要工具。首先,您应该了解抽象基类提供的 ABCMeta 元类。规则是每个抽象类都必须使用 ABCMeta 元类。

ABCMeta 元类提供了一个称为 register 方法的方法,可以由它的实例调用。通过使用这个注册方法,任何抽象基类都可以成为任意具体类的祖先。让我们通过一个抽象基类的例子来理解这个过程,该基类将自己注册为 dict 的祖先。

Python3
import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    def abstractfunc(self):
        return None
  
  
print(AbstractClass.register(dict))


Python3
import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    def abstractfunc(self):
        return None
  
  
AbstractClass.register(dict)
print(issubclass(dict, AbstractClass))


Python3
import abc
  
  
class MySequence(metaclass=abc.ABCMeta):
    pass
  
MySequence.register(list)
MySequence.register(tuple)
  
a = [1, 2, 3]
b = ('x', 'y', 'z')
  
print('List instance:', isinstance(a, MySequence))
print('Tuple instance:', isinstance(b, MySequence))
print('Object instance:', isinstance(object(), MySequence))


Python3
import abc
  
  
class MySequence(metaclass=abc.ABCMeta):
    pass
  
class CustomListLikeObjCls(object):
    pass
  
MySequence.register(CustomListLikeObjCls)
print(issubclass(CustomListLikeObjCls, MySequence))


Python3
import abc
  
  
class MySequence(metaclass=abc.ABCMeta):
    pass
  
@MySequence.register
class CustomListLikeObjCls(object):
    pass
  
print(issubclass(CustomListLikeObjCls, MySequence))


Python3
import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, other):
        print('subclass hook:', other)
        hookmethod = getattr(other, 'hookmethod', None)
        return callable(hookmethod)
  
class SubClass(object):
    def hookmethod(self):
        pass
  
class NormalClass(object):
    hookmethod = 'hook'
  
  
print(issubclass(SubClass, AbstractClass))
print(issubclass(NormalClass, AbstractClass))


Python3
import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def abstractName(self):
        pass
  
class InvalidSubClass(AbstractClass):
    pass
  
isc = InvalidSubClass()


Python3
import abc
  
class AbstractClass(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def abstractName(self):
        pass
  
class ValidSubClass(AbstractClass):
    def abstractName(self):
        return 'Abstract 1'
  
vc = ValidSubClass()
print(vc.abstractName())


Python3
import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def abstractName(self):
        pass
  
  
class ValidSubClass(AbstractClass):
    @property
    def abstractName(self):
        return 'Abstract 1'
  
  
vc = ValidSubClass()
print(vc.abstractName)


Python3
from collections.abc import Sized
  
  
class SingleMethod(object):
    def __len__(self):
        return 10
  
  
print(issubclass(SingleMethod, Sized))


输出:

在这里,dict 将自己标识为 AbstractClass 的子类。让我们检查一下。

Python3

import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    def abstractfunc(self):
        return None
  
  
AbstractClass.register(dict)
print(issubclass(dict, AbstractClass))

输出:

True

为什么要声明一个抽象基类?

为了理解声明虚拟子类的必要性,我们需要考虑一个类似列表的对象的示例,您不想限制只考虑列表或元组。在此之前,让我们看看如何使用isinstance来检查类的列表或元组。

isinstance([], (list, tuple))

如果您仅接受列表或元组,则此isinstance检查符合目的。但这里的情况不同,没有这样的限制。因此,对于使用您的库发送列表或元组以外的其他内容的开发人员来说,此解决方案不可扩展。这就是抽象类的重要性。让我们通过下面的代码来理解。

Python3

import abc
  
  
class MySequence(metaclass=abc.ABCMeta):
    pass
  
MySequence.register(list)
MySequence.register(tuple)
  
a = [1, 2, 3]
b = ('x', 'y', 'z')
  
print('List instance:', isinstance(a, MySequence))
print('Tuple instance:', isinstance(b, MySequence))
print('Object instance:', isinstance(object(), MySequence))

输出:

List instance: True
Tuple instance: True
Object instance: False

如您所见,当您进行 isinstance 检查时,它对列表和元组都返回 true;对于其他对象,它返回 false。让我们考虑一个开发人员期望类对象本身的场景。在上述情况下,isinstance 将返回 false。但可以通过创建自定义类并将其注册到抽象基类来实现。

这里的“MySequence”是库中的一个抽象类。开发人员可以导入它并注册一个自定义类。让我们看看下面的代码。

Python3

import abc
  
  
class MySequence(metaclass=abc.ABCMeta):
    pass
  
class CustomListLikeObjCls(object):
    pass
  
MySequence.register(CustomListLikeObjCls)
print(issubclass(CustomListLikeObjCls, MySequence))

输出:

True

在这里,CustomListLikeObjCls 实例通过将其注册到 MySequence 来传递给库。因此,实例检查返回 True。除了上述方法外,还可以使用 register 方法作为装饰器来注册自定义类。让我们看看如何将register 方法用作装饰器

Python3

import abc
  
  
class MySequence(metaclass=abc.ABCMeta):
    pass
  
@MySequence.register
class CustomListLikeObjCls(object):
    pass
  
print(issubclass(CustomListLikeObjCls, MySequence))

输出:

True

使用上面实现的方法注册一个类就达到了目的。但是,您必须为每个预期的子类进行手动注册。基于特定方法的自动子类化怎么样?抽象类有一个称为 __subclasshook__ 的概念来对类进行子类化。

__子类挂钩___

它是 ABCMeta 定义的一种特殊的魔法方法。 __subclasshook__ 必须使用 @classmethod 装饰器定义为类方法。除了类之外,它还需要一个额外的位置参数,并且可以返回三个值中的任何一个——True、False 或 NotImplemented。让我们看看下面的实现。

Python3

import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, other):
        print('subclass hook:', other)
        hookmethod = getattr(other, 'hookmethod', None)
        return callable(hookmethod)
  
class SubClass(object):
    def hookmethod(self):
        pass
  
class NormalClass(object):
    hookmethod = 'hook'
  
  
print(issubclass(SubClass, AbstractClass))
print(issubclass(NormalClass, AbstractClass))

输出:

subclass hook: 
True
subclass hook: 
False

从上面的讨论中,您了解了如何自动挂钩子类。现在我们将研究如何避免实例化不覆盖超类中特定方法的子类。这个特性可以使用@abc.abstractmethod 来实现。

@abc.abstractmethod

@abc.abstractmethod 阻止任何尝试实例化不覆盖超类中特定方法的子类。让我们看一下下面的代码:

Python3

import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def abstractName(self):
        pass
  
class InvalidSubClass(AbstractClass):
    pass
  
isc = InvalidSubClass()

由于 InvalidSubclass 不会覆盖方法 abstractName,@abc.abstractmethod 会阻止子类实例化并引发以下错误。

让我们看看另一个子类覆盖抽象方法的例子。

Python3

import abc
  
class AbstractClass(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def abstractName(self):
        pass
  
class ValidSubClass(AbstractClass):
    def abstractName(self):
        return 'Abstract 1'
  
vc = ValidSubClass()
print(vc.abstractName())

输出:

Abstract 1

接下来,让我们看看如何将属性声明为抽象类。

抽象属性

我们可以使用@property 装饰器和@abc.abstractmethod 将属性声明为抽象类。让我们看看下面的代码。

Python3

import abc
  
  
class AbstractClass(metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def abstractName(self):
        pass
  
  
class ValidSubClass(AbstractClass):
    @property
    def abstractName(self):
        return 'Abstract 1'
  
  
vc = ValidSubClass()
print(vc.abstractName)

输出:

Abstract 1

内置抽象类

Python 3 标准库为抽象和非抽象方法提供了一些内置的抽象类。这些包括序列、可变序列、可迭代等。它通常用作子类化内置Python类的替代方法。例如,子类化 MutableSequence 可以替代 list 或 str 的子类化。使用 Abstract 类的主要目的是它允许您考虑一种常见的集合类型,而不是为每种类型的集合进行编码。在这里,我们将讨论单一方法 ABC 和替代收集 ABC。

  • 单一方法 ABC
  • 另类收集 ABC

单一方法 ABC

Python有五个抽象基类。它们如下:

  • 可调用 (__call__)
  • 容器 (__contains__)
  • 可散列(__hash__)
  • 可迭代 (__iter__)
  • 大小 (__len__)

这些抽象基类中的每一个都包含一个抽象方法。让我们考虑一个 __len__ 方法的例子。

Python3

from collections.abc import Sized
  
  
class SingleMethod(object):
    def __len__(self):
        return 10
  
  
print(issubclass(SingleMethod, Sized))

输出:

True

任何具有适当方法的类都被视为抽象基类的子类。在上述五个抽象基类中,Iterator 略有不同。它为 __iter__ 提供了一个实现,并添加了一个名为 __next__ 的抽象方法。

另类收集 ABC

Alternative-Collection ABC 是用于标识子类的内置抽象基类,用于类似目的。它们可以分为三类。让我们一一介绍。

  • 序列和可变序列:序列和可变序列是抽象基类,其行为通常类似于元组或列表。序列抽象基类需要 __getitem__ 和 __len__ ,而可变序列需要 __setitem__ 和 __getitem__。
  • 映射:映射自带可变映射,主要针对类字典对象
  • Set :该集合带有一个可变集合,用于无序集合。

概括

抽象类的主要目的是检查对象是否符合特定协议。它是用于测试类的某些属性或测试类本身的有价值的类。然而,还有很多其他的东西是抽象类没有检查的。其中一些是签名、返回类型等。另一个优点是它为开发人员提供了一种灵活的方式来测试常见的集合类型。