抽象基类

抽象基类

如何知道正在使用的对象是否符合一个给定的规范?在Python中回答该问题的常见答案被称作duck typing模式。如果它看起来像一只鸭子并且叫起来像一只鸭子,那么它大概就是一只鸭子。
在处理编程和对象时,问题通常可以转化为一个对象是否实现了给定方法,或包含一个特定的属性。如果一个对象有一个quack方法,你就有恰当的证据证明它是一只鸭子。此外,如果你只需要一个 quack方法,,实际上它是否是一只鸭子就没那么重要了。
这通常是一个非常有用的构造,并且它能轻而易举地在Python这种松散类型系统中实现。它强调构成问题而不是身份问题,强调hasattr函数而不是isinstance函数。
但是有时,身份很重要。例如,假定你正在使用要求输入遵循特殊身份的类库。或者,有时检查不同种类的属性和方法显得过于繁琐时。
抽象基类是一个分配身份的机制。它们是回答“从本质上讲,这是一只鸭子?”这个问题的一种方式。抽象基类也提供了一个标明抽象方法的机制,就是要求其他实现提供关键性功能,这些功能是在基类实现中不主动提供的功能。

一、使用抽象基类

抽象基类的基本目的就是提供有点形式化的方法,来测试一个对象是否符合特定规范。

如何确定你正在处理的对象是列表?这十分简单-----只需要调用isinstance将变量与列表类进行比较,然后查看函数是返回True还是False。

>>> isinstance([], list)
True
>>> isinstance(object(), list)
False

另一方面,你编写的代码真的需要一个列表吗?考虑这种情况,你只是去读取像列表一样的对象,但是绝不会修改该对象。在这种情况下,可以接收一个元组来代替列表。
isinstance方法的确提供了一个针对多个基类测试的机制,如下:

>>> isinstance([], (list, tuple))
True
>>> isinstance((), (list, tuple))
True
>>> isinstance(object(), (list, tuple))
False

但是,这也不是你真正 想要的,毕竟,一个自定义序列类也完全可以被接受,假如它使用__getitem__方法接受升序的整数和切片对象。因此,只是针对能够显示地被识别出来的类使用 isinstance可能会返回错误的False,从而是允许使用的对象不被允许使用。
当然,也可以测试__getitem__方法是否存在:

>>> hasattr([], '__getitem__')
True
>>> hasattr(object(), '__getitem__')
False

此外,这不是一个完整的解决方案。与isinstance检查不同,它不产生False结果。相反,它会产生True结果,因为不仅仅只有类似列表的对象实现了__getitem__方法。

>>> hasattr({}, '__getitem__')
True

从本质上讲,仅仅对某个属性或者方法是否存在进行测试有时不足以确定该对象是否符合你所寻找的参数。
抽象基类提供了声明一个类是另一个类的派生类的机制(无论它是否是另一个类的派生类)。该机制并没有影响实际的对象继承关系或是改变方法解析顺序。其目的是声明性的,它提供了一种断言对象符合协议的方式。
此外,抽象基类提供了一种要求子类实现指定协议的方式。如果一个抽象基类要求实现指定方法,并且子类没有实现这个方法,然后当试图创建子类时解释器会抛出一个异常。

二、声明虚拟子类

Python2.6、2.7和Python3的所有版本都提供了一个名为abc(表示抽象基类)的模块,该模块提供了一些使用抽象基类的工具。
abc模块提供的第一个内容是名ABCMeta的元类。任何抽象基类,无论它们的目的是什么,必须使用ABCMeta元类。
所有抽象基类可以任意地声明它是任意具体类的父类(不是派生类),包括在标准库的具体类(甚至哪些使用C语言实现的类)。ABCMeta的实例通过使用register方法提供了对声明的实现(记住,这些使用ABCMeta作为它们元类的类都是类 本身)。

考虑一个注册自身作为dict的父类的抽象基类:

import abc
class AbstractDict(metaclass = abc.ABCMeta):
	def foo(self):
		return None
	
>>> AbstractDict.register(dict)

这并没有对dict类本身进行任何修改。在此没有发生显著的变化,至关重要的是,dict的方法解析没有发生改变。你并不会突然发现dict拥有了foo方法。

>>> {}.foo()
Traceback (most recent call last):
  File "", line 1, in 
    {}.foo()
AttributeError: 'dict' object has no attribute 'foo'

这样做就使得dict对象也被标识为AbstractDict的实例,并且现在dict自身也被标识为一个AbstractDict的子类。

>>> isinstance({}, AbstractDict)
True
>>> issubclass(dict, AbstractDict)
True

注意,反过来执行却不是这样的结果。AbstractDict不是dict的子类。

>>> issubclass(AbstractDict, dict)
False

(1)声明虚拟子类的原因

为了理解声明虚拟子类的原因,会议本章开始时打算读取类似列表对象的实例。实例需要像list或tuple一样可被遍历,并且还需要一个__getitem__方法来接收整型参数。另一方面没有必要限制只接受list或tuple。
为此,抽象基类提供了一种非常好的可扩展机制。之前的示例表明,可以使用isinstance来检查一个类的元组。

>>> isinstance([], (list, tuple))
True

但是,这并不是真的可扩展。如果在你的实现中检查list或tuple,并且使用你的类库的人打算发送一些其他的类似列表的对象,而对象并不是list或tuple的子类,此时就遇到了难以实现扩展的问题。
抽象基类提供了解决这个问题的方案。首先,定义一个抽象基类并且对它注册list和tuple,如下所示:

import abc
class MySequence(metaclass = abc.ABCMeta):
	pass

>>> MySequence.register(list)

>>> MySequence.register(tuple)

现在修改 isinstance,检测MySequence而不再检测(list, tuple)。当针对list或tuple进行检测时仍将返回True,而针对其他对象的检测将会返回False。

>>> isinstance([], MySequence)
True
>>> isinstance((), MySequence)
True
>>> isinstance(object(), MySequence)
False

到目前为止,与之前的情况一样。但有一个关键的区别。考虑这样一种情况,一个开发人员正使用类库并期望一个MySequence对象,并且因此,也期望一个list或tuple。
当(list, tuple)在类库中是硬编码时,开发人员什么也做不了。但是,MySequence是一个在类库中定义的抽象基类。这意味着开发人员能导入它。
一旦开发人员导入该类库,这个完全类似列表的自定义类就能简单地被注册在MySequence中:

class CustomListLikeClass(object):
	pass

>>> MySequence.register(CustomListLikeClass)

>>> issubclass(CustomListLikeClass, MySequence)
True

开发人员可以将CustomListLikeClass的实例传递到期望MySequence的类库。现在,当类库执行isinstance检测时,检测会通过,,因此准许接收该对象。

(2)使用register作为装饰器

自Python3.3以来,使用ABCMeta元类的类所提供的register方法也可以用作装饰器。
如果你创建一个应该被注册为ABCMeta子类的新类,那么一般情况下可以像下面一样注册它:

class CustomListLikeClass(object):
	pass

>>> MySequence.register(CustomListLikeClass)

但是,注意,register方法会返回传递给它的类。这样的工作机制使得register也能被用作装饰器。register接受一个可调用函数,同时返回一个可调用函数(这个示例中,是完全相同的可调用函数)。
下面的代码作用相同:

 @MySequence.register
 class CustomListLikeClass(object):
	pass

>>> issubclass(CustomListLikeClass, MySequence)
True

(3)__subclasshook__方法

对于大多数目的,使用一个使用ABCMeta元类的类,然后使用由ABCMeta提供的register方法就完全足以获得你所需要的结果。但是,为所有希望的子类手动注册这种情况并不合理。
用ABCMeta元类创建的类可以有选择性地定义一个特殊的魔术方法,称为__subclasshook__。
__subclasshook__方法必须被定义为一个类方法(使用@classmethod装饰器定义)并且接受一个额外的位置参数,该参数是被测试的类。它可以返回三个值:True、False或NotImplemented。
返回值是True和False的情况是很清晰的。如果被测试类被认为是子类,那么__subclasshook__方法的返回结果是True;而如果被认为不是子类,那么返回值是False。

考虑传统的“鸭子类型”范例。在“鸭子类型”范例中最根本的问题是,一个对象是否有某个方法或者属性(比如是否它叫起来像只鸭子),而不是对象是否是这个或那个
类的子类。抽象基类可以用__subclasshook__实现这个概念,如下所示:

import abc
class AbstractDuck(metaclass = abc.ABCMeta):
	@classmethod
	def __subclasshook__(cls, other):
		quack = getattr(other, 'quack', None)
		return callable(quack)

该抽象基类声明,任何带有quack方法(但是该方法不是一个不可调用的quack属性)的类都被认为时它的子类,而任何其他类都不是它的子类。

class Duck(object):
	def quack(self):
		pass

	
class NotDuck(object):
	quack = 'foo'
	
>>> issubclass(Duck, AbstractDuck)
True
>>> issubclass(NotDuck, AbstractDuck)
False

这里需要注意的一件重要事情是,当 __subclasshook__方法被定义时,它优先于register方法。

>>> AbstractDuck.register(NotDuck)

>>> issubclass(NotDuck, AbstractDuck)
False

在此需要用到NotImplemented。如果__subclasshook__方法返回NotImplemented,然后(并且只有然后)传统检测的路径就会查看已注册的类是否已被选中。
考虑下面这个修改过的AbstractDuck类:

import abc
class AbstractDuck(metaclass = abc.ABCMeta):
	@classmethod
	def __subclasshook__(cls, other):
		quack = getattr(other, 'quack', None)
		if callable(quack):
			return True
		return NotImplemented

这里仅有的变化就是quack方法好像已经不存在,__subclasshook__方法返回值是NotImplemented而不是False。现在,注册表已经被检查,并且之前注册的类将作为子类返回。

>>> issubclass(NotDuck, AbstractDuck)
False
>>> AbstractDuck.register(NotDuck)

>>> issubclass(NotDuck, AbstractDuck)
True

本质上讲,第一个例子说的是,“如果它叫起来像只鸭子,那么它就是AbstractDuck”,然后第二个例子是说,“如果它叫起来像只鸭子或者直接说它是AbstractDuck,那么它就是AbstractDuck”。
当然,注意如果这样做,就必须能够处理接收的任何东西。如果依赖于调用quack方法,那么使得该方法可选择对你没有任何好处。
那么,这样做的价值何在?这样做可以仅简单地针对你所需的方法做hasattr或callable检查。
对于复杂的情况,使用抽象基类就有一些价值了。首先,在区分上有价值。抽象基类为整个测试定义了一个存在的统一位置。使用抽象基类子类的任何代码仅仅是使用 issubclass或isinstance方法。随着需求的变化,仅在一个地方存放一致性检查的代码。
另外,NotImplemented作为__subclasshook__的可用返回值增加了一些功能。它提供了一种确保绝对匹配或不匹配给定协议的机制,这也是对于自定义类作者显示可选的方式。

三、声明协议

抽象基类的另一个主要价值在于它有声明协议的功能。在之前的示例中,已经介绍了抽象基类是如何使一个类能够声明它自身可以通过类型检查测试的。
但是,抽象基类也可以定义子类必须提供的内容。这类似于在其他诸如Java等面向对象的语言中接口的概念。

(1)其他现有的方法

即使不使用抽象基类也能解决这种基本问题。因为抽象基类是一个相对较新的语言特性,下面这几种方法十分常见。

1.使用NotImplementedError

from datetime import datetime
class Task:
	'''An abstract class representing a task that must run,and which should track individual runs and results.'''
	def __init__(self):
		self.runs = []
	def run(self):
		start = datetime.now()
		result = self._run()
		end = datetime.now()
		self.runs.append({'start': start, 'end': end, 'result': result,})
		return result
	def _run(self):
		raise NotImplementedError('Task subclasses must define a _run method.')

这个类的目的是运行某种类型的任务并追踪何时执行这些任务。但是,基类Task不能提供任务主体。这需要由子类完成。相反,Task类提供一个shell方法_run,该方法仅抛出带有有用错误信息的 NotImplementedError。任何未能重写_run的子类大都会引发这个错误,如果打算调用Task类本身的run方法,也会得到这个错误。

>>> t = Task()
>>> t.run()
Traceback (most recent call last):
  File "", line 1, in 
    t.run()
  File "", line 7, in run
    result = self._run()
  File "", line 12, in _run
    raise NotImplementedError('Task subclasses must define a _run method.')
NotImplementedError: Task subclasses must define a _run method.

2.使用元类

NotImplementedError不是声明协议的唯一方式。另一种声明协议的常用方式是使用元类。

from datetime import datetime, timezone
class TaskMeta(type):
	'''A Metaclass that ensures the presence of a _run method on any non-abstract classes it creates.'''
	def __new__(cls, name, bases, attrs):
		#If this is an abstract class, do not check for a _run method.
		if attrs.pop('abstract', False):
			return super(TaskMeta, cls).__new__(cls, name, bases, attrs)
		#create the resulting class
		new_class = super(TaskMeta, cls).__new__(cls, name, bases, attrs)
		#verify that a _run method is present and raise
		#TypeError otherwise.
		if not hasattr(new_class, '_run') or not callable(new_class.run):
			raise TypeError('Task subclass must define a _run method.')
		return new_class
		
class Task(metaclass = TaskMeta):
	'''An abstract class representing a task that must run,and which should track individual runs and results.'''
	abstract = True
	def __init__(self):
		self.runs = []
	def run(self):
		start = datetime.now(tz = timezone.utc)
		result = self._run()
		end = datetime.now(tz = timezone.utc)
		self.runs.append({'start': start, 'end': end, 'result': result,})
		return result

这个示例与前面的示例有一些区别。第一个区别就是Task类本身,虽然它任然被实例化,但是不再声明_run 方法,因此面向公用的run 方法会抛出AttributeError错误。

>>> t = Task()
>>> t.run()
Traceback (most recent call last):
  File "", line 1, in 
    t.run()
  File "", line 8, in run
    result = self._run()
AttributeError: 'Task' object has no attribute '_run'

但是,更为重要的区别在于子类。因为当子类被创建时元类会运行__new__方法,解析器将不再允许创建没有_run 方法的子类。

>>> class TaskSubclass(Task):
	pass

Traceback (most recent call last):
  File "", line 1, in 
    class TaskSubclass(Task):
  File "", line 12, in __new__
    raise TypeError('Task subclass must define a _run method.')
TypeError: Task subclass must define a _run method.

(2)抽象基类的价值

抽象基类提供了一种呈现相同模式的更正式的方法。它们提供了一个使用某个抽象基类声明协议的机制,并且子类一定要提供一个符合该协议的实现。
abc模块提供了一个名为@abstractmethod 的装饰器,它指定了一个必须被所有子类重写的特定方法。该方法体可以是空的(pass),或者可能包含一个子类方法可能选择使用super调用的实现。
考虑一个使用@abstractmethod 装饰器代替自定义元类的Task类。

import abc
from datetime import datetime, timezone
class Task(metaclass = abc.ABCMeta):
	'''An abstract class representing a task that must run,and which should track individual runs and results.'''

	def __init__(self):
		self.runs = []
	def run(self):
		start = datetime.now(tz = timezone.utc)
		result = self._run()
		end = datetime.now(tz = timezone.utc)
		self.runs.append({'start': start, 'end': end, 'result': result,})
		return result
	@abc.abstractmethod
	def _run(self):
		pass

前两个例子几乎相同,但略有不同。首先,注意Task类本身不能被实例化。

>>> t = Task()
Traceback (most recent call last):
  File "", line 1, in 
    t = Task()
TypeError: Can't instantiate abstract class Task with abstract methods _run

对于不能正确重写_run 方法的子类,在错误的情况下它与之前的两个方法的差别也是细微的。在第一个示例中,使用NotImplementedError,最终在_run 方法被调用的地方引发NotImplementedError错误。在第二个示例中,使用自定义的TaskMeta元类,当这个抽象类被创建时引发TypeError 错误。
当使用抽象基类时,解析器乐于去创建一个不实现基类中所有(或者任何一个)抽象方法的子类;

>>> class Subtask(Task):
	pass

但是,解析器不愿做的就是实例化这个类。实际上,这样做给出的错误信息与Task类给出的完全一样,逻辑上是与你期望的完全一致:

>>> st = Subtask()
Traceback (most recent call last):
  File "", line 1, in 
    st = Subtask()
TypeError: Can't instantiate abstract class Subtask with abstract methods _run

但是,一旦你定义一个重写抽象方法的子类,那么它就能正常工作,并且也能够实例化子类:

class OtherSubtask(Task):
	def _run(self):
		return 2 + 2

	
>>> ost = OtherSubtask()
>>> ost.run()
4

而且,如果检查runs 属性,会发现关于run的信息已经被保存了,如下所示:

>>> ost.runs
[{'start': datetime.datetime(2018, 10, 12, 3, 3, 0, 779934, tzinfo=datetime.timezone.utc), 'end': datetime.datetime(2018, 10, 12, 3, 3, 0, 779934, tzinfo=datetime.timezone.utc), 'result': 4}]

实际上,基于以下几个原因,这是解决这个问题的一个非常有用的方法。首先(并且可能是最重要的),这个方法是正式而不是临时的。抽象基类被明确地提出作为满足这种特定需求的解决方案,依据这一概念,理想情况下应该有且仅有一种实现这种需求的“正确”方法。
其次,@abstractmethod装饰器非常简单,并且可以在打算编写应该应该模板代码时避免出现大量潜在的错误。举个例子,如果在TaskMeta元类中意外地仅检查attrs字典中存在_run,而不允许子类中存在_run会如何呢?很容易犯这样的错误,并且会导致Task子类不能成为自身的子类,除非每次都手动重写_run方法。使用@abstractmethod装饰器,你将在不用过多考虑的情况下获得正确的行为。
最后,这个方法使得引入中间实现变得很容易。考虑一个有10 个抽象方法而不是1个抽象方法的抽象基类。理所当然会有一个完整的子类树,在树中链上的高级别的子类实现一些常用方法,但是将在抽象状态中的方法留给其他子类去实现。公正地讲,也能够使用自定义元类方法实现该功能(在TaskMeta示例中,通过在每一个中间类中声明abstract=True)。但是,当使用@abstractmethod装饰器时,基本上能够得到你直观上希望的行为。

(3)抽象属性

也可以将属性声明为抽象属性(也就是使用@property装饰器的方法)。但是实现此目的的正确方法有点取决于所支持的Python版本。
在Python2.6到3.2中(包含跨版本兼容的代码),正确的方法是使用@abstractproperty装饰器,这个装饰器由abc模块提供。

import abc
class AbstractClass(metaclass =  abc.ABCMeta):
	@abstractproperty
	def foo(self):
		pass

在Python3.3及以上版本:

import abc
class AbstractClass(metaclass =  abc.ABCMeta):
	@property
	@abc.abstractmethod
	def foo(self):
		pass

试图实例化AbstractClass的子类且不重写foo方法将会导致引发一个错误:

 class InvalidChild(AbstractClass):
	pass

>>> ic = InvalidChild()
Traceback (most recent call last):
  File "", line 1, in 
    ic = InvalidChild()
TypeError: Can't instantiate abstract class InvalidChild with abstract methods foo

但是,重写抽象方法的子类能够被实例化:

class ValidChild(AbstractClass):
	@property
	def foo(self):
		return 'bar'

	
>>> vc = ValidChild()
>>> vc.foo
'bar'

(4)抽象类或静态方法

与属性一样,你可能希望将@abstractmethod装饰器与一个类方法或静态方法组合使用(也就是说,用@classmethod或@staticmethod装饰器装饰的方法)。
这是一个小技巧。在Python2.6到3.1版本中根本没有提供一种方式来实现这一点。Python3.2中的确提供了一种实现方式,也就是使用@abstractclassmethod或者@abstractstaticmethod装饰器来实现。这些功能与之前的抽象属性的示例很相似。
在Python3.3以后,通过修改@abstractmethod从而兼容@classmethod和@staticmethod装饰器改变了这一点,并且废弃了Python3.2 版本中的方法。
考虑下面这个使用Python3.3语法的抽象类:

class AbstractClass(metaclass = abc.ABCMeta):
	@classmethod
	@abc.abstractmethod
	def foo(self):
		return 42

继承该类的子类在未重写该方法的情况下照样能够正常工作,但是子类不能被实例化:

class InvalidChild(AbstractClass):
	pass

>>> ic = InvalidChild()
Traceback (most recent call last):
  File "", line 1, in 
    ic = InvalidChild()
TypeError: Can't instantiate abstract class InvalidChild with abstract methods foo

尽管如此,实际上抽象方法本身能够被直接调用,且不会引发任何错误:

>>> InvalidChild.foo()
42

一旦抽象方法在子类中被重写,那么就能够实例化该子类:

class ValidChild(AbstractClass):
	@classmethod
	def foo(cls):
		return 'bar'

	
>>> ValidChild.foo()
'bar'
>>> vc = validChild()
>>> vc.foo()
'bar'

四、内置抽象基类

除了通过abc模块建立自己的抽象基类之外,Python3标准类库也为语言提供了少量的内置抽象基类,尤其是选择使用一些特殊类用于实现常用模式(例如序列、可变序列、迭代器等)。最常用的抽象基类是用于集合的抽象基类,它们存在于collections.abc模块中。
绝大多数内置的抽象基类都提供了抽象和非抽象的方法,并且通常是继承Python内置类的替代方法。例如,继承于MutableSequence相对于继承list或str可能是更好的选择。
抽象基类能被划分为两种基本类别:一种需要和检查单一方法(比如Iterable和Callable),另一种作为普通内置Python类型的替身。

(1)只包含一个方法的抽象基类

Python提供了5个抽象基类,每个基类包含一个抽象方法,并且抽象基类的__subclasscheck__方法仅仅检查该方法是否存在。这些抽象基类如下所示:

  • Callable(__call__)
  • Container(__contains__)
  • Hashable(__hash__)
  • Iterable(__iter__)
  • Sized(__len__)

任何包含相应方法的类都会自动地被当作相关抽象基类的子类。

from collections.abc import Sized
class Foo(object):
	def __len__(self):
		return 42

	
>>> issubclass(Foo, Sized)
True

与之相似,类可以直接作为抽象基类的子类,并期望重写相关方法:

class Bar(Sized):
	pass

>>> b = Bar()
Traceback (most recent call last):
  File "", line 1, in 
    b = Bar()
TypeError: Can't instantiate abstract class Bar with abstract methods __len__

除了这5个抽象基类,还有另外一个抽象基类Iterator。Iterator有点特殊,它继承自Iterable,提供__iter__的实现(就是返回自身并可以被重写),并且添加抽象方法__next__。

你可能感兴趣的:(Python)