[TOC]

第五部分 控制流程


Chapter14 可迭代的对象, 迭代器和生成器###

0. 本章要点

  • 所有生成器都是迭代器
  • 迭代器用于从集合中取出元素
  • 生成器用于"凭空"生成元素, 比如 生成 斐波那契数列
  • 迭代器的用途
    • for 循环
    • 构建遍历文本文件
    • 逐行遍历文本文件
    • 列表推导, 字典推导, 集合推导
    • 元组拆包
    • 调用函数时, 使用 * 拆包实参
  • 本章话题:
    • iter(…) 处理可迭代对象的方式
    • 使用 Python 实现经典的迭代器模式
    • 生成器的工作原理
    • 使用生成器函数/表达式, 来代替经典的迭代器
    • 标准库中的生成器函数
    • 使用 yield from 语句合并生成器
    • 使用生成器构造一个数据库转换工具来处理大型数据集
    • 生成器与协程

1. Sentence 类第一版: 单词序列

  1. sentence.py

     ::Python
     import re
     import reprlib
     # reprlib: Redo the builtin repr() (representation) 
     # but with limits on most sizes.
    
     RE_WORD = re.compile('\w+')
    
     class Sentence:
         def __init__(self, text):
             self.text = text
             self.words = RE_WORD.findall(text)
    
         def __getitem__(self, index):
             return self.words[index]
    
         def __len__(self):
             return len(self.words)
    
         def __repr__(self):
             return 'Sentence(%s)' % reprlib.repr(self.text)
    
     s = Sentence('"The time has come," the Walrus said, ')
     print(s)
    
     for word in s:
         print(word)
     print(list(s))
     # ['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
    
  2. 序列可以迭代的原因: iter 函数

    • 解释器需要迭代对象 x 时, 会自动调用 iter(x)
    • iter 函数的作用:
      • 检查是否实现了 __iter__ 方法, 如果实现了就调用它, 获取一个迭代器
      • 如果没有实现 __iter__ 方法, 但是实现了 __getitem__ 方法, Python 会创建一个迭代器, 尝试按顺序获取元素
      • 如果 尝试失败, Python 会抛出 TypeError 异常, “C object is not iterable”, 其中 C 是目标对象所属的类
    • Python 序列可以迭代的原因是, 它们都实现了 __getitem__ 方法
    • 鸭子类型(duck typing): 指忽略对象的真正类型,转而关注对象有没有实现所需的方法、签名和语义。在 Python 中指免使用 isinstance 检查对象的类型。
    • 白鹅类型(goose-typing): 白鹅类型指,只要 cls 是抽象基类,即 cls 的元类是 abc.ABCMeta,就可以使用 isinstance(obj, cls)。如果实现了 __iter__ 方法, 就认为对象是可迭代的, 因为 abc.Iterable 类实现了 __subclasshook 方法
    • 检查对象 x 能否迭代: Python 3.4 后调用 iter(x)

2. 可迭代对象(iterable) 与 迭代器(iterator)

  1. 可迭代对象: 使用 iter(x) 函数可以获取迭代器的对象, 如果对象实现了能返回迭代器的 __iter__ 方法, 那么对象就是可以迭代的, 序列都是可以迭代的

  2. Python 从可迭代对象中获取迭代器

     ::python
     In [97]: s = 'ABC'
    
     In [99]: it = iter(s)
    
     In [100]: it
     Out[100]: <str_iterator at 0x105793240>
    
     In [101]: while True:
          ...:     try:
          ...:         print(next(it))
          ...:
          ...:     except StopIteration:
          ...:         del it
          ...:         break
          ...:
     A
     B
     C
    
  3. 标准迭代器接口的两个方法:

    • __next__, 返回下一个可用元素, 如果没有, 抛出 StopIteration 异常
    • __iter__, 返回 self, 以便在应该使用可迭代对象的地方使用迭代器, 例如 for 循环
  4. 两个接口在 collections.abc.Iterator 抽象基类中制定, Iterator 继承了 Iterable(定义了抽象方法 __iter__), 并实现了 __next__ 抽象方法

  5. Python3 Iterator 抽象基类定义的抽象方法是 it.__next__(), Python2 中是 it.next() 方法, 如果要调用, 应该避免直接调用特殊方法, 使用 next(it)

  6. 迭代器: 实现了__iter__ 和 无参数的 __next__ 方法, 返回序列中的下一个元素如果没有元素了, 抛出 StopIteration 异常

3. Sentence 类第二版: 典型的迭代器

  1. sentence_iter.py:

     ::python
     import re
     import reprlib
    
     RE_WORD = re.compile('\w+')
    
     class Sentence:
         def __init__(self, text):
             self.text = text
             self.words = RE_WORD.findall(text)
         # def __getitem__(self, index):
         #     return self.words[index]
         # def __len__(self):
         #     return len(self.words)
         def __repr__(self):
             return 'Sentence(%s)' % reprlib.repr(self.text)
         def __iter__(self):
             return SentenceIterator(self.words)
    
     class SentenceIterator:
         def __init__(self, words):
             self.words = words
             self.index = 0
         def __next__(self):
             try:
                 word = self.words[self.index]
             except IndexError:
                 raise StopIteration()
             self.index += 1
             return word
         def __iter__(self):
             return self
    
  2. 迭代器与可迭代对象的区别:

    • 可迭代对象中有个 __iter__ 方法, 每次实例化一个新的迭代器, 而迭代器要实现 __next__ 方法, 返回单个元素, 实现 __iter__ 方法, 返回迭代器本身
    • 可迭代对象一定不能是自身的迭代器
    • 可迭代对象必须实现 __iter__ 方法, 但不能实现 __next__ 方法
  3. 设计模式中的迭代器模式:

    • 访问一个聚合对象的内容而无需暴露它的内部表示
    • 支持对聚合对象的多种遍历
    • 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)
  4. 为了支持上面的第二条"多种遍历”, 必须从同一个可迭代实例中获取多个独立的迭代器, 所以迭代器模式的正确实现方式是每次调用 iter(my_iterable) 都新建一个独立的迭代器, 所以示例中需要定义 SentenceIteration 类

  5. range 是可迭代对象, 不是迭代器

  6. zip, enumerate, generator, 是迭代器

4. Sentence 类第三版: 生成器函数

  1. sentence_gen.py:

     ::python
     import re
     import reprlib
    
     RE_WORD = re.compile('\w+')
    
     class Sentence:
         def __init__(self, text):
             self.text = text
             self.words = RE_WORD.findall(text)
         # def __getitem__(self, index):
         #     return self.words[index]
         # def __len__(self):
         #     return len(self.words)
         def __repr__(self):
             return 'Sentence(%s)' % reprlib.repr(self.text)
         def __iter__(self):
             # return SentenceIterator(self.words)
             for word in self.words:
                 yield word
             return # 非必要
    
  2. 生成器函数的工作原理

    • 生成器函数定义: 含有 yield 关键字
    • 调用生成器函数, 会返回一个生成器对象, 所以生成器函数就是生成器工厂
  3. Sentence.__iter__ 方法的作用: __iter__ 方法是生成器函数, 调用时会构建一个实现了迭代器接口的生成器对象

5. Sentence 类第四版: 惰性实现

  1. 惰性求值(lazy evaluation) 及早求值(eager evaluation)

  2. re.finditer 是 re.findall 函数的惰性版本, 返回的不是列表, 而是一个生成器, 按需生成 re.MatchObject 实例, 可以节省内存

  3. sentece.gen2.py:

     ::python
     import re
     import reprlib
    
     RE_WORD = re.compile('\w+')
    
     class Sentence:
         def __init__(self, text):
             self.text = text
             # self.words = RE_WORD.findall(text)
    
         def __repr__(self):
             return 'Sentence(%s)' % reprlib.repr(self.text)
         def __iter__(self):
         #     # return SentenceIterator(self.words)
         #     for word in self.words:
         #         yield word
         #     return 
             for match in RE_WORD.finditer(self.text):
                 yield match.group()
    

6. Sentence 类第五版: 生成器表达式

  1. 生成器函数可以替换为生成器表达式

  2. 生成器表达式可以理解为 列表推导的惰性版本

  3. 列表推导式 […] 与 生成器表达式 (…)的区别: gen_AB:

     ::python
     In [103]: def gen_AB():
          ...:     print('start')
          ...:     yield 'A'
          ...:     print('continue')
          ...:     yield 'B'
          ...:     print('end.')
          ...:
     In [104]: res1 = [x*3 for x in gen_AB()]
     start
     continue
     end.
    
     In [105]: for i in res1:
          ...:     print('-->', i)
          ...:
     --> AAA
     --> BBB
    
     In [106]: res2 = (x*3 for x in gen_AB())
    
     In [107]: res2
     Out[107]: <generator object <genexpr> at 0x10577de08>
    
     In [108]: for i in res2:
          ...:     print('-->', i)
          ...:
     start
     --> AAA
     continue
     --> BBB
     end.
    
  4. 生成器表达式 (…)会产出生成器

  5. sentence_genexp.py

     ::python
     import re
     import reprlib
    
     RE_WORD = re.compile('\w+')
    
     class Sentence:
         def __init__(self, text):
             self.text = text
             # self.words = RE_WORD.findall(text)
    
         def __repr__(self):
             return 'Sentence(%s)' % reprlib.repr(self.text)
         def __iter__(self):
             # for match in RE_WORD.finditer(self.text):
             #     yield match.group()
             return (match.group() for match in RE_WORD.finditer(self.text))
    
  6. 生成器表达式是语法糖

7. 生成器表达式的用途

  • 省内存, 简洁…

8. 等差数列生成器

  1. ArithmeticProgression

     ::python
     class ArithmeticProgression:
         def __init__(self, begin, step, end=None):
             self.begin = begin
             self.step = step
             self.end = end
    
         def __iter__(self):
             # 先强制把 begin 转换成 加法运算后得到的类型, 再赋值给 result
             result = type(self.begin + self.step)(self.begin)
             forever = self.end is None
             index = 0
             while forever or result < self.end:
                 yield result
                 index += 1
                 result = self.begin + self.step * index
    
  2. ariprog_gen 生成器函数

     ::python
     def aritprog_gen(begin, step, end=None):
         result = type(begin + step)(begin)
         forever = end is None
         index = 0
         while forever or result < end:
             yield result 
             index += 1
             result = begin + step * index
    
  3. 使用 itertools 模块生成等差数列

    1. itertools.count, 不会停止生成

       ::python
       In [109]: import itertools
      
       In [110]: gen = itertools.count(1, .5)
      
       In [111]: gen
       Out[111]: count(1, 0.5)
      
       In [112]: next(gen)
       Out[112]: 1
      
       In [113]: next(gen)
       Out[113]: 1.5
      
    2. itertools.takewhile 函数会生成一个使用另一个生成器的生成器, 当条件为 False 时停止

       ::python
       In [123]: gen = itertools.takewhile(lambda x: x<3, itertools.count(1, .5))
      
       In [124]: gen
       Out[124]: <itertools.takewhile at 0x104a4a088>
      
       In [125]: list(gen)
       Out[125]: [1, 1.5, 2.0, 2.5]
      
    3. aritprog_v2.py:

       ::python
       def aritprog_gen(begin, step, end=None):
           # result = type(begin + step)(begin)
           # forever = end is None
           # index = 0
           # while forever or result < end:
           #     yield result 
           #     index += 1
           #     result = begin + step * index
           first = type(begin + step)(begin)
           ap_gen = itertools.count(first, step)
           if end is not None:
               ap_gen = itertools.takewhile(lambda n: n < end, ap_gen)
           return ap_gen
      

9. 标准库中的生成器函数

  1. os.walk 遍历目录树
  2. itertools 的各种函数, 处理列表, 生成各种列表, 排序…
    • iterator.tee: 从输入的一个可迭代对象中产出多个生成器, 每个生成器都可以产出输入的各个元素

10. yield from

  • 生成器函数需要产出另一个生成器生成的值

11. 可迭代的归约函数

  • 归约函数: 接受一个可迭代的对象, 然后返回单个结果
    • all(it), it 中所有元素都为真值时返回 True, all([]) 返回 True
    • any(it), it 中只要有元素为真值就返回 True, any([]) 返回 False
    • functools.reduce(func, it, [initial]), 把前两个元素传给 func, 然后把计算结果和第三个元素传给 func, 依次类推

12. 深入分析 iter 函数

  1. iter(x, y), 传入两个参数, 使用常规的函数或任何可调用的对象创建迭代器, 第一个参数是可调用对象, 第二个参数是哨符, 标记值, 当可调用对象返回这个值时, 触发, StopIteration 异常
    • 逐行读取文件, 直到遇到空行或者到达文件末尾为止

        ::python
        with open('mydata.txt') as fp:
            for line in iter(fp.readline, '\n'):
                process_line(line) 
      

13. 案例分析: 在数据库转换工具中使用生成器

  1. isis2json.py, 支持读取两种数据结构, 输出 JSON 格式
    • 难点: 大文件, 两种格式
    • 解决方法: 隔离读取逻辑, 写进两个生成器函数中, 一个函数支持一种输入格式. 利用生成器函数解耦了读逻辑和写逻辑, 数据量的情况下使用生成器交叉读写

14. 把生成器当成协程

  1. send() 方法允许在客户代码和生成器之间双向交换数据, 而 __next__() 方法只允许客户从生成器中获取数据, 使用 send() 生成器就变成了协程
  2. 生成器用于生成供迭代的数据, 协程是数据的消费者, 虽然协程使用 yield 产出值, 但与迭代无关

15. 本章小结

  1. 本章通过编写一个类, 用于读取数据量大的文件, 并迭代里面的单词, 最终使用生成器, 以此了解了生成器的工作原理

16. 延伸阅读

  1. 生成器与协程
  2. 生成器与迭代器
    1. 接口:

      • 所有生成器都是迭代器, 生成器对象实现了迭代器协议的两个方法: __next__, __iter__

      • enumerate() 函数创建的对象是迭代器

          ::python
          In [39]: from collections import abc
        
          In [40]: e = enumerate('ABC')
        
          In [41]: isinstance(e, abc.Iterator)
          Out[41]: True
        
    2. 实现方式

      • 生成器两种方式: 含有 yield 关键字的函数, 或者生成器表达式
      • 两种得到的生成器对象 都属于 GeneratorType 类型, 因为 Generator 类型的实例实现了迭代器接口, 所以可以说所有生成器的都是迭代器
    3. 概念:

      • 迭代器用于遍历集合从中产出元素
      • 生成器可以无需遍历集合就能生成值

Chapter15 上下文管理器和 else 块

0. 本章要点

  • with 语句和上下文管理器
  • for, while, try 语句的 else 子句

1. if 语句之外的 else 块

  1. else 可以在 for, while, try 中使用, 语义相当于 then
    • for: 当循环结束后(即 for 循环没有被 break 语句中止)才运行 else
    • while: 当条件为 False 时, 执行 else(break 不执行)
    • try: 当 try 语句没有异常抛出时运行 else
  2. 两种风格
    • EAFP (easier to ask for forgiveness than permission)
      • 特点: 使用 try/except 语句频繁
    • LBYL (look before you leap)
      • 特点: 使用 if 语句频繁

2. 上下文管理器和 with 块

  1. with 目的: 简化 try/finally 模式

  2. 上下文管理器协议, 两个方法:

    • __enter__
    • __exit__, 类似 finally 的作用
  3. 与函数和模块不同, with 块没有定义新的作用域, 所以在 with 结束后, 依然可以访问对象的属性, 但不能在 fp 对象上执行 I/O 操作, 因为在 with 块末尾, 调用 TextIOWrapper.__exit__ 把文件关闭了

  4. 实现了一个上下文管理器 LookingGlass 类

    • 代码:

        ::python        
        class LookingGlass:
            def __enter__(self):
                import sys
                # 保存原来的 write 方法
                self.original_write = sys.stdout.write
                # 猴子补丁
                sys.stdout.write = self.reverse_write
                return 'JABBERWOCKY'
      
            def reverse_write(self, text):
                self.original_write(text[::-1])
      
            def __exit__(self, exc_type, exc_value, traceback):
                import sys # 重复导入模块不会消耗很多资源, 因为 Python 会缓存导入的模块
                sys.stdout.write = self.original_write
                if exc_type is ZeroDivisionError:
                    print('Please DO NOT divide by zero!')
                    return True
                # 如果 __exit__ 方法返回 None 或 True 以外的值, 
                # with 块中的任何异常都会向上冒泡
      
    • __exit__ 参数介绍

      • exc_type: 异常类
      • exc_value: 异常实例
      • traceback: traceback 对象
    • 在 with 块之外使用 LookingGlass 类

        ::python
        In [8]: manager = LookingGlass()
      
        In [9]: manager
        Out[9]: <__main__.LookingGlass at 0x10490f400>
      
        In [10]: monster = manager.__enter__() # 进入上下文, 标准的 stdou.write 都已经被替换 
      
        In [11]: monster
        Out[11]: 'YKCOWREBBAJ'
      
        In [12]: monster == 'JABBERWOCKY'
        Out[12]: eurT
      
        In [13]: manager
        Out[13]: >004f09401x0 ta ssalGgnikooL.__niam__<
      
        In [16]: print('abc')
        cba
      
        In [17]: manager.__exit__(None, None, None)
      
        In [18]: monster
        Out[18]: 'JABBERWOCKY'
      
  5. 一些应用

3. contextlib 模块中的使用工具(TODO)

  1. 标准库 contextlib

4. 本章小结

  • 自己实现了一个上下文管理器 LookingGlass 类, 说明了如何在 __exit__ 方法中处理异常
  • 标准库中 contextlib 模块里, @contextmanager 装饰器, 把一个 yield 语句的简单生成器变成上下文管理器, 依次使用 looking_glass 生成器函数实现了 LookingGlass 类
  • @contextmanager 装饰器, 包含了三个特性:
    • 装饰器
    • 生成器
    • with 语句

Chapter16 协程

0. 本章要点

  • yield item 会产出一个值, 提供给 next(…) 调用, 此外会暂停执行生成器, 让调用方继续工作, 直到需要使用另一个值时再调用 next()

Chapter17 使用期物(future)处理并发 TODO

0. 本章要点

  • concurrent.futures 模块
  • 期物(future)概念, 表示异步执行的操作, 类比期货, 期权, 期物封装待完成的操作

1. 网络下载的三种风格(for, threadpool, asyncio)

  1. 时间:

    • flags.py 7 秒
    • flags_threadpool.py 1.4 秒 concurrent.futures 包
    • flags_asyncio.py 1.35 秒 asyncio 包
  2. flags.py:

     ::python
     import os 
     import time 
     import sys
    
     import requests # requests 不是标准库, 放在最后且隔一行
    
     POP20_CC = ('CN IN US ID BR PK NG BD RU JP \
         MX PH VN ET EG DE IR TR CD FR').split()
    
     BASE_URL = 'http://flupy.org/data/flags'
    
     DEST_DIR = 'downloads/'
    
     def save_flag(img, filename):
         path = os.path.join(DEST_DIR, filename)
         with open(path, 'wb') as fp:
             fp.write(img)
    
     def get_flag(cc):
         url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
         resp = requests.get(url)
         return resp.content
    
     def show(text):
         print(text, end=' ')
    
         # 在Linux,必须加flush()才能一秒输一个数字
         # 在Windows, 都能一秒输出一个数字
         sys.stdout.flush() 
         # 刷新 sys.stdout 以显示, 正常情况换行才刷新缓冲
    
     def download_many(cc_list):
         for cc in sorted(cc_list):
             image = get_flag(cc)
             show(cc)
             save_flag(image, cc.lower() + '.gif')
    
         return len(cc_list)
    
     def main(download_many):
         t0 = time.time()
         count = download_many(POP20_CC)
         elapsed = time.time() - t0
         msg = '\n{} flags downloaded in {:.2f}s'
         print(msg.format(count, elapsed))
    
     if __name__ == '__main__':
         main(download_many)
        
     ######## output ###########
     BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 
     20 flags downloaded in 31.01s
    
  3. concurrent.futures 模块

  • 主要有两个类: ThreadPoolExecutor 和 ProcessPoolExecutor

  • flags_threadpool.py:

      ::python
      from concurrent import futures
    
      from flags import save_flag, get_flag, show, main
    
      # 最大线程数
      MAX_WORKERS = 20
    
      def download_one(cc):
          image = get_flag(cc)
          show(cc)
          save_flag(image, cc.lower() + '.gif')
          return cc
    
    
      def download_many(cc_list):
          # 把 flags.py 中的 for 使用并发函数重写
          workers = min(MAX_WORKERS, len(cc_list))
          with futures.ThreadPoolExecutor(workers) as executor:
              res = executor.map(download_one, sorted(cc_list))
    
          return len(list(res))     
    
      if __name__ == '__main__':
          main(download_many)
    
      ####### output ########
      BD ID RU NG DE VN IN JP TR ET PK MX US CN CD FR IR EG PH BR 
      20 flags downloaded in 5.16s
    
  • 标准库中两个名为 Future 的类: concurrent.futures.Future 和 asyncio.Future 两个类的实例表示可能已经完成或者尚未完成的延迟计算

  • 期物:

    • 期物封装 待完成的操作, 可以放入队列, 完成状态可以查询, 得到结果后可以获取结果(或异常)
    • 期物通常不应自己创建, 只能由并发框架实例化. 原因: 期物表示终将发生的事情, 而确定某件事发生的唯一方式是执行的时间已经排定
    • 客户端代码不应带改变期物的状态, 并发框架在 期物表示的延迟计算 结束后会改变 期物的状态, 而我们无法控制计算何时结束
    • 两种期物 都有 .done() 方法, 返回布尔值, 表明期物连接的可调用对象是否已经执行. 同样的 .add_done_callback() 参数是一个可调用对象, 期物运行结束后会调用指定的可调用对象
  • flags_threadpool_ac.py

      ::Python
      def download_many(cc_list):
          cc_list = cc_list[:5]
          with futures.ThreadPoolExecutor(max_workers=3) as executor:
              to_do = []
              for cc in sorted(cc_list):
                  # submit() 会为传入的可调用对象排期, 并返回一个期物
                  future = executor.submit(download_one, cc)
                  to_do.append(future)
                  msg = 'Scheduled for {}: {}'
                  print(msg.format(cc, future))
              results = []
    
              # as_completed 参数是一个期物列表, 返回值是一个迭代器
              # 在期物运行结束后产出期物
              for future in futures.as_completed(to_do):
                  # result() 返回可调用对象的结果
                  res = future.result()
                  msg = '{} result: {!r}'
                  print(msg.format(future, res))
                  results.append(res)
          return len(results)
    
      Scheduled for BR: <Future at 0x10120aba8 state=running>
      Scheduled for CN: <Future at 0x1012116d8 state=running>
      Scheduled for ID: <Future at 0x101211c88 state=running>
      Scheduled for IN: <Future at 0x10121d240 state=pending>
      Scheduled for US: <Future at 0x10121d2e8 state=pending>
      BR ID <Future at 0x10120aba8 state=finished returned str> result: 'BR'
      <Future at 0x101211c88 state=finished returned str> result: 'ID'
      IN <Future at 0x10121d240 state=finished returned str> result: 'IN'
      US <Future at 0x10121d2e8 state=finished returned str> result: 'US'
      CN <Future at 0x1012116d8 state=finished returned str> result: 'CN'
    
      5 flags downloaded in 2.99s
    
  • 问题:

    • 问: Python 有 GIL 为什么 flags_threadpool.py 会比 flags.py 快 5 倍?
    • 答: I/O 密集型处理基本不会受到 GIL 的影响
    • 问: flags_asyncio.py 和 flags.py 都是单个线程, 前者为什么快 5 倍?
    • 答: 18.3

2. 阻塞型 I/O 和 GIL

  • Python 标准库中的所有阻塞型 I/O 函数都会释放 GIL
  • time.sleep() 函数也会释放 GIL
  • 所以 Python 线程不是一点用都没有

3. 使用 concurrent.futures 模块启动进程(从而 在 CPU 密集型作业中绕开 GIL)

  1. concurrent.futures 模块实现的是真正的并行计算(parallel tasks), 使用 ProcessPoolExecutor 类, 多进程, 适合处理 CPU 密集型任务

  2. ProcessPoolExecutor(进程) 与 ThreadPoolExecutor(线程) 区别

    • 都实现了 Executor 接口
    • ThreadPoolExecutor.__init__, 需要 max_workers 参数, 指定线程池中线程的数量, 而在 ProcessPoolExecutor 类中是可选的, 默认值是 os.cpu_count() 函数返回的 CPU 数量
    • 对 CPU 密集型处理, 不可能要求使用超过 CPU 数量的 workers(原文翻译的是职程)
    • 对于 I/O 密集型处理, 你可以通过 ThreadPoolExecutor 使用 10,100 或者1000 个线程, 取决于 事件本身, 和可用内存
  3. CPU 密集型任务测试方法: 使用 hashlib 模块, 实现 SHA-256 算法, 计算数组的 SHA-256 的散列值

4. 实验 Executor.map 方法(TODO)

5. 显示下载进度并处理错误(TODO)

6. 小结(TODO)

7. 延伸阅读(TODO)

  • CPU 密集处理建议:
    • multiprocessing
  • Go 与 Python
    • 不支持宏
    • 语法比 Python 简单
    • 不支持继承, 运算符重载

Chapter18 使用 asyncio 包处理并发 TODO

0. 本章要点

  • asyncio: 使用事件循环驱动的协程实现并发
  • 多线程和异步任务之间的关系
  • asyncio.Future 与 concurrent.futures.Future 之间的区别
  • 17 章 flags 异步版
  • 使用异步编程处理网络中高并发
  • 异步编程中, 协程性能好于回调
  • 将阻塞操作交给线程池从而避免阻塞事件循环
  • asyncio 版服务器
  • asyncio 的影响

1. 线程与协程对比(TODO)

  1. 区别: 0. 线程需要使用锁来保证数据安全, 协程不需要锁, 因为任意时刻只有一个协程在运行
    1. asyncio.Future 故意不阻塞
    2. 从期物, 任务和协程中产出

2. 使用 asyncio 和 aiohttp 下载(TODO)

3. 避免阻塞型调用(TODO)

4. 改进 asyncio 下载版本(TODO)

5. 从回调 到 期物和协程(TODO)

6. 使用 asyncio 编写服务器(TODO)

7. 本章小结(TODO)


第六部分 元编程


Chapter19 动态属性和特性

0. 本章要点

  • Python 中, 数据属性 和 处理数据的方法 统称 属性(attribute)
  • 特性(property)

1. 使用动态属性转换数据

  1. osconfeed.py, 下载个 JSON 文件来用

     ::python
     from urllib.request import urlopen
     import warnings
     import os
     import json
    
     URL =  'http://www.oreilly.com/pub/sc/osconfeed'
     JSON = 'data/osconfeed.json'
    
     def load():
         if not os.path.exists(JSON):
             msg = 'downloading {} to {}'.format(URL, JSON)
             warnings.warn(msg)
             with urlopen(URL) as remote, open(JSON, 'wb') as local:
                 local.write(remote.read())
    
         with open(JSON) as fp:
             return json.load(fp)
    
  2. 使用类似 JavaScript 中, 用属性访问值, feed['Schedule']['events'][40]['name'], 可以使用 feed.Schedule.events[40].name 获取, 定义一个 FrozenJSON 类来模仿上述效果

  3. 先看 FrozenJSON 的效果

     ::python
     >>> from osconfeed import load
     >>> raw_feed = load()
     >>> feed = FrozenJSON(raw_feed)  # 实例化, FrozenJSON 接受一个 JSON 对象
     >>> len(feed.Schedule.speakers)
     357
     >>> talk = feed.Schedule.events[40]
     >>> talk.name
     'There *Will* Be Bugs'
    
  4. FrozenJSON, explore0.py

     ::python
     from collections import abc 
    
     class FrozenJSON:
         """
         提供一个只读接口, 使用属性表示法 访问 JSON 类对象
         """
         def __init__(self, mapping):
             # mapping 的作用: 1.确保传入的是字典或能转换成字典 2.创建一个副本
             self.__data = dict(mapping)
    
         def __getattr__(self, name):
             print(type(name))
             # 如果是对象原来的属性就返回原来的属性
             if hasattr(self.__data, name):
                 return getattr(self.__data, name)
             else:
                 try: 
                     value = self.__data[name]
                 except  KeyError:
                     raise AttributeError('{} object has no attribute {}'.format(self.__class__.__name__, name))
                 else:
                     return FrozenJSON.build(value)
    
         @classmethod
         def build(cls, obj):
             print('obj',obj)
             # 如果是映射(字典), 构建一个 FrozenJSON 对象(迭代下去)
             if isinstance(obj, abc.Mapping):
                 return cls(obj)
             # 如果是列表
             elif isinstance(obj, abc.MutableSequence):
                 return [cls.build(item) for item in obj]
             # 如果是单纯的一个值
             else:
                 return obj
    
     g = FrozenJSON({"name":"wher", "age":12})
     print(g.name)
    
  5. 处理无效属性名(属性名为关键字), explore1.py

     ::python
     import keyword
     def __init__(self, mapping):
         self.__data = {}
         for key, value in mapping.items():
             # 或者 Python3 中 str.isidentifier()
             if keyword.iskeyword(key):
                 key += '_'
             self.__data[key] = value
    
  6. __new__ 方法创建对象的灵活性

    • 其他语言所谓的构造方法, 其实是__new__

    • 类方法, 但不用 @classmethod 装饰器

    • 必须返回一个实例, 以作为 __init__ 方法的参数

    • __new__ 也可以返回其他类的实例, 这样解释器不会调用__init__方法了

    • code:

        ::python
        # 构建对象的伪代码
        def object_maker(the_class, some_arg):
            new_object = the_class.__new__(some_arg)
            if isinstance(new_object, the_class):
                the_class.__init__(new_object, some_arg)
            return new_object
      
        x = Foo('bar') 
        # 就相当于:
        x = object_maker(Foo, 'bar')
      
  7. explore2.py, 使用 __new__ 方法取代 build方法, 构建可能是也可能不是 FrozenJSON 实例的新对象

     ::Python
     class FrozenJSON:
         """
         提供一个只读接口, 使用属性表示法 访问 JSON 类对象
         """
         def __new__(cls, arg):
             if isinstance(arg, abc.Mapping):
                 # 调用 object.__new__(FrozenJSON), 
                 return super().__new__(cls)
             elif isinstance(arg, abc.MutableSequence):
                 return [cls(item) for item in arg]
             else:
                 return arg
    
         def __init__(self, mapping):
             # mapping 的作用: 1.确保传入的是字典或能转换成字典 2.创建一个副本
             self.__data = dict(mapping) # 转换成字典
    
         def __getattr__(self, name):
             print(type(name))
             # 如果是对象原来自带的属性就返回原来的属性
             if hasattr(self.__data, name):
                 return getattr(self.__data, name)
             else:
                 try: 
                     value = self.__data[name]
                 except  KeyError:
                     raise AttributeError(
                         '{} object has no attribute {}'.format(self.__class__.__name__, name))
                 else:
                     # return FrozenJSON.build(value)
                     return FrozenJSON(self.__data[name])
    
  8. 使用 shelve 模块调整 OSCON 数据源的结构(TODO)

    1. 目的: 访问编号, 得到对象(类似于外键)

    2. shelve.open 高阶函数, 返回 shelve.Shelf 实例

      • 是 abc.MutableMapping 的子类, 可以处理映射
      • shelve.Shelf 类有可以管理 I/O 的方法, sync, close, 也是上下文管理器
      • 保存键值对的方法, 新值赋予键
      • 键必须是字符串
      • 值必须是可以 pickle 的对象
    3. schedule1.py

       ::python
       import warnings
       import osconfeed
      
       DB_NAME = 'data/schedule1_db'
       CONFERENCE =  'conference.115'
      
       class Record:
           def __init__(self, **kwargs):
               #  更新 __dict__ 字典, 能够快速更新属性
               self.__dict__.update(kwargs)
      
       def load_db(db):
           raw_data = osconfeed.load()
           warnings.warn('loading ' + DB_NAME)
           for collection, re_list in raw_data['Schedule'].items():
               record_type = collection[:-1] # 去除末尾字符
               for record in rec_list:
                   key = '{}.{}'.format(record_type, record['serial'])
                   record['serial'] = key
                   db[key] = Record(**record)
      
                   # record 字典实例样式:
                   # {
                   #     'serial': 'conference.115',
                   #     ...
                   #     'serial': 'event.33457'
                   # }
      
    4. 使用 schedule1.py

       ::python
       In [10]: db = shelve.open(DB_NAME)
      
       In [11]: if CONFERENCE not in db:
           ...:     load_db(db)
           ...:
       /Library/Frameworks/Python.framework/Versions/3.5/bin/ipython:15: UserWarning: loading data/schedule1_db
      
       In [12]: speaker = db['speaker.3471']
       In [13]: type(speaker)
       Out[13]: __main__.Record
      
       In [15]: from schedule1 import *
      
       In [16]: speaker.name
       Out[16]: 'Anna Martelli Ravenscroft'
       In [17]: db.close()
      
    5. Record 类与 FrozenJSON 类的区别, 即 Record 类没有的功能:

      • FrozenJSON 类需要递归转换嵌套的映射和列表
      • FrozenJSON 类需要访问内嵌__data属性
  9. 使用特性获取链接记录(schedule2.py)

    1. 实现目的: 扩展 schedule1.py, 从 event 中的编号获取对象全部, 实现类似于外键, 但得到的不是键, 是链接的模型对象

    2. doctest:

       ::python
       # 测试的 event:
             # {
             #   "serial": 33950,
             #   "name": "There *Will* Be Bugs",
             #   "event_type": "40-minute conference session",
             #   "time_start": "2014-07-23 14:30:00",
             #   "time_stop": "2014-07-23 15:10:00",
             #   "venue_serial": 1449,
             #   "description": "If you\u0026#39;re pushing the envelope of programming (or of your own skills)... and even when you’re not... there *will* be bugs in your code.  Don\u0026#39;t panic!  We cover the attitudes and skills (not taught in most schools) to minimize your bugs, track them, find them, fix them, ensure they never recur, and deploy fixes to your users.\r\n",
             #   "website_url": "https://conferences.oreilly.com/oscon/oscon2014/public/schedule/detail/33950", 
             #   "speakers": [3471,5199],
             #   "categories": [
             #     "Python"
             #   ]
             # },
      
       In [3]: import shelve
      
       In [4]: db = shelve.open(DB_NAME)
       In [5]: DbRecord.set_db(db)
      
       In [7]: if CONFERENCE not in db:
          ...:     load_db(db)
          ...:
      
       In [8]: event = DbRecord.fetch('event.33950')
      
       In [9]: event
       Out[9]: <Event 'There *Will* Be Bugs'>
      
       In [10]: repr(event)
       Out[10]: "<Event 'There *Will* Be Bugs'>"
      
       In [11]: event.venue
       Out[11]: <DbRecord serial='venue.1449'>
      
       In [12]: event.venue.name
       Out[12]: 'Portland 251'
       In [17]: for spkr in event.speakers:
           ...:     print('{0.serial}: {0.name}'.format(spkr))
           ...:
       speaker.3471: Anna Martelli Ravenscroft
       speaker.5199: Alex Martelli
       In [18]: db.close()
      
    3. DbRecord, Record 的子类, 添加了__db类属性, 用于设置和获取__db属性的set_dbget_db静态方法, 用于从数据库中获取记录的 fetch 类方法, 以及辅助测试和调试的__repr__实例方法

    4. Event, DbRecord 类的子类, 添加了用于获取所链接记录的 venue 和 speakers 属性, 以及特殊的__repr__方法

    5. 特性是用于管理实例属性的类属性

    6. schedule2.py

       ::python
       import warnings
       import inspect
       import osconfeed
      
       DB_NAME = 'data/schedule2_db'
       CONFERENCE =  'conference.115'
      
       class Record:
           def __init__(self, **kwargs):
               #  更新 __dict__ 字典, 能够快速更新属性
               self.__dict__.update(kwargs)
           # 用于测试
           def __eq__(self, other):
               if isinstance(other, Record):
                   return self.__dict__ == other.__dict__
               else:
                   return NotImplemented
      
       class MissingDatabaseError(RuntimeError):
           """需要数据库但没有指定数据库时抛出"""
           # 自定义的异常通常是标志类, 没有定义体, 写 """xxx""" 比写 pass 好
      
       class DbRecord(Record):
           __db = None
      
           # 静态方法, 参数没有 实例,类, 不过调用多少次都是一样的, 
           # 用子类 Event 调用, __db 也是在 DbRecord 中 
           @staticmethod 
           def set_db(db):
               DbRecord.__db = db
      
           @staticmethod   # 返回值始终是 DbRecord.__db
           def get_db():
               return DbRecord.__db
      
           # 类方法: 子类可以定制
           @classmethod
           def fetch(cls, ident):
               db = cls.get_db()
               try:
                   return db[ident]
               except TypeError:
                   if db is None:
                       msg = "database not set; call '{}.set_db(my_db)'"
                       raise MissingDatabaseError(msg.format(cls.__name))
                   else:
                       raise
      
           def __repr__(self):
               if hasattr(self, 'serial'):
                   cls_name = self.__class__.__name__
                   return '<{} serial={!r}>'.format(cls_name, self.serial)
               else:
                   return super().__repr__()
      
       class Event(DbRecord):
      
           @property
           def venue(self):
               key = 'venue.{}'.format(self.venue_serial)
               return self.__class__.fetch(key)
               # 不用 self.fetch(key) 的原因: Event 实例有可能键为 fetch
      
           @property
           def speakers(self):
               if not hasattr(self, '_speaker_objs'):
                   spkr_serials = self.__dict__['speakers']
                   fetch = self.__class__.fetch
                   self._speaker_objs = [fetch('speaker.{}'.format(key))
                                         for key in spkr_serials]
      
               return self._speaker_objs
      
           def __repr__(self):
               if hasattr(self, 'name'):
                   cls_name = self.__class__.__name__
                   return '<{} {!r}>'.format(cls_name, self.name)
               else:
                   return super().__repr__()
      
       def load_db(db):
           raw_data = osconfeed.load()
           warnings.warn('loading ' + DB_NAME)
           for collection, rec_list in raw_data['Schedule'].items():
               record_type = collection[:-1] # 去除末尾字符
               cls_name = record_type.capitalize()
               # 从全局作用域中获取 cls_name 所对应的对象, 如果找不到就使用 DbRecord
               cls = globals().get(cls_name, DbRecord)
               if inspect.isclass(cls) and issubclass(cls, DbRecord):
                   factory = cls  
               else:
                   factory = DbRecord
      
               for record in rec_list:
                   key = '{}.{}'.format(record_type, record['serial'])
                   record['serial'] = key
                   # db[key] = Record(**record) schedule1.py
                   db[key] = factory(**record)
      

2. 使用特性(@property)验证属性, 使用@property实现可写特性

  1. 散装商品, bulkfood_v1.py, 无法限制属性设置, 例如数量为负

     ::python
     class LineItem:
    
         def __init__(self, description, weight, price):
             self.description = description
             self.weight = weight
             self.price = price
    
         def subtotal(self):
             return self.weight * self.price
    
  2. bulkfood_v2.py, 使用 property 限制属性的读写, 且保持接口不变

    • bulkfood_v2 只是限制 weight, 如果 price 也限制, 代码就会重复

    • 去除重复的方法: 抽象

      1. 使用特性工厂函数, @property
      2. 描述符
    • bulkfood_v2.py

        ::Python
        class LineItem:
      
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight
                self.price = price
      
            def subtotal(self):
                return self.weight * self.price
      
            @property
            def weight(self):
                return self.__weight
      
            @weight.setter
            def weight(self, value):
                if value > 0:
                    self.__weight = value
                else:
                    raise  ValueError('value must be > 0')
      

3. 特性 property 全解析

  1. property

    • property 是一个类
    • 调用 构造方法 和调用 工厂函数 没有区别, 只要能返回新的可调用对象, 代替被装饰的函数, 二者都可以用作装饰器
    • property, 构造方法签名:
      • property(fget=None, fset=None, fdel=None, doc=None
  2. bulkfood_v2b.py, property 非装饰器版本

     ::Python
     class LineItem:
    
         def __init__(self, description, weight, price):
             self.description = description
             self.weight = weight
             self.price = price
    
         def subtotal(self):
             return self.weight * self.price
    
         def get_weight(self):
             return self.__weight
    
         def set_weight(self, value):
             if value > 0:
                 self.__weight = value
             else:
                 raise ValueError('value must be > 0')
    
         weight = property(get_weight, set_weight)
    
         # @property
         # def weight(self):
         #     return self.__weight
    
         # @weight.setter
         # def weight(self, value):
         #     if value > 0:
         #         self.__weight = value
         #     else:
         #         raise  ValueError('value must be > 0')
    
  3. 特性 property 会覆盖实例属性

    • 总结: 实例属性(obj.attr) 不会从 obj, 开始寻找 attr, 而是从 obj.__class__, 开始, 而且, 仅当类中没有名为 attr 的特性时, Python 才会从 obj 中找, (特性其实是覆盖的描述符)

    • property 是类属性, 但是管理的是实例属性

    • 普通情况下, 实例属性会覆盖类属性

        ::Python
        In [4]: class Class:
           ...:     data = 'class data attr'
      
           ...:     @property
           ...:     def prop(self):
           ...:         return 'the prop value'
           ...:
        In [5]: obj = Class()
      
        In [6]: vars(obj)
        Out[6]: {}
      
        In [7]: obj.data
        Out[7]: 'class data attr'
      
        In [8]: obj.data = 'bar'
      
        In [9]: vars(obj)
        Out[9]: {'data': 'bar'}
      
        In [10]: obj.data
        Out[10]: 'bar'
      
        In [11]: Class.data
        Out[11]: 'class data attr'
      
    • 而 property 则不会

        ::Python
        In [17]: Class.prop
        Out[17]: <property at 0x1045c93b8>
      
        In [18]: obj.prop
        Out[18]: 'the prop value'
      
        In [19]: obj.prop = '实例属性' # 无法设置 property 实例属性
        ---------------------------------------------------------------------------
        AttributeError                            Traceback (most recent call last)
        <ipython-input-19-854f59eccc74> in <module>()
        ----> 1 obj.prop = '实例属性'
      
        AttributeError: can't set attribute
      
        In [20]: obj.__dict__['prop'] = 'foo'
      
        In [21]: vars(obj)
        Out[21]: {'data': 'bar', 'prop': 'foo'}
      
        In [22]: obj.prop 
        Out[22]: 'the prop value'
        # 在实例的 `__dict__` 中设置属性, 仍然被 property 属性覆盖, 无法读取
              
        # property 对象被销毁了, 实例对象就可读取了
        In [23]: Class.prop = '新 property 属性'
      
        In [24]: obj.prop
        Out[24]: 'foo'
      
        In [25]: Class.prop
        Out[25]: '新 property 属性'
      
    • 新增 property 属性, 会覆盖实例属性

        ::python
        In [26]: obj.data
        Out[26]: 'bar'
      
        In [27]: Class.data
        Out[27]: 'class data attr'
      
        In [28]: Class.data = property(lambda self: '设置 property 新值')
      
        In [30]: obj.data
        Out[30]: '设置 property 新值'
      
        In [31]: del Class.data # 删完了, 实例属性又可以访问了
      
        In [32]: obj.data
        Out[32]: 'bar'
      
  4. property(特性) 的文档

    • help() 函数会从__doc__中提取
    • 为 property 设置 doc:
      • weight = property(get_weight, set_weight, doc='weight in kilograms')

      • 如果类名为 Foo, help(Foo) 会多出:

          ::Python
           |  ----------------------------------------------------------------------
           |  Data descriptors defined here:
           |
           |  __dict__
           |      dictionary for instance variables (if defined)
           |
           |  __weakref__
           |      list of weak references to the object (if defined)
           |
           |  weight
           |      weight in kilograms
          (END)
        

4. 定义一个特性工厂函数,

  1. quantity(翻译: 量), 属性不能为 0 或 负数

    • 工厂函数: 源于设计模式, python核心编程:工厂函数看上去有点像函数,实质上他们是类,当你调用它们时,实际上是生成了该类型的一个实例,就像工厂生产货物一样.

    • 所以这里的特性工厂函数就是, 根据响应参数, 返回一个 property 特性

    • bulkfood_v2prop.py

        ::python
        class LineItem:
            # 使用 特性工厂函数 quantity 将 property weight 设置为类属性
            weight = quantity('weight')
            price = quantity('price')
      
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight # property 激活, 确保不能为负
                self.price = price
      
            def subtotal(self):
                return self.weight * self.price
      
    • 需要改进的地方 weight = quantity('weight'), 要输入两次 weight

      • 因为赋值语句右边先计算, 需要先初始化才能赋值
      • 解决方法: 类装饰器或元类, 20 章, 21 章
  2. bulkfood_v2prop.py

     ::python
     def quantity(storage_name):
         # instance 不是 self 的原因: 
         # qty_getter 不在类定义体中, instance 代表把属性存到 LineItem 实例中
         def qty_getter(instance):
             return instance.__dict__[storage_name]
    
         def qty_setter(instance, value):
             if value > 0:
                 instance.__dict__[storage_name] = value
             else:
                 raise ValueError('value must be > 0')
         return property(qty_getter, qty_setter)
         # 使用 闭包, 将 storage_name 封闭在上下文中, 再次运行都会读取
    
     class LineItem:
         # 设置为类属性
         weight = quantity('weight')
         price = quantity('price')
    
         def __init__(self, description, weight, price):
             self.description = description
             self.weight = weight
             self.price = price
    
         def subtotal(self):
             return self.weight * self.price
    
     In [43]: nutmeg = LineItem('Moluccan nutmeg', 8, 13.95) # numeg(肉豆蔻)
     In [44]: nutmeg.weight, nutmeg.price
     Out[44]: (8, 13.95)
    
     In [47]: sorted(vars(nutmeg).items())
     Out[47]: [('description', 'Moluccan nutmeg'), ('price', 13.95), ('weight', 8)]
    

5. 处理属性删除操作

  • blackknight.py, 除了使用 deleter, 还可以使用__delattr__

      ::python
      class BlackKnight:
          def __init__(self):
              self.members = ['an arm', 'another arm', 
                              'a leg', 'another leg']
              self.phrases = ["'Tis but a scratch.",
                              "It's just a flesh wound.",
                              "I'm invincible!", # 不可侵犯
                              "All right, we'll call it a draw."]  
                              # 好吧, 算是平局!             
    
          @property
          def member(self):
              print('next member is:')
              return self.members[0]
    
          @member.deleter
          def member(self):
              text = 'BLACK KNIGHT (loses {})\n-- {}'
              print(text.format(self.members.pop(0), self.phrases.pop(0)))
    
      In [49]: knight = BlackKnight()
    
      next member is:
      next member is:
      In [50]: knight.member
      next member is:
      Out[50]: 'an arm'
    
      In [51]: knight.member
      next member is:
      Out[51]: 'an arm'
    
      In [52]: del knight.member
      BLACK KNIGHT (loses an arm)
      -- 'Tis but a scratch
    
      In [53]: knight.member
      next member is:
      Out[53]: 'another arm'
    
      In [54]: del knight.member
      BLACK KNIGHT (loses another arm)
      -- It's just a flesh wound.
    
      In [55]: del knight.member
      BLACK KNIGHT (loses a leg)
      -- I'm invinceble!
    
      In [56]: del knight.member
      BLACK KNIGHT (loses another leg)
      -- All right, we'll call it a draw.
    

6. 处理重要属性和函数

  1. 和属性有关的特殊属性, 魔法方法
    • __class__, 对象所属类的引用(即obj.__class__type(obj)作用相同), 某些特殊方法__getattr__只在对象的类中寻找, 不在实例中寻找
    • __dict__, 映射(dict), 存储对象或类的可写属性, 有__dict__属性的对象, 任何时候都能设置新属性, 如果类有__slots__属性, 其实例可能就没有__dict__属性
    • __slots__, 字符串组成的元组, 作用是限制类有哪些属性, 指明允许有的属性
  2. 处理属性的内置函数
    • dir([object]), 列出大部分属性, 不包括例如__mro__, __bases__, __name__, 不指定对象, 则列出当前作用域中的名称
    • getattr(object, name[, default]), object 对应名为 name 属性, 获取属性可能来自对象所属的类或超类, 没有找到的话, 抛出 AttributeError, 或者 返回 default 参数的值
    • hasattr(object, name), 如果 object 对象中存在指定属性, 或者能够以某种方式获取指定的属性(例如继承), 返回 True, 实现方式: 调用 getattr(object, name), 看是否抛出 AttributeError 异常
    • setattr(object, name, value), 设置属性, 创建新属性或者覆盖旧属性
    • vars([object])
      • 返回 object 对象__dict__属性,
      • 若 object 所属的类定义了__slots__, 且 object 没有__dict__属性, 则会报错, vars() argument must have __dict__ attribute
      • 若没有指定参数, 则与locals()函数一样, 表示本地作用域的字典
  3. 处理属性的特殊方法
    • 直接操作__dict__来读写属性, 不会触发 getattr, hasattr, setattr 函数
    • 触发__getattribute__: obj.attr点号获取 和 getattr(obj, 'attr', 42)内置函数
    • __delattr__: 使用 del 删除属性
    • __dir__: dir(obj)
    • __getattr__: 仅当在 obj, Class, 超类中找不到指定的属性时会调用
    • __getattribute__: 优先级高, 点号, getattr, hasattr 都会触发, 当此方法触发 AttributeError 异常时, 才会调用__getattr__, 为了在获取 obj 实例属性时不导致无限递归, __getattribute__方法的实现要使用 super().__getattribute__(obj, name)
    • __setattr__(self, name, value): 点号和 setattr 会触发

7.本章小结

pass

8. 延伸阅读

pass

Chapter20 属性描述符

0. 本章要点

  • 描述符是对多个属性运用相同存取逻辑的一种方式, 是实现了特定协议的类, 这个协议包括 __get__, __set____delete__, 实现其中一种就可以
  • 用法: 创建一个实例, 作为另一个类的类属性
  • 作用: 简单的说描述符会改变对象属性的获取、设置和删除方式。(访问属性不再从__dict__ 中获取, 而是调用描述符的__get__ 方法)
  • 应用:
    • 装饰器, 属性, 绑定和非绑定方法, 静态方法, 类方法, __slots__
    • Django 的 paginator.py 模块
    • Django ORM, SQL Alchelmy 等 ORM 中的字段类型是描述符, 把数据库中字段里的数据与 Python 对应的属性对应起来
  • 其他:

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting witha.dict[‘x’], then type(a).dict[‘x’], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.—–摘自官方文档

1. 描述符示例: 验证属性

  • 避免重复编写读写方法和设值方法的解决方法
    1. 以面向对象方式: 描述符
    2. 上一章中, 以函数式编程模式: 特性工厂函数, 高阶函数, 在闭包中存储 storage_name 等设置, 由参数决定创建哪些存取函数, 再使用存取函数构建一个特性实例
  1. LineItem 类第 3 版: 一个简单的描述符

    • Quantity 描述符类, 实现了__set__

    • LineItem 类(托管类)有两个属性, weight 和 price 属性, 使用描述符 Quantity 类的实例 来实现, weight 和 price 分别有两个, 一个类属性一个是实例属性

    • 定义:

      • 描述符类,
        • 实现描述符协议的类, Quantity 类
      • 托管类,
        • 把描述符声明为类属性的类, LineItem 类
      • 描述符实例,
        • 描述符类的各个实例, 托管类的类属性, weight, price
      • 托管实例,
        • 托管类的实例, LineItem 的实例
      • 储存属性,
        • 托管实例中存储自身托管属性的属性, LineItem 实例的 weight, 和 price 属性
      • 托管属性
        • 托管类中由描述符实例处理的公开属性
    • Quantity 实例是 LineItem 类的类属性

    • Quantity.__set__ 中, self 是描述符实例, instance 是托管实例, 管理实例属性的描述符应该把值存储在托管实例中, 因此, Python 才为描述符中的那个方法(__set__)提供了 instance 参数

    • bulkfood_v3.py:

        ::Python
        class Quantity:
            def __init__(self, storage_name):
                self.storage_name = storage_name
      
            def __set__(self, instance, value):
                # self 是描述符实例(即LineITem.weight 或 LineItem.price)
                # instance 是托管实例
                if value > 0:
                    instance.__dict__[self.storage_name] = value
                    # 如果使用 setattr 函数会再次书法 __set__ 方法, 导致无限递归
                else:
                    raise ValueError('value must be > 0')
      
        class LineItem:
            # 绑定描述符给 weight 属性
            weight = Quantity('weight') 
            price = Quantity('price')
      
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight
                self.price = price
      
            def subtotal(self):
                return self.weight * self.price
      
    • LineItem 类中, weight = QUantity('weight') 麻烦而且容易出错, 不能使用 weight = Quantity() 的原因是, 先计算赋值等式右边—创建描述符实例, 此时必须要指定属性名称, 下一节是解决这一问题的简单方法, 优雅的解决方法是 21 章类装饰器或者元类

  2. LineItem 类第 4 版: 自动获取储存属性的名称,

    • 其实就是把属性名称用数字代替(类似主键)换上, 实例化一次, 主键自增 1

    • bulkfood_v4.py

        ::Python
        class Quantity:
            __counter = 0 # 统计 Quantity 实例的数量
      
            def __init__(self):
                cls = self.__class__ # cls 是 Quantity 类的引用
                prefix = cls.__name__
                index = cls.__counter
                # 每个描述符实例的 storage_name 都是独一无二
                self.storage_name = '_{}#{}'.format(prefix, index)
                cls.__counter += 1
      
            def __get__(self, instance, owner):
                print(self.storage_name)
                if instance is None: 
                    return self # 如果不是通过实例调用, 返回描述符自身
                else:
                    return getattr(instance, self.storage_name)
      
            def __set__(self, instance, value):
                # self 是描述符实例(即LineITem.weight 或 LineItem.price)
                # instance 是托管实例
                if value > 0:
                    setattr(instance, self.storage_name, value)
                    # instance.__dict__[self.storage_name] = value
                    # 这里不会造成 无限递归的原因: 托管属性 和 储存属性的名称不同
                    # 即描述符实例 set/get 的属性不同于托管实例 set/get 的属性
                else:
                    raise ValueError('value must be > 0')
      
        class LineItem:
            weight = Quantity() 
            price = Quantity()
      
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight
                self.price = price
      
            def subtotal(self):
                return self.weight * self.price
      
        truffle = LineItem('White truffle', 100, 1)
        print(truffle.weight, truffle.price)
        # output:
        _Quantity#0
        _Quantity#1
        100 1
      
    • bulkfood_v4c.py, 将描述符类拆分出来, 类似 Django model 的用法, Django 的模型字段就是描述符

        ::Python    
        import model_v4c as model
      
        class LineItem:
            weight = model.Quantity() 
            price = model.Quantity()
      
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight
                self.price = price
      
            def subtotal(self):
                return self.weight * self.price
      
    • bulkfood_v4prop.py, 使用特性工厂函数, 解决 weight = Quantity('weight'), weight 出现两次的问题

        ::Python
        # property 工厂函数
        def quantity(storage_name):
      
            try:
                quantity.counter += 1
            except AttributeError:
                quantity.counter = 0
            storage_name = '_{}:{}'.format('quantiry', quantity.counter)
      
            # instance 不是 self 的原因: 
            # qty_getter 不在类定义体中, instance 代表把属性存到 LineItem 实例中
            def qty_getter(instance):
                # return instance.__dict__[storage_name] 使用 getattr 和 setattr
                return getattr(instance, storage_name)
      
            def qty_setter(instance, value):
                if value > 0:
                    # instance.__dict__[storage_name] = value
                    setattr(instance, storage_name, value)
                else:
                    raise ValueError('value must be > 0')
            return property(qty_getter, qty_setter)
            # 使用 闭包, 将 storage_name 封闭在上下文中, 再次运行都会读取
      
    • 特性工厂函数 与 描述符类比较

      • 特性工厂函数使用: 闭包原理, 局部变量为 storage_name, 使用闭包保持状态, 缺点: 要重用, 只能复制粘贴;

      • 描述符: 使用子类扩展, 易重用扩展, 易于理解;

          ::Python
          def quantity():
              try:
                  quantity.counter += 1
              except AtrributeError:
                  quantity.counter = 0
        
              storage_name = '_{}:{}'.format('quantity', quantity.counter)
        
              def qty_getter(instance):
                  return getattr(instance, storage_name)
        
              def qty_settter(instance, value):
                  if value > 0:
                      setattr(instance, storage_name, value)
                  else:
                      raise ValueError('value must be > 0')
        
              return property(qty_getter, qty_setter)
        
  3. LineItem 类第 5 版: 新型描述符(TODO)

2. 覆盖型与非覆盖型描述符对比

  1. 实现__set__方法的是覆盖型描述符, 反之是非覆盖型描述符

  2. 覆盖型描述符, descriptorkinds.py

     ::python
     # 辅助函数
     def cls_name(obj_or_cls):
         cls = type(obj_or_cls)
         if cls is type:
             cls = obj_or_cls
         return cls.__name__.split('.')[-1]
    
     def display(obj):
         cls = type(obj)
         if cls is type:
             return '<class {}>'.format(obj.__name__)
         elif cls in [type(None), int]:
             return repr(obj)
         else:
             return repr(obj)
    
     def print_args(name, *args):
         pseudo_args = ', '.join(display(x) for x in args)
         print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
    
     # 重要的类:
     class Overriding:
         """ 数据描述符或强制描述符, 典型的覆盖型描述符"""
         def __get__(self, instance, owner):
             print_args('get', self, instance, owner)
    
         def __set__(self, instance, value):
             print_args('set', self, instance, value)
    
     class OverridingNoGet:
         """ 没有__get__方法的覆盖型描述符"""
         def __set__(self, instance, value):
             print_args('set', self, instance, value)
    
     class NonOverriding:
         """ 非数据描述符或遮盖型描述符, 没有 __set__, 非覆盖型描述符"""
         def __get__(self, instance, owner):
             print_args('get', self, instance, owner)
    
     class Managed:
         """ 托管类 """
         over = Overriding()
         over_no_get = OverridingNoGet()
         non_over = NonOverriding()
    
         def spam(self):
             print('-> Managed.spam({})'.format(display(self)))
    
  3. 交互模式下测试 descriptorkinds.py

     ::python
     In [4]: obj = Managed
    
     In [5]: obj = Managed()
    
     In [7]: obj.over
     -> Overriding.__get__(<descriptorkinds.Overriding object at 0x104484438>, <descriptorkinds.Managed object at 0x104572978>, <class Managed>)
    
     In [8]: Managed.over
     -> Overriding.__get__(<descriptorkinds.Overriding object at 0x104484438>, None, <class Managed>)
    
     In [9]: obj.over = 7
     -> Overriding.__set__(<descriptorkinds.Overriding object at 0x104484438>, <descriptorkinds.Managed object at 0x104572978>, 7)
    
     In [10]: obj.over
     -> Overriding.__get__(<descriptorkinds.Overriding object at 0x104484438>, <descriptorkinds.Managed object at 0x104572978>, <class Managed>)
    
     In [11]: obj.__dict__['over'] = 8
    
     In [12]: obj.over
     -> Overriding.__get__(<descriptorkinds.Overriding object at 0x104484438>, <descriptorkinds.Managed object at 0x104572978>, <class Managed>)
    
     In [13]: vars(obj)
     Out[13]: {'over': 8}
    
    • [8] 直接用托管类触发描述符的__get__方法, instance 是 None
    • 不管是直接赋值[9]还是通过obj.__dict__赋值[11], obj.over 都会被 Managed.over 描述符覆盖
  4. 没有__get__方法的覆盖型描述符

     ::Python
     In [16]: obj.over_no_get
     Out[16]: <descriptorkinds.OverridingNoGet at 0x1044845f8>
    
     In [17]: Managed.over_no_get
     Out[17]: <descriptorkinds.OverridingNoGet at 0x1044845f8>
    
     In [18]: obj.over_no_get = 7
     -> OverridingNoGet.__set__(<descriptorkinds.OverridingNoGet object at 0x1044845f8>, <descriptorkinds.Managed object at 0x104572978>, 7)
    
     In [19]: obj.over_no_get
     Out[19]: <descriptorkinds.OverridingNoGet at 0x1044845f8>
    
     In [20]: obj.__dict__['over_no_get'] = 9
    
     In [21]: obj.over_no_get
     Out[21]: 9
    
     In [22]: obj.over_no_get = 7
     -> OverridingNoGet.__set__(<descriptorkinds.OverridingNoGet object at 0x1044845f8>, <descriptorkinds.Managed object at 0x104572978>, 7)
    
     In [23]: obj.over_no_get
     Out[23]: 9
    
    • obj 直接赋值时, 会被描述符__set__覆盖, 而通过__dict__赋值会覆盖描述符,
    • 即, 写操作经由描述符处理, 读取时实例属性会覆盖描述符
  5. 非覆盖型描述符(没有__set__)

     ::python
     In [24]: obj = Managed()
    
     In [25]: obj.non_over
     -> NonOverriding.__get__(<descriptorkinds.NonOverriding object at 0x1044849e8>, <descriptorkinds.Managed object at 0x104770908>, <class Managed>)
    
     In [26]: obj.non_over = 7
    
     In [27]: obj.non_over
     Out[27]: 7
    
     In [28]: Managed.non_over
     -> NonOverriding.__get__(<descriptorkinds.NonOverriding object at 0x1044849e8>, None, <class Managed>)
    
     In [30]: del obj.non_over
    
     In [31]: obj.non_over
     -> NonOverriding.__get__(<descriptorkinds.NonOverriding object at 0x1044849e8>, <descriptorkinds.Managed object at 0x104770908>, <class Managed>)
    
    • [27] obj 有实例属性 non_over, 把 Managed 类的同名描述符遮盖了
    • [30][31] 删除实例属性 non_over 后, 读取 obj.non_over 会触发类中描述符的__get__方法, instance 参数是托管实例
  6. 在类中覆盖描述符

    1. 不管描述符是不是覆盖型, 类属性赋值都能覆盖描述符;

       ::python
       In [30]: del obj.non_over
      
       In [32]: Managed.over = 1
      
       -> NonOverriding.__get__(<descriptorkinds.NonOverriding object at 0x1044849e8>, None, <class Managed>)
       In [33]: Managed.over_no_get = 2
      
       In [34]: Managed.non_over = 3
      
       In [35]: obj.over, obj.over_no_get, obj.non_over
       Out[35]: (1, 2, 3)
      

3. 方法即描述符

  1. 方法是非覆盖型描述符

    • 用户定义的函数都有__get__, 又属于绑定方法(bound method), 相当于一个描述符绑定到类上

    • obj.spam 和 Managed.spam 是不同的对象, 原因:

      • 通过托管类访问时[37], 函数的__get__方法会返回自身的引用
      • 通过实例访问时[36], 函数的__get__方法返回的是绑定方法的对象: 一种可调用的对象, 里面包装着函数, 并把托管实例绑定给函数的第一个参数(self), 同 functools.partial 函数
    • 没有实现__set__方法, 是非覆盖型描述符[38][39]

        ::Python
        In [36]: obj.spam
        Out[36]: <bound method Managed.spam of <descriptorkinds.Managed object at 0x104770908>>
      
        In [37]: Managed.spam
        Out[37]: <function descriptorkinds.Managed.spam>
      
        In [38]: obj.spam = 7
      
        In [39]: obj.spam
        Out[39]: 7
      
        class Managed:
            """ 托管类 """
            over = Overriding()
            over_no_get = OverridingNoGet()
            non_over = NonOverriding()
      
            def spam(self):
                print('-> Managed.spam({})'.format(display(self)))
      
  2. method_is_descriptor.py

     ::python
     import collections
    
     class Text(collections.UserString):
    
         def __repr__(self):
             return 'Text({!r})'.format(self.data)
    
         def reverse(self):
             return self[::-1]
    
  3. 测试 method_is_descriptor.py

     ::Python
     In [46]: from method_id_descriptor import *
    
     In [47]: word = Text('forward')
    
     In [48]: word
     Out[48]: Text('forward')
    
     In [49]: word.reverse()
     Out[49]: Text('drawrof')
    
     In [50]: Text.reverse(Text('backward'))
     Out[50]: Text('drawkcab')
    
     In [51]: type(Text.reverse), type(word.reverse)
     Out[51]: (function, method)
     # 类方法类型是 function, 实例方法类型是 method
    
     In [52]: list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')]))
     Out[52]: ['diaper', (30, 20, 10), Text('desserts')]
    
     In [53]: Text.reverse.__get__(None, Text)
     Out[53]: <function method_id_descriptor.Text.reverse>
    
     In [54]: Text.reverse.__get__(word)
     Out[54]: <bound method Text.reverse of Text('forward')>
    
     In [55]: word.reverse
     Out[55]: <bound method Text.reverse of Text('forward')>
    
     In [56]: word.reverse.__self__   
     Out[56]: Text('forward')
     # 绑定方法对象__self__属性是对实例的引用
    
     In [57]: word.reverse.__func__ is Text.reverse
     Out[57]: True
     # 绑定方法的__func__属性是依附在托管类上那个原始函数的引用
    
     In [58]: word.reverse.__call__
     Out[58]: <method-wrapper '__call__' of method object at 0x103d65bc8>
     # 此方法会调用__func__属性引用的原始函数, 函数的第一个参数是其__self__属性
     # 这就是形参 self 的隐式绑定方式
    

4. 描述符用法建议

  1. 使用特性 property 保持简单
  2. 只读描述符必须有__set__方法,
  3. 用于验证的描述符可以只有__set__方法
  4. 仅有__get__方法的描述符可以实现高度缓存
  5. 非特殊的方法可以被实例属性遮盖

5. 描述符的文档字符串和覆盖删除操作(TODO)

6. 本章小结

  • 术语:
    • 覆盖型描述符 == 数据描述符(data descriptor) == 强制描述符
    • 非覆盖型描述符 == 非数据描述符(non data descriptor) == 遮盖型描述符

Chapter21 类元编程(TODO)