Python抽象基类
Python这门语言中,由于存在动态声明类属性的存在,我们很难说xxx是xxx,比如如何确定你正在处理的对象是一个列表?针对上面问题,我们可以使用isinstance(变量,list)的方式,如果得到True那么意味着变量是一个列表。但有时候我们并不是真的想得操作一个list,或者说如果我只想确认,我操作的变量是否能够用[int]的方式进行遍历,这种类似的列表的操作,那我们该怎么办呢?我们当然可以使用hasattr(变量,“__getitem__”)来确认该对象是否具有能使用下标遍历的功能。但我们总不能每个功能都去确认吧,换句话说仅仅对某个属性或者某个方法是否存在进行测试并不能确定该对象是否符合你正在寻找的对象。
抽象基类则提供了声明一个类是另一个类的派生类的机制(无论这两个类之间是否真的有继承关系)。这种机制并不影响对象的继承关系,也不会改变解析顺序,这只是一种声明,用于标志"我认为A类是B类的子类",但实际上A类和B类之间并不存在继承。
1、声明虚拟子类
上面提到了,我可以创造一个类(下文称抽象基类),这个类可以是所有类的父类,尽管这个抽象基类与其他类没有任何关系。
import abc
class AbstractClass(metaclass=abc.ABCMeta):
def foo(self):
pass
class A(object):
pass
AbstractClass.register(A)
print(issubclass(A, AbstractClass))
A().foo()
我们可以通过abc模块的abc.ABCMeta这个元类创造一个抽象基类(只要是由这个元类生成的类对象就可以作为抽象基类来使用)。我们主要到我们在AbstractClass中还定义了一个foo()方法。我们又创造了一个与AbstractClass类毫不相关的A类。需要注意第10行,我们使用 抽象基类.register(A) 的方式,将A类"注册"成了AbstractClass的一个子类,接下来的issubclass和isinstance都会"认为"A是AbstractClass的子类。但是从功能上,A类与AbstractClass没有任何关系,所以第12行,我们企图执行A.foo(),会得到报错消息。上述代码结果如下:
需要提一句的是,我们除了使用 抽象基类.register(A)的方式“注册”A类,还可以使用 @ 抽象基类.register 这种类装饰器的方式进行“注册”,代码7-8行可以改为:
@AbstractClass.register
class A(object):
pass
2、__subclasshook__方法
前面提到采用.register()的是方法是一种手动注册,需要我们自己确认这个类就是我们要找的子类。但实际上我们可以在抽象基类中重写__subclasshook__方法(这个方法需要声明称类方法,即@
class AbstractClass(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, otherClass: type) -> bool:
quack = getattr(otherClass, 'quack', None) ## 如果没有获得这个 类属性/方法 则返回None
if callable(quack):
return True ## 认为是otherClass是AbstractClass的子类/子类实例
else:
return False
class D(object):
def quack(self):
pass
class E(object):
quack = "类属性"
@AbstractClass.register
class F(object):
quack = "类属性"
print("D类是否是AbstractClass:", issubclass(D, AbstractClass))
print("E类是否是AbstractClass:", issubclass(E, AbstractClass))
print("F类是否是AbstractClass:", issubclass(F, AbstractClass))
我们在__subclasshook__方法中判断,类是否具有quack这个方法(注意是可执行的方法,而不是quack属性),我们定义了D,E,F三个类,其中D类具有quack方法,E类具有quack属性,F类也具有quack属性但与E类不同的是F类通过.register方法进行了注册。我们看执行结果如下:
可以看到E,F类都不被认为是抽象基类的子类。我们需要讨论的是F类不是被注册了么?为什么没有用呢?因为__subclasshook__方法会优先于.register,如果__subclasshook__()中已经得到了True或者False的答案,那么将不会再去查询注册表(也就是通过.register()方法注册的类)。但是如果__subclasshook__()方法返回了NotImplemented,才会去查询注册表中是否有答案,比如我们执行下面代码:
class AbstractClass(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, otherClass: type) -> bool:
quack = getattr(otherClass, 'quack', None)
if callable(quack):
return True
return NotImplemented ## 如果返回NotImplemented将检查注册表(也就是被放入AbstractClass.register())
class D(object):
def quack(self):
pass
class E(object):
quack = "类属性"
@AbstractClass.register
class F(object):
quack = "类属性"
print("D类是否是AbstractClass:", issubclass(D, AbstractClass))
print("E类是否是AbstractClass:", issubclass(E, AbstractClass))
print("F类是否是AbstractClass:", issubclass(F, AbstractClass))
最后执行结果会变成:
3、接口声明
抽象基类的另一个作用是在于它的声明功能,抽象基类可以规定子类必须提供的内容(必须实现的功能),这对应于Java语音中接口的概念。比如我们,自定义了一个类A,有一个方法需要子类去实现(必须实现),那么如果有子类继承自这个类A但是没有实现对应的功能,那么就会抛出异常,或者终止执行。针对这种情况,Python中最常见的就是使用NotImplementedError异常,比如下面这个例子:
import time
## 传统的"接口定义"
class Task(object):
def __init__(self):
self.runs = []
def run(self, *args):
time_start = time.time()
result = self._run(*args)
time_end = time.time()
self.runs.append({
"start": time_start,
"end": time_end,
"recult": result
})
return result
def _run(self):
raise NotImplementedError("Task的子类必须重写_run()方法")
class A(Task):
pass
class B(Task):
def _run(self, num):
count = 0
for i in range(num):
count += i
return count
我们定义了一个父类Task,这个父类中有两个方法,分别是_run和run。run方法中显式的调用了self._run()方法,并对_run()的运行时间进行了记录(运行时间放入到runs这个列表中)。但需要注意的是Task的_run方法中并没有任何内容,而是直接抛出了NotImplementedError,这就意味着如果继承Task的子类没有重写_run()方法且还企图执行self.run(),那么只要子类执行self.run()就会报错了。我们这里给出了两个类继承自Task,分别是A类和B类,其中B类重写了_run方法(计算求和),我们执行以下代码:
task = Task() # 可以实例化
a = A() # 可以实例化
b = B()
print(b.run(10))
print(b.runs) # 可以访问父类属性
a.run() # 抛出错误
可以知道上述代码的前7行都是可以正常执行的,但第9行会报出错误,因为A类中并没有重写_run方法。
以上就是常见的Python要求子类重写方法时采用的手段,即在父类中抛出NotImplementedError。学习过Java的小伙伴可能会知道,Java中接口是不能被实例化的,因为一个接口本身就是为了被继承才设置的,这显然与上面的执行不同(Task可以被实例化)。这里不讨论哪个语言中的操作更合适,但是很直观的想法是,既然A类没有实现接口的规定,就不应该能创建实例,毕竟我们只有运行到最后一行,我们才知道“原来A类有问题”,有没有什么手段能帮我们自动检查,我们的类是否合规呢?
这种情况下就体现了抽象基类的价值。抽象基类提供了一个@abstractmethod装饰器(准确的说是abc包下的),这个装饰器如果装饰了抽象基类中的某个方法,那么这个方法就会被识别为抽象方法(子类必须显式重写的方法,如果子类没有重写,并且企图实例化那么会触发TypeError),我们把上述例子用抽象基类的方法进行重写。
import abc
import time
## 使用抽象基类的方式完成接口申明
class Task(metaclass=abc.ABCMeta):
def __init__(self):
self.runs = []
def run(self, *args):
time_start = time.time()
result = self._run(*args)
time_end = time.time()
self.runs.append({
"start": time_start,
"end": time_end,
"recult": result
})
return result
@abc.abstractclassmethod
def _run(self):
pass
class A(Task):
pass
class B(Task):
def _run(self, num):
count = 0
for i in range(num):
count += i
return count
代码逻辑与之前的没有差别,只不过将Task声明成一个抽象基类(由abc.ABCMate元类创建的类),并且对_run()方法使用了@abc.abstractclassmethod进行了装饰。接下来,我们再试试看创建实例:
# task = Task() # 会报错: 不可以实例化,因为包含抽象方法
# a = A() # 会报错: 没有实现相关方法,因此会报错
b = B()
print(b.run(10))
print(b.runs) # 可以访问父类属性
第1行和第2行代码,由大家自己去测试。这样的方式就与其他语言中接口的实现就非常相似了。
参考网站:
你真的了解__instancecheck__、__subclasscheck__、__subclasshook__三者的用法吗 - 古明地盆 - 博客园 (cnblogs.com)