4.2_抽象基类(abc模块 )
参考博客
一、前言 (废话,可以不看)
抽象基类就好比java
中的接口,在java
中它是无法实现多继承的,也就是只能继承一个类,但可以继承多个接口,且接口是不能用来实例化的。
在python
中抽象基类也是不能实例化的。python
是动态语言,定义变量时不用声明变量类型。变量只是一个符号,可以指向任何类型的对象。
我们可以复制任何一个类型的数据给python
中的任何一个变量,且可以修改。不用向java
那样去实现一个多态,因为python
本身就是一个支持多态的语言。
动态语言和静态语言相比,不需要指明变量类型,这会导致如果在python
中写错了代码,只有在运行是才会发现错误,即无法做类型检查。
我们在使用设计python类的时候,要把鸭子类型放在第一位。一个类有什么特性或者是什么类型,是看其内部实现了什么魔法函数,不同魔法函数赋予了类不同的特性。
鸭子类型和魔法函数构成了整个python语言的基础,也可以说是python里边的一种协议。可以说python本身不是通过继承某一个类或某一接口来实现某些特性,而是通过指定魔法函数的类,使其变成某种类型的对象。比如字符串表示: __repr__
、__str__
;迭代相关:__iter__
,__next__
;可调用:__call__
等等。在编程时要遵循这种协议。
二、抽象基类
- 在基础类中设定好一些方法,所有继承这个基类的类都必须覆盖这个抽象基类里的方法。
- 抽象基类无法用来实例化。
- abc模块作用:
Python本身不提供抽象类和接口机制,要想实现抽象类,可以借助abc模块。abc是Abstract Base Class的缩写。
Question
- 既然python是基于鸭子类型来设计的,那为何又有抽象基类的概念,直接实现某些方法不就可以了吗,干嘛非得继承?
Answer
抽象基类的两种应用场景
- 第一种应用是检查某一个类是否具有某种方法(判定某个对象的类型)
class Company(object):
def __init__(self,employee_list):
self.employee = employee_list
def __len__(self):
return len(self.employee)
com=Company(['ming','ming2'])
print(hasattr(com,'__len__')) # True
# hasattr内置函数 某一个对象是否具有某种属性,类中函数其实就是一个属性
print(len(com)) # 2
那么学过java
的应该知道,我们更倾向去判断Company
类实例化的com
对象是什么类型。而不是通过调用hasattr(com,'__len__')
这种方式来判断某一个对象。
基于上述内容就引出了抽象基类的第一种应用场景,判定某一类对象的类型,如果没有抽象基类的话,就必须使用hasattr()
这种方法。而使用抽象基类就变成如下方式:
from collections.abc import Sized
isinstance(com,Sized) # True
# isinstance 判断某一对象是否是指定的类型
也就是说可以通过继承Sized来拥有__len__
方法,此时instance(对象, Sized)
返回True
最后来看一下上面import的Sized抽象基类的源码
![](/qrcode.jpg)
#所有的抽象基类中的metaclass都必须是ABCMeta
class Sized(metaclass = ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls,C):
if cls is Sized:
return _check_methods(C,'__len__')
return NotImplemented
-
第二种应用是强制某个子类必须实现某些方法
比如自己实现了一个
web
框架(例如Django
),希望这个框架可以将cache
缓存集成进来,且希望未来可以用redis
或cache
或memorychache
来替换,或者说可以使用redis
或cache
或memorycache
来自定义组件,来替换掉现有的cache
,总之是无缝集成。所以我们就需要设计一个抽象基类,并指定子类必须实现某些方法。比如写了一个系统,在写框架的时侯,我们不知道以后使用该框架的人会用
redis
还是cache
等来替换,但我们希望用户在写了这些之后,不需要或是减少自己去调用redis
或cache
等里面代码,所以会事先约定一个抽象基类(主键)
class CacheBase():
def get(self,key):
pass
def set(self,key,value):
pass
CacheBase
抽象基类中定义了get
方法(从缓存里获取数据,拿到key
)和set
方法(添加value
),用户在继承这个抽象基类的时候也必须要再次把这两个方法实现(不实现调用会出错,所以才会强制用户实现这些方法)。
如果事先不定义好这些接口,用户在后期使用时,就会回到类中改写代码;如果事先定好了一个约定,用户只需要实现抽象类的一个子类,在配置文件中配好,主键是按照约定实现的话,替换起来非常方便。
写框架的时候经常要考虑,对系统的可扩展性很有帮助。
三、如何去模拟一个抽象基类?
代码如下
#假如有一个抽象基类CacheBase,用户在继承它时必须实现get,set方法
class CacheBase():
def get(self,key):
raise NotImplementedError
def set(self,key,value):
raise NotImplementedError
class RedisCache(CacheBase):
pass
redise_cache = RedisCache()
redis_cache.set('key','value') # NotImplementedError
# 调用set方法会报错
如果子类重新实现了set
方法,就会调用该子类的方法,不会抛异常。
class CacheBase():
def get(self,key):
raise NotImplementedError
def set(self,key,value):
raise NotImplementedError
class RedisCache(CacheBase):
def set(self,key,value):
pass
redise_cache = RedisCache()
redis_cache.set('key','value') # 无异常
上述模拟缺点是在调用set方法时,才会抛这个异常,那如果我们希望在初始化对象是就能抛出异常,怎么做?
需要用到abc
模块,它有两个,一个是全局下的abc
,一个是collections
里的abc
。
- 用
abc
模块实现抽象基类
import abc
class CacheBase():
@abc.abstractmethod #装饰器 抽象方法(设计方法)
def get(self,key):
pass
@abc.abstractmethod #装饰器 抽象方法(设计方法)
def set(self,key,value):
pass
class RedisCache(CacheBase): #没有重写方法
pass
redise_cache = RedisCache()
# redis_cache.set('key','value') # 无异常
上述代码因为子类RedisCache
未重载方法,故在初始化时抛出异常TypeError: Can't instantiate abstract class RedisCache with abstract methods get,set
class RedisCache(CacheBase):
def get(self,key):
pass
def set(self,key,value):
pass
redise_cache = RedisCache() #RedisCache子类实现get和set方法后,运行不抛异常了
- python中已经实现了一些通用的抽象基类,让我们可以了解数据结构的接口。在源码
_collections_abc.py
文件里面的起始位置可以看到很多定义好的抽象基类。
from abc import ABCMeta, abstractmethod
import sys
__all__ = ["Awaitable", "Coroutine",
"AsyncIterable", "AsyncIterator", "AsyncGenerator",
"Hashable", "Iterable", "Iterator", "Generator", "Reversible",
"Sized", "Container", "Callable", "Collection",
"Set", "MutableSet",
"Mapping", "MutableMapping",
"MappingView", "KeysView", "ItemsView", "ValuesView",
"Sequence", "MutableSequence",
"ByteString",
]
随便看一个,比如第一个Awaitable
class Awaitable(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __await__(self):
yield
@classmethod
def __subclasshook__(cls, C):
if cls is Awaitable:
return _check_methods(C, "__await__")
return NotImplemented
其中__subclasshook__
这个魔法函数很重要。
回想一个问题:对于一开始举的例子com
是Company
类实例化的一个对象,且Company
类并没有继承抽象基类Sized,那么为什么isinstance(com,Sized)
可以判断出com
是一个Sized
类型?
答案就是Sized
抽象基类中定义了__subclasshook__
这个魔法函数,源码如下(上边给了完整的,这里就简单给出部分)
@classmethod
def __subclasshook__(cls,C):
if cls is Sized:
return _check_methods(C,'__len__')
return NotImplemented
__subclasshook__
函数首先会调用_check_methods
这个函数来看一下传递进来的对象C有没有__len__
方法,有的话返回True
。也正Sized
抽象基类中的这一魔法函数让isinstance(com,Sized)
可以返回True
。
当然isinstance
作为python
的内置函数,功能肯定不止这么简单。不仅仅只是调用Sized
类中的__subclasshook__
函数,还会去做许多其他的尝试。比如去找一个继承链:
class A:
pass
class B(A):
pass
b = B()
print(isinstance(b,A)) # True
总结:在使用时尽量利用python
鸭子类型,非常灵活,如果一定要继承某些接口的话,推荐使用Mixin
(Python的多重继承)来实现,不推荐使用抽象基类(那我学这个干啥= =),因为抽象基类在设计时容易使用过度,不易理解。