资讯专栏INFORMATION COLUMN

流畅的python读书笔记-第11章-接口:从协议到抽象基类

Kyxy / 2070人阅读

摘要:自己定义的抽象基类要继承。抽象基类可以包含具体方法。这里想表达的观点是我们可以偷懒,直接从抽象基类中继承不是那么理想的具体方法。

抽象基类 抽象基类的常见用途:

实现接口时作为超类使用。

然后,说明抽象基类如何检查具体子类是否符合接口定义,以及如何使用注册机制声明一个类实现了某个接口,而不进行子类化操作。

如何让抽象基类自动“识别”任何符合接口的类——不进行子类化或注册。

接口在动态类型语言中是怎么运作的呢?

按照定义,受保护的属性和私有属性不在接口中:

即便“受保护的”属性也只是采用命名约定实现的(单个前导下划线)

私有属性可以轻松地访问(参见 9.7 节),原因也是如此。 不要违背这些约定。

不要觉得把公开数据属性放入对象的接口中不妥,

因为如果需要,总能实现读值方法和设值方法,把数据属性变成特性,使用 obj.attr 句法的客户代码不会受到影响。

Python喜欢序列

协议是接口,但不是正式的(只由文档和约定定义),

因此协议不能像正式接口那样施加限制(本章后面会说明抽象基类对接口一致性的强制)。

一个类可能只实现部分接口,这是允许的。

看看示例 11-3 中的 Foo 类。它没有继承 abc.Sequence,而且只实现了序列协议
的一个方法: getitem (没有实现 len 方法)

定义 getitem 方法,只实现序列协议的一部分,这样足够访问元
素、迭代和使用 in 运算符了
>>> class Foo:
... def __getitem__(self, pos):
... return range(0, 30, 10)[pos]
...
>>> f = Foo()
>>> f[1]
10
>>> for i in f: print(i)
...
0
10
20
>>> 20 in f
True
>>> 15 in f
False

综上,鉴于序列协议的重要性,如果没有 itercontains 方法,Python 会调
getitem 方法,设法让迭代和 in 运算符可用。

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

random.shuffle 函数打乱 FrenchDeck 实例

为FrenchDeck 打猴子补丁,把它变成可变的,让 random.shuffle 函
数能处理

def set_card(deck, position, card): ➊
... deck._cards[position] = card
>>> FrenchDeck.__setitem__ = set_card ➋
>>> shuffle(deck) ➌
>>> deck[:5]
[Card(rank="3", suit="hearts"), Card(rank="4", suit="diamonds"), Card(rank="4",
suit="clubs"), Card(rank="7", suit="hearts"), Card(rank="9", suit="spades")]

❶ 定义一个函数,它的参数为 deck、position 和 card。
❷ 把那个函数赋值给 FrenchDeck 类的 setitem 属性。
❸ 现在可以打乱 deck 了,因为 FrenchDeck 实现了可变序列协议所需的方法。

这里的关键是,set_card 函数要知道 deck 对象有一个名为 _cards 的属性,而且
_cards 的值必须是可变序列。
然后,我们把 set_card 函数赋值给特殊方法__setitem__,从而把它依附到 FrenchDeck 类上。
这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。
协议是动态的

random.shuffle 函数不关心参数的类型,只要那个对象实现了部分可变序列协议即可。

即便对象一开始没有所需的方法也没关系,后来再提供也行

抽象基类使用姿势

有时,为了让抽象基类识别子类,甚至不用注册。
其实,抽象基类的本质就是几个特殊方法。

>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
可以看出,无需注册,abc.Sized 也能把 Struggle 识别为自己的子类,只要实现
了特殊方法 len 即可(要使用正确的句法和语义实现,前者要求没有参数,后
者要求返回一个非负整数,指明对象的长度;
作者建议

如果实现的类体现了 numbers、collections.abc 或其他框架中
抽象基类的概念,
要么继承相应的抽象基类(必要时),要么把类注册到相应的抽象
基类中。
开始开发程序时,不要使用提供注册功能的库或框架,要自己动手注册

一句话:
1.要么继承基类
2.要么自己把类注册到相应的抽象基类中 ,别使用自动注册

isinstance 检查使用姿势

然而,即便是抽象基类,也不能滥用 isinstance 检查,用得多了可能导致代码异味,即表明面向对象设计得不好。

在一连串 if/elif/elif 中使用 isinstance 做检查,然后根据对象的类型执行不同的操作,通常是不好的做法;

此时应该使用多态,即采用一定的方式定义类,让解释器把调用分派给正确的方法,而不使用 if/elif/elif 块硬编码分派逻辑。

鸭子类型 和 类型检查

在框架之外,鸭子类型通常比类型检查更简单,也更灵活。

本书有几个示例要使用序列,把它当成列表处理。

我没有检查参数的类型是不是list,而是直接接受参数,立即使用它构建一个列表。

这样,我就可以接受任何可迭代对象;

如果参数不是可迭代对象,调用立即失败,并且提供非常清晰的错误消息。

一句话:
看起来像鸭子(如序列),直接用序列的特性方法,(如果爆错就是类型不对),如果可以就是通过

这种做法省去了,用isinstance 做检查的痛苦(有时不知道什么类型)

标准库中的抽象基类急顺序 page 375 376 定义并使用一个抽象基类

重点来了

想象一下这个场景:

你要在网站或移动应用中显示随机广告,但是在整个广告清单轮转一遍之前,不重复显示
广告。

假设我们在构建一个广告管理框架,名为 ADAM。

它的职责之一是,支持用户提供随机挑选的无重复类。

为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么意思,我们将定义一个抽象基类。

我将使用现实世界中的物品命名这个抽象基类:宾果机和彩票机是随机从有限的集合中挑选物品的机器,选出的物品没有重复,直到选完为止
Tombola 抽象基类有四个方法,其中两个是抽象方法。

.load(...):把元素放入容器。

.pick():从容器中随机拿出一个元素,返回选中的元素。

另外两个是具体方法。

.loaded():如果容器中至少有一个元素,返回 True。

.inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容 (内部的顺序不保留)。

代码:

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,然后将其返回。
        如果实例为空,这个方法应该抛出`LookupError`。
        """

    def loaded(self):
        """如果至少有一个元素,返回`True`,否则返回`False`。"""
        return bool(self.inspect())

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))


自己定义的抽象基类要继承 abc.ABC。
根据文档字符串,如果没有元素可选,应该抛出 LookupError。
❹ 抽象基类可以包含具体方法。
❻ 我们不知道具体子类如何存储元素,不过为了得到 inspect 的结果,我们可以不断调
用 .pick() 方法,把 Tombola 清空……
❼ ……然后再使用 .load(...) 把所有元素放回去。

其实,抽象方法可以有实现代码。即便实现了,子类也必须覆盖抽象方法,但
是在子类中可以使用 super() 函数调用抽象方法,为它添加功能,而不是从头开始
实现。
定义Tombola抽象基类的子类

BingoCage 类是在示例 5-8 的基础上修改的,使用了更好的随机发生
器。
BingoCage 实现了所需的抽象方法 load 和 pick,从 Tombola 中继承了 loaded 方
法,覆盖了 inspect 方法,还增加了 call 方法。

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,然后将其返回。
        如果实例为空,这个方法应该抛出`LookupError`。
        """

    def loaded(self):
        """如果至少有一个元素,返回`True`,否则返回`False`。"""
        return bool(self.inspect())

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))


import random

class BingoCage(Tombola):
    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    def load(self, items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError("pick from empty BingoCage")

    def __call__(self):
        self.pick()

❹ 没有使用 random.shuffle() 函数,而是使用 SystemRandom 实例的 .shuffle() 方法。
这里想表达的观点是:我们可以偷懒,直接从抽象基类中继承不是那么理想的具体方法。

从 Tombola 中继承的方法没有BingoCage 自己定义的那么快,不过只要 Tombola 的子类正确实现 pick 和 load 方法,就能提供正确的结果。

LotteryBlower 打乱“数字球”后没有取出最后一个,而是取出一个随机位置上的

球。

❷ 如果范围为空,random.randrange(...) 函数抛出 ValueError,为了兼容
Tombola,我们捕获它,抛出 LookupError。

❹ 覆盖 loaded 方法,避免调用 inspect 方法(示例 11-9 中的 Tombola.loaded 方法是
这么做的)。我们可以直接处理 self._balls 而不必构建整个有序元组,从而提升速
度。

有个习惯做法值得指出:

init 方法中,self._balls 保存的是list(iterable),而不是 iterable 的引用(即没有直接把iterable 赋值给self._balls)。

前面说过, 这样做使得 LotteryBlower 更灵活,因为 iterable 参数可以是任何可迭代的类型。

把元素存入列表中还确保能取出元素。

就算 iterable 参数始终传入列表,list(iterable)
会创建参数的副本,这依然是好的做法,因为我们要从中删除元素,而客户可能不希望自己提供的列表被修改。

Tombola的虚拟子类

注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后,注册的类会变成抽象基类的虚拟子类,

而且 issubclass 和 isinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。

3.虚拟子类不会继承注册的抽象基类,为了避免运行时错误,虚拟子类要实现所需的全部方法。

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,然后将其返回。
        如果实例为空,这个方法应该抛出`LookupError`。
        """

    def loaded(self):
        """如果至少有一个元素,返回`True`,否则返回`False`。"""
        return bool(self.inspect())

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))


import random


class BingoCage(Tombola):
    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    def load(self, items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError("pick from empty BingoCage")

    def __call__(self):
        self.pick()


class LotteryBlower(Tombola):
    def __init__(self, iterable):
        self._balls = list(iterable)

    def load(self, iterable):
        self._balls.extend(iterable)

    def pick(self):
        try:
            position = random.randrange(len(self._balls))
        except ValueError:
            raise LookupError("pick from empty lotteryBlower")

    def loaded(self):
        return bool(self._balls)

    def inspect(self):
        return tuple(sorted(self._balls))


from random import randrange


@Tombola.register
class TomboList(list):
    def pick(self):
        if self:
            position = randrange(len(self))
            return self.pop(position)
        else:
            raise LookupError("pop from empty TomboList")

    load = list.extend

    def loaded(self):
        return bool(self)

    def inspect(self):
        return tuple(sorted(self))


# Tombola.register(TomboList)

把 Tombolist 注册为 Tombola 的虚拟子类。
❸ Tombolist 从 list 中继承 bool 方法,列表不为空时返回 True。
❹ pick 调用继承自 list 的 self.pop 方法,传入一个随机的元素索引。

注册之后,可以使用 issubclass 和 isinstance 函数判断 TomboList 是不是Tombola的子类:

>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True
Tombola子类的测试方法

__subclasses__()
  这个方法返回类的直接子类列表,不含虚拟子类。
_abc_registry
  只有抽象基类有这个数据属性,其值是一个 WeakSet 对象,即抽象类注册的虚拟子
类的弱引用。

Python使用register的方式

Tombola.register 当作类装饰器使用。在 Python 3.3 之前的版本中不能这
样使用 register

虽然现在可以把 register 当作装饰器使用了,但更常见的做法还是把它当作函数使用,
用于注册其他地方定义的类。

即便不注册,抽象基类也能把一个类识别为虚拟子类
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True

issubclass 函数确认(isinstance 函数也会得出相同的结论)

Struggle 是abc.Sized 的子类,

这是因为 abc.Sized 实现了一个特殊的类方法,名为__subclasshook__。

Sized 类的源码:
class Sized(metaclass=ABCMeta):
 __slots__ = ()
 @abstractmethod
 def __len__(self):
 return 0
 @classmethod
 def __subclasshook__(cls, C):
 if cls is Sized:
 if any("__len__" in B.__dict__ for B in C.__mro__): # ➊
 return True # ➋
 return NotImplemented # ➌

对 C.__mro__ (即 C 及其超类)中所列的类来说,如果类的 dict 属性中有名为
len 的属性……

小结

1.抽象基类的使用姿势
2.定义一个随机抽象基类
3.虚拟子类 只是注册就行,(没继承),必须实现所有方法
4.Tombola 这个自定义的抽象基类多写几次

其他:

非正式接口(称为协议)的高度动态本性,
以及使用 subclasshook 方法动态识别子类。

我们发现 Python 对序列协议的支持十分深入。
如果一个类实现了__getitem__ 方法,此外什么也没做,那么 Python 会设法迭代它,而且 in 运算符也随之可以使用。

显式继承抽象基类的优缺点。
继承abc.MutableSequence 后,必须实现 insert 和 delitem 方法,而我们并不需要这两个方法。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41771.html

相关文章

  • 流畅python读书笔记--继承优缺点

    摘要:继承的优缺点推出继承的初衷是让新手顺利使用只有专家才能设计出来的框架。多重继承的真实应用多重继承能发挥积极作用。即便是单继承,这个原则也能提升灵活性,因为子类化是一种紧耦合,而且较高的继承树容易倒。 继承的优缺点 推出继承的初衷是让新手顺利使用只有专家才能设计出来的框架。——Alan Kay 子类化内置类型很麻烦 (如 list 或 dict)) ,别搞这种 直接子类化内置类型(如 ...

    morgan 评论0 收藏0
  • Python学习之路30-接口协议抽象基类

    摘要:本篇内容将从鸭子类型的动态协议,逐渐过渡到使接口更明确能验证实现是否符合规定的抽象基类。抽象基类介绍完动态实现接口后,现在开始讨论抽象基类,它属于静态显示地实现接口。标准库中的抽象基类从开始,标准库提供了抽象基类。 《流畅的Python》笔记。本篇是面向对象惯用方法的第四篇,主要讨论接口。本篇内容将从鸭子类型的动态协议,逐渐过渡到使接口更明确、能验证实现是否符合规定的抽象基类(Abst...

    LucasTwilight 评论0 收藏0
  • Python基础之接口——协议抽象基类

    摘要:本文重点协议是中非正式的接口了解抽象基类的基本概念以及标准库中的抽象基类掌握抽象基类的使用方法。三抽象基类的使用通过继承声明抽象基类声明抽象基类最简单的方式是继承或其他抽象基类注意在之间,继承抽象基类的语法是。 导语:本文章记录了本人在学习Python基础之面向对象篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。 本文重点: 1、协议是Python中非正式的接...

    TwIStOy 评论0 收藏0
  • 流畅python读书笔记--使用一等函数实现设计模式

    摘要:在复杂的情况下,需要具体策略维护内部状态时,可能需要把策略和享元模式结合起来。函数比用户定义的类的实例轻量,而且无需使用享元模式,因为各个策略函数在编译模块时只会创建一次。 一等函数实现设计模式 经典的策略模式定义 定义一系列算法,把它们一一封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。 案例 假如一个网店制定了下述折扣规则。 有 1000 或以上积分...

    cnsworder 评论0 收藏0
  • 流畅python读书笔记--序列修改、散列和切片

    摘要:例如,的序列协议只需要和两个方法。任何类如,只要使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。 序列的修改、散列和切片 接着造Vector2d类 要达到的要求 为了编写Vector(3, 4) 和 Vector(3, 4, 5) 这样的代码,我们可以让 init 法接受任...

    cpupro 评论0 收藏0

发表评论

0条评论

Kyxy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<