首页 > 编程语言 >流畅的python--第十三章 接口、协议和抽象基类

流畅的python--第十三章 接口、协议和抽象基类

时间:2024-06-13 18:22:40浏览次数:12  
标签:__ 13 python self -- 抽象 基类 def

面向对象编程全靠接口。在 Python 中,支撑一个类型的
是它提供的方法,也就是接口。

在不同的编程语言中,接口的定义和使用方式不尽相同。从 Python 3.8
开始,有 4 种方式,如图 13-1 中的类型图所示。这 4 种方式概述如下。

  • 鸭子类型
    自 Python 诞生以来默认使用的类型实现方式。从第 1 章开始,本书一直在研究鸭子类型。
  • 大鹅类型
    自 Python 2.6 开始,由抽象基类支持的方式,该方式会在运行时检查对象是否符合抽象基类的要求。大鹅类型是本章的主要话题。
  • 静态类型
    CJava 等传统静态类型语言采用的方式。自 Python 3.5 开始,由
    typing 模块支持,由符合“PEP 484—Type Hints”要求的外部类型检查
    工具实施检查。本章不涉及该方式。第 8 章的大多数内容和第 15 章讨论了静态类型。
  • 静态鸭子类型
    Go 语言而流行的方式。由 typing.ProtocolPython 3.8 新增)的子类支持,由外部类型检查工具实施检查。静态鸭子类型首次出现在 8.5.10 节。

类型图

图 13-1 描述的 4 种类型实现方式各有优缺点,相辅相成,缺一不可。

图 13-1:上半部分是只使用 Python 解释器在运行时检查类型的方式;
下半部分则要借助外部静态类型检查工具,例如 MyPyPyCharm
IDE。左边两象限中的类型基于对象的结构(对象提供的方法),与
对象所属的类或超类无关;右边两象限中的类型要求对象有明确的类
型名称:对象所属类的名称,或者超类的名称。

这 4 种方式全都依靠接口,不过静态类型可以只使用具体类型实现(效
果差),而不使用协议和抽象基类等接口抽象。本章涵盖围绕接口实现
的 3 种类型:鸭子类型、大鹅类型和静态鸭子类型。

两种协议

在计算机科学中,根据上下文,“协议”一词有不同的含义。HTTP 这种
网络协议指明了客户端可向服务器发送的命令,例如 GETPUT
HEAD。12.4 节讲过,对象协议指明为了履行某个角色,对象必须实现哪
些方法。第 1 章中的 FrenchDeck 示例演示了一个对象协议,即序列协
议:一个 Python 对象想表现得像一个序列需要提供的方法。
完全实现一个协议可能需要多个方法,不过,通常可以只实现部分协
议。下面以示例 13-1 中的 Vowels 类为例。

示例13-1 使用__getitem__方法实现部分序列协议

只要实现 __getitem__ 方法,就可以按索引获取项,以及支持迭代和
in 运算符。其实,特殊方法 __getitem__ 是序列协议的核心。
如果对象提供序列协议,就返回 1,否则返回 0。注意,除了 dict
子类,如果一个 Python 类有 __getitem__() 方法,则也返回 1……
我们预期序列支持 len() 函数,也就是要实现 __len__ 方法。Vowels
没有 __len__ 方法,不过在某些上下文中依然算得上是序列。而有些
时候,这就足够了。所以,我经常说协议是“非正式接口”。第一个使
用“协议”这个术语的面向对象编程环境 Smalltalk 也是这么理解协议的。

  • 动态协议
    Python 一直有的非正式协议。动态协议是隐含的,按约定定义,在
    文档中描述。Python 大多数重要的动态协议由解释器支持,在《Python
    语言参考手册》的第 3 章“数据模型”中说明。
  • 静态协议
    “PEP 544—Protocols: Structural subtyping (static duck typing)”定义的
    协议,自 Python 3.8 开始支持。静态协议要使用 typing.Protocol 子 类显式定义。

二者之间的主要区别如下。

  • 对象可以只实现动态协议的一部分,但是如果想满足静态协议,则对象必须提供协议类中声明的每一个方法,即使程序用不到。
  • 静态协议可以使用静态类型检查工具确认,动态协议则不能。

两种协议共有一个基本特征:类无须通过名称(例如通过继承)声明支持什么协议。除了静态协议,Python 还提供了另一种定义显式接口的方式,即抽象基类。

利用鸭子类型编程

我们以 Python 中两个最重要的协议(序列协议和可迭代协议)为例展
开对动态协议的讨论。即使对象只实现了这些协议的最少一部分,也会
引起解释器的注意。

Python 喜欢序列

Python 数据模型的哲学是尽量支持基本的动态协议。对序列来说,即便
是最简单的实现,Python 也会力求做到最好。
图 13-2 展示的是通过一个抽象基类确立的 Sequence 接口。Python
释器和 liststr 等内置序列根本不依赖那个抽象基类。我只是利用
它说明一个功能完善的序列应该支持什么操作。

图 13-2:Sequence 抽象基类和 collections.abc 中相关抽象类的
UML 类图。箭头由子类指向超类。以斜体显示的是抽象方法。Python3.6 之前的版本中没有 Collection 抽象基类,Sequence
ContainerIterableSized 的直接子类

从图 13-2 可以看出,为了确保行为正确,Sequence 的子类必须实现
__getitem__ __len__(来自 Sized)。Sequence 中的其他方法都
是具体的,因此子类可以继承或者提供更好的实现。
再回顾一下示例 13-1 中的 Vowels 类。那个类没有继承
abc.Sequence,而且只实现了 __getitem__
虽然没有 __iter__ 方法,但是 Vowels 实例仍然可以迭代。这是因为
如果发现有 __getitem__ 方法,那么 Python 就会调用它,传入从 0
始的整数索引,尝试迭代对象(这是一种后备机制)。尽管缺少
__contains__ 方法,但是 Python 足够智能,能正确迭代 Vowels
例,因此也能使用 in 运算符:Python 做全面检查,判断指定的项是否
存在。

综上所述,鉴于序列类数据结构的重要性,如果没有 __iter__ 方法和
__contains__ 方法,则 Python 会调用__getitem__方法,设法让迭
代和 in 运算符可用。
第 1 章定义的 FrenchDeck 类也没有继承 abc.Sequence,但是实现了
序列协议的两个方法:__getitem__ __len__。如示例 13-2 所示。
示例 13-2 一摞有序的纸牌(与示例 1-1 相同)

import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                        for rank in self.ranks]
    def __len__(self):
        return len(self._cards)
    def __getitem__(self, position):
        return self._cards[position]

第 1 章中的那些示例之所以能用,是因为 Python 会特殊对待看起来像
序列的对象。Python 的迭代协议是鸭子类型的一种极端形式:为了迭代
对象,解释器会尝试调用两个不同的方法。
需要明确指出的是,本节描述的行为在解释器自身中实现,大多数是用
C 语言实现的,不依赖 Sequence 抽象基类的方法。例如,Sequence
类中的具体方法 __iter____contains__ 是对 Python 解释器内置
行为的模仿。如果觉得好奇,可以到 Lib/_collections_abc.py 文件中阅读
这些方法的源码。
下面再分析一个示例,强调协议的动态本性,并解释静态类型检查工具
为什么没机会处理动态协议。

使用猴子补丁在运行时实现协议

猴子补丁在运行时动态修改模块、类或函数,以增加功能或修正 bug
例如,网络库 gevent 对部分 Python 标准库打了猴子补丁,不借助线程
async/await 实现一种轻量级并发。
示例 13-2 中的 FrenchDeck 类缺少一个重要功能:无法洗牌。几年
前,我在第一次编写 FrenchDeck 示例时实现了 shuffle 方法。后
来,在对 Python 风格有了深刻理解后我发现,既然 FrenchDeck 的行
为像序列,那么它就不需要 shuffle 方法,因为有现成的
random.shuffle 函数可用。根据文档,该函数的作用是“就地打乱序
x”。
标准库中的 random.shuffle 函数用法如下所示。

标签:__,13,python,self,--,抽象,基类,def
From: https://www.cnblogs.com/bonne-chance/p/18246500

相关文章

  • CSharpe中的IO+NPOI+序列化
    CSharpe中的IO+NPOI+序列化文件文件夹操作学习一下常见的文件、文件夹的操作。什么是IO流?I:就是inputO:就是output,故称:输入输出流将数据读入内存或者内存输出的过程。常见的IO流操作,一般说的是[内存]与[磁盘]之间的输入输出。作用持久化数据,保证数据不再丢失!文件操作......
  • Conntrack 监控,别等故障了再回来加监控
    这是专栏第8篇,介绍一下node-exporter的conntrack插件。这个插件大家平时关注可能较少,但是在一些场景下,比如防火墙、NAT网关等,需要监控conntrack表的使用情况。我就遇到过一次生产事故,就是因为conntract表满了,导致新连接无法建立,所以这个插件还是很有用的。conntrack......
  • 代理离线服务器yum、curl命令访问网络
    在CentOS7上,你可以使用Squid作为代理服务器。以下是在CentOS7上设置代理的简要步骤:1.在可以上网的服务器上安装Squid:sudoyuminstallsquid2.配置Squid:打开Squid配置文件进行编辑:sudonano/etc/squid/squid.conf找到并编辑以下行(确保取消注释并设置合适的值):http_access......
  • 工业通讯协议(四)- OPCUA
    参考:https://www.opc-router.com/what-is-opc-ua/#:~:text=Inthiscontext%2CthemeaningofUAin,COM%2FDCOMtopurelybinaryTCP%2FIPoralternativelySOAP.https://github.com/OPCFoundation/UA-.NETStandard一.OPC介绍OPC(OLEforProcessControl)协议是一种用于......
  • 【模版】线段树
    https://www.luogu.com.cn/problem/P3372#include<cstring>#include<iostream>#include<algorithm>usingnamespacestd;#defineN100005#defineLLlonglong#definelcu<<1#definercu<<1|1LLw[N];structTree{//线段树LL......
  • MySQL的联合索引
    这里我先创建一张叫tb_user的表数据有创建一个联合索引  1.符合index(a,b,c)的查询条件 他的查询范围是ref使用了索引2.符合index(a,b)的查询条件  他的查询范围是ref使用了索引3.符合index(a)的查询条件   他的查询范围是ref使用了索引4.符合index(b,c......
  • kuberbetes-PVC与PV的创建 和绑定
    PVC与PV的创建如下yaml文件apiVersion:v1kind:PersistentVolume#PV是集群中的一块存储,可以由PVC请求并使用。-虚拟存储-实体机的存储、不是容器中的存储metadata:name:postgresql-pvnamespace:ops-systemspec:storageClassName:nfs#指定了与此PV关联......
  • 43、k8s-数据存储-高级存储-生命周期
    ·资源供应:管理员手动创建底层存储和PV·资源绑定:用户创建PVC,kubernetes负责根据PVC的声明去寻找PV,并绑定在用户定义好PVC之后,系统将根据PVC对存储资源的请求在已存在的PV中选择一个满足条件的。一旦找到,就将该PV与用户定义的PVC进行绑定,用户的应用就可以使用这个PVC了、如......
  • 42、k8s-数据存储-高级存储-pv和pvc、NFS服务器
    PV(PersistentVolume)是持久化卷的意思、是对底层的共享存储的一种抽象、一般情况下PV有kubernetes管理员进行创建和配置、他与底层具体的共享存储技术有关、并通过插件完成于共享存储的对接PVC(PersistentVolumeClaim)是持久卷声明的意思、是用户对于存储需求的一种声明、换句话......
  • 使用sql语句查询平均值,使用sql语句查询数据总条数, not in 筛选语句的使用
    1.查询平均值语法selectavg(要计算的值)as别名from表名select别名=avg(要计算的值)from表名2.获取数据总条数selectcount(*)as别名from表名select别名=count(*)from表名以下是举例:publicDictionary<string,string>keyValuePairs(){//as......