Python类和对象学习笔记——抽象基类

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抽象基类的源码

扫描二维码关注公众号,回复: 12376486 查看本文章
#所有的抽象基类中的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缓存集成进来,且希望未来可以用rediscachememorychache来替换,或者说可以使用rediscachememorycache来自定义组件,来替换掉现有的cache,总之是无缝集成。

    所以我们就需要设计一个抽象基类,并指定子类必须实现某些方法。比如写了一个系统,在写框架的时侯,我们不知道以后使用该框架的人会用redis还是cache等来替换,但我们希望用户在写了这些之后,不需要或是减少自己去调用rediscache等里面代码,所以会事先约定一个抽象基类(主键)

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__这个魔法函数很重要。

回想一个问题:对于一开始举的例子comCompany类实例化的一个对象,且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的多重继承)来实现,不推荐使用抽象基类(那我学这个干啥= =),因为抽象基类在设计时容易使用过度,不易理解。

猜你喜欢

转载自blog.csdn.net/weixin_43901214/article/details/106864388