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

# 第11章 接口:从协议到抽象基类
"""
本章探讨从鸭子类型的代表特征动态协议,到使接口更明确、能雅正实现是否符合规定的抽象基类
本章将实现一个抽象基类,看看它的运作方式,但作者不建议自己编写抽象基类,因为容易过度设计
"""

# 11.1 Python文化中的接口和协议
# 协议是接口,但不能像正式接口那样施加限制,python通过抽象基类对接口作一致性的强制,
# 一个类可以只实现部分接口

# 11.2 Python喜欢序列
# Foo类没有继承abc.sequence,而且只实现了序列协议的一个方法
# 示例 11-3 实现了__getitem__就能访问元素/迭代/使用in运算符

"""
class Foo:
    def __getitem__(self, pos):
        return range(0,30,10)[pos]
f = Foo()
print(f[1])
for i in f:
    print(i)
print(20 in f)
print(15 in f)
"""

# 如果没有__iter__和__contains__方法,python解释器会尝试调用__getitem__来迭代对象或者使用in

# 示例11-4 实现序列协议的FrenchDeck类(代码和第一章相同)
"""
import collections
from random import choice
Card = collections.namedtuple('card',['rank','suit'])
class FrenchDeck:
    ranks = [str(x) for x 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]
"""

# 11.3 使用猴子补丁在运行时实现协议
# 示例11-4中的FrenchDeck无法实现洗牌

# 标准库中random.shuffle函数用法
"""
from random import shuffle
l = list(range(10))
shuffle(l)  # shuffle是就地打乱序列
print(l)

deck = FrenchDeck()
"""
# shuffle(deck)
# TypeError: 'FrenchDeck' object does not support item assignment
# FrenchDeck 不支持为元素赋值
# 这是因为FrenchDeck只实现了不可变的序列协议
# 可变的还必须提供__setitem__方法

# python是动态语言,在运行时修正这个问题,这被称为猴子补丁
# 猴子补丁:在运行时修改类或者模块,而不改动源码
# 实例11-5 在运行时把FrenchDeck变成可变的,让shuffle能处理
"""
def set_card(deck,position,card):
    deck._cards[position] = card
# 把函数赋给类的__setitem__方法
FrenchDeck.__setitem__ =set_card
shuffle(deck)
print(deck[:5])
"""

# 11.4 Alex Martelli的水禽
# 鸭子类型:忽略对象的真正类型,转而关注对象有没有实现所需的方法\签名和语义
# 白鹅类型(goose typing):
# 只要cls是抽象基类,即cls的元类是abc.ABCMeta,就可以使用isinstance(obj,cls)来检查obj的类型
# 继承抽象基类只需实现规定的方法,或者通过注册虚拟子类来实现

# 示例11-7 使用鸭子类型处理单个字符串或由字符串组成的可迭代对象
"""
try:# 假设是单个字符串
    # 把逗号替换成空格在拆分成列表
    field_names = field_names.replace(',',' ').splt()
except AttributeError:# field_names看起来不像字符串,或者返回值不能使用split拆分
    pass # 假设已经是由名称组成的可迭代对象了
field_names = tuple(field_names)  # 为了确保已经是可迭代对象了,也为了创建一份副本,使用所得值创建元组
"""

# 11.5定义抽象基类的子类
# 将Frenchdeck定义为collections.MutableSequence的子类
# frenchdeck2.py

# 11.6 标准库中的抽象基类
# 大多数抽象基类在collections.abc模块中
# numbers和io包中也有一些抽象基类

# 11.6.1 collections.abc模块中的抽象基类
# 集合s应该继承的三个抽象基类 Iterable Container
#     Iterable:通过__iter__支持迭代
#     Container:通过__contains__支持in运算符
#     Sized:通过__len__支持len()函数
# 不可变集合应该继承的三个抽象基类:Sequence Mapping Set
# 可变集合应该继承的三个抽象基类:MutableSequence MutableMapping MutableSet
# MappingView:如.items() .keys() .values()返回的对象
# Callable Hashable:这两个抽象子类一般不子类化,主要作用是为isinstance提供支持
#     判断对象能不能调用或者哈希
# Iterator:是Iterable的子类

# 11.6.2 Numbers 抽象基类的数字塔
# Numbers
# Complex
# Real
# Rational
# Integral
# 这样检查一个对象是不是整数就可以使用  isinstance(obj,Numbers.Integral)
#     这样的代码能接受int\bool,或者外部库使用Numbers抽象基类注册的其他类型
# 如果一个对象可能是浮点数类型,可以使用 isinstance(obj,Numbers.Real)
#     这样的代码能接受int\bool\float\fractions.Fraction,或者外部库(如Numpy)提供的非复数类型

# 11.7 定义并使用一个抽象基类及其子类
#     这么做是为了教你阅读标准库中的抽象基类源码 tombola.py

# __mro__:方法解析顺序 (Method Resolution Order)
#     按顺序列出对象的类和超类


# 11.8 Tombola子类的测试方法
# tombola_runner.py

# python使用register的方式
# collections.abc源码中,把内置类型tuple\str\range\memoryview注册为Sequence的虚拟子类
#     Sequence.register(tuple)
#     Sequence.register(str)
#     Sequence.register(range)
#     Sequence.register(memoryview)

# 11.10 鹅的行为有可能像鸭子
class Struggle:
    def __len__(self):return 23
from collections import abc

print(isinstance(Struggle(), abc.Sized))  # True
# Struggle是Sized的子类,这是因为abc.Sized实现了一个特殊的类方法__subclasshook__

# 示例 11-17 Sized源码
"""
class Sized(metaclass=ABCMeta):
    __slots__ = ()
    @abstractmethod
    def __len__(self):
        return 0
    
    @classmethod
    def __subclasshook__(cls,C):
        # 对于C.__mro__中所列的类中,如果__dict__属性中有__len__)
        if any("__len__" in B.__dict__ for B in C.__mro__):
            return True
        # 否则让子类检查
        return NotImplemented
"""

# 总结:
"""
本章首先介绍了非正式接口(协议)的高度动态本性
然后讲解了抽象基类的静态接口声明
最后指出了抽象基类的动态特性:虚拟子类,
以及使用__subclasshook__方法动态识别子类 
"""








# frenchdeck2.py
# frenchdeck2.py
import collections
Card = collections.namedtuple('card',['rank','suit'])

class FrenchDeck2(collections.MutableSequence):
    ranks = [str(x) for x 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]

    def __setitem__(self, position, value):
        '''为了支持洗牌'''
        self._cards[position] = value

    def __del__(self,position):
        '''继承collections.MutableSequence的类必须实现__del__方法'''
        del self._cards[position]

    def insert(self, position, value) :
        '''继承collections.MutableSequence的类必须实现insert方法'''
        self._cards.insert(position,value)

"""
导入时python不会检查抽象方法的实现
在运行时,FrenchDeck2实例化才会真正检查
"""
tombola_runner.py
# 定义一个抽象基类及其子类

"""
运用场景:
    在网站或者移动应用中显示随机广告,在整个广告清单轮转一遍之前不重复显示广告
    假设我们在构建一个广告管理框架ADAM
        它的职责之一是,支持用户提供随机挑选的无重复类
    定义一个抽象基类:Tombola
        它有四个方法
            两个抽象方法:
                .load():把元素放入容器
                .pick():从容器随机拿出一个元素,返回选中的元素
            两个具体方法
                .loaded():如果容器中至少有一个元素,返回True
                .inspect():
                    返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容
    Tombola的抽象基类和三个具体实现:
        BingoCage:
            __init__
            load
            pick
            __call__
        LotteryBlower:
            __init__
            load
            pick
            loaded
            inspect
        Tombolist:通过registered注册的
抽象基类的一些编程规则:
    抽象基类会检查子类
    abc模块定义了三个装饰器:
        @abc.abstractmethod
        @abc.abstractproperty
        @abc.abstractstaticmethod
        @abc.abstractclassmethod
        但是因为python3.3以后装饰器可以堆叠后三个装饰器就被废弃了
        因为可以这样写
        @classmethod
        @@abc.abstractmethod # 注意这个在最内层
        def xxx():
            pass

"""
import abc
import random

class Tombola(abc.ABC):  # 自己定义的抽象基类必须继承abc.ABC
    @abc.abstractmethod  # 抽象方法用@abc.abstractmethod装饰,而且定义体中通常只有文档字符串
    def load(self,iterable):
        '''从可迭代对象中添加元素'''

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

    def loaded(self):  # 抽象基类可以包含具体方法
        '''如果至少有一个元素,返回True
        否则返回False
        抽象基类只能依赖抽象基类定义的接口如inspect'''
        return bool(self.inspect())

    def inspect(self):
        '''返回一个有序元组,有当前元素构成
        我们不知道具体子类如何存储元素,不过为了得到inspect的结果
        可以不断pick把Tombola清空,然后调用
        .load把所有元素放回去'''
        items = []
        while 1:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

# 11.7.2定义抽象基类的子类
# 定义BingoCage类
class BingoCage(Tombola):
    def __init__(self,items):
        '''假设我们将在线上游戏中使用这个,random.SystemRandom
        使用os.urandom()函数实现random API
        生成适合用于加密的字节序列'''
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)  # 委托load实现初始加载

    def loaded(self,items):
        self._items.extend(items)
        # 没有使用random.shuffle而是使用self._randomize的shuflle方法
        self._randomizer.shuffle(self._items)

    def pick(self):  # 重写了pick方法
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')

    def __call__(self):
        return self.pick()

# 定义LotteryBlower子类 覆盖了继承的inspect和loaded方法
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))
        # 如果范围为空,抛出ValueError
        except ValueError:
            # 为了兼容Tombola抛出LookuopError
            raise LookupError('pick from empty LotteryBlower')
        return self._balls.pop(position)
    def loaded(self):
        '''覆盖loaded方法,避免调用insepect方法
        直接处理self._balls而不必构建整个有序元组'''
        return bool(self._balls)
    def inspect(self):
        '''使用一行代码覆盖inspect'''
        return tuple(sorted(self._balls))

# 11.7.3 Tombola的虚拟子类
# 白鹅类型的一个基本特性:
#     即便不继承,也有办法把一个类注册为抽象基类的虚拟子类
# 注册虚拟子类的方法:
#     在抽象基类上调用register方法
#     这样注册的类会变成抽象基类的虚拟子类,而且isinstance和issubclass都能识别
#     ,但是注册的类不会从抽象基类继承任何的属性和方法
# 虚拟子类在任何时候都不会检查是否符合抽象基类的接口,即便实例化时也不会检查
# 为了避免运行错误,虚拟子类要实现所需的全部方法

# 11-14 Tombola的虚拟子类 TomboList
from random import randrange

@Tombola.register  # 把TomboList注册为Tombola的虚拟子类
class TomboList(list):  # 继承list
    def pick(self):
        if self:  # 继承list的__bool__方法,列表不为空返回True
            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) 3.3以前的版本







if __name__ == '__main__':
    # 示例11-11不符合Tombola要求的子类无法蒙混过关
    class Fake(Tombola):
        def pick(selfs):
            return 13
    print(repr(Fake))
    # fake = Fake() # TypeError: Can't instantiate abstract class Fake with abstract method load
    # print(repr(fake))
    print(issubclass(TomboList, Tombola))
    t = TomboList(range(100))
    print(isinstance(t, Tombola))
    print(TomboList.__mro__)  # (<class '__main__.TomboList'>, <class 'list'>, <class 'object'>)
tombola_runner.py这个测试文件没有完全实现,应该是要在类里面添加相关属性
import doctest
from tombola import Tombola,BingoCage,LotteryBlower,TomboList
TEST_FILE = 'tombola_tests.rst'
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed-{2}'

def main(argv):
    verbose = '-v' in argv
    real_subclasses = Tombola.__subclasses__()
    virtual_subclasses = list(Tombola._abc_registry)
    for cls in real_subclasses+virtual_subclasses:
        test(cls,verbose)

def test(cls,verbose = False):
    res = doctest.testfile(
        TEST_FILE,
        globs={'ConcreteTombola':cls},
        verbose=verbose,
        optionflags = doctest.REPORT_ONLY_FIRST_FAILURE
    )
    tag = 'FAIL' if res.failed else 'OK'
    print(TEST_MSG.format(cls.__name__,res,tag))

if __name__ == '__main__':
    import sys
    main(sys.argv)

35岁学Python,也不知道为了啥?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值