【代码】Fluent Python

14.1 Sentence类第1版:单词序列
从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法是:调用 iter(x) 函数,如果不可迭代,再处理 TypeError 异常。这比使用 isinstance(x, abc.Iterable) 更准确,因为 iter(x) 函数会考虑到遗留的__getitem__方法,而 abc.Iterable 类则不考虑。

>>> class F():
	def __iter__(self):
		pass
>>> f = F()
>>> iter(f)
Traceback (most recent call last):
  File "", line 1, in 
    iter(f)
TypeError: iter() returned non-iterator of type 'NoneType'
>>> isinstance(f, collections.abc.Iterable)
True

>>> class G():
	def __getitem__(self):
		pass
>>> g = G()
>>> iter(g)

>>> isinstance(g, collections.abc.Iterable)
False

14.3 典型的迭代器
构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个__iter__方法,每次都实例化一个新的迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现__iter__方法,返回迭代器本身。因此,迭代器可以迭代,但是可迭代的对象不是迭代器。

14.4 生成器函数
可迭代对象中:
__iter__方法调用迭代器类的构造方法创建一个迭代器并将其返回:

def__iter__(self): 
	return SentenceIterator(self.words)

迭代器可以是生成器对象,每次调用__iter__方法都会自动创建,因为这里的__iter__方法是生成器函数:

def__iter__(self):
	for word in self.words: 
		yield word

14.12 深入分析iter函数
内置函数 iter 的文档中有个实用的例子。这段代码逐行读取文件,直到遇到空行或者到达文件末尾为止:

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

用print(line)实测发现,必须要有至少一个整个空行,否则到达结尾也不会结束

14 杂谈
。。。。。。。
生成器与迭代器的语义对比
。。。。。。。
第三方面是概念。
根据《设计模式:可复用面向对象软件的基础》一书的定义,在典型的迭代器设计模式中,迭代器用于遍历集合,从中产出元素。迭代器可能相当复杂,例如,遍历树状数据结构。但是,不管典型的迭代器中有多少逻辑,都是从现有的数据源中读取值;而且,调用 next(it) 时,迭代器不能修改从数据源中读取的值,只能原封不动地产出值。
而生成器可能无需遍历集合就能生成值,例如 range 函数。即便依附了集合,生成器不仅能产出集合中的元素,还可能会产出派生自元素的其他值。enumerate 函数是很好的例子。根据迭代器设计模式的原始定义,enumerate 函数返回的生成器不是迭代器,因为创建的是生成器产出的元组。

15.2 上下文管理器和with块
with 语句的目的是简化 try/finally 模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return 语句或 sys.exit() 调用而中止,也会执行指定的操作。finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。
上下文管理器协议包含__enter__和__exit__两个方法。with 语句开始运行时,会在上下文管理器对象上调用__enter__方法。with 语句运行结束后,会在上下文管理器对象上调用__exit__方法,以此扮演 finally 子句的角色。

15.3 contextlib模块中的实用工具
。。。。。。。。
@contextmanager
这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
。。。。。。。。
显然,在这些实用工具中,使用最广泛的是 @contextmanager 装饰器,因此要格外留心。这个装饰器也有迷惑人的一面,因为它与迭代无关,却要使用 yield 语句。由此可以引出协程,这是下一章的主题。
Python魔法模块之contextlib

使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。

16.2 用作协程的生成器的基本行为
最先调用 next(my_coro) 函数这一步通常称为“预激”(prime)协程(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。
【代码】Fluent Python_第1张图片
图 16-1:执行 simple_coro2 协程的 3 个阶段(注意,各个阶段都在 yield 表达式中结束,而且下一个阶段都从那一行代码开始,然后再把 yield 表达式的值赋给变量)

16.4 预激协程的装饰器

from functools import wraps

def coroutine(func):
""" 装饰器,向前执行第一个yield表达式"""
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer
####################测试用例#############################
@coroutine
def ave():
    total = 0.0
    count = 0
    ave = None
    while 1:
        term = yield ave
        total += term
        count += 1
        ave = total/count
if __name__ == "__main__":

    aa = ave()
    #next(aa) 不在需要
    print aa.send(1)
    print aa.send(2)
    print aa.send(3)

=========================================================
(python2.7) C:\Users\ASUS\Desktop>ipython main2.py
1.0
1.5
2.0

作者:你呀呀呀
链接:https://www.jianshu.com/p/813ca3081d3f
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

使用 yield from 句法(参见 16.7 节)调用协程时,会自动预激,因此与示例 16-5 中的@coroutine 等装饰器不兼容。Python 3.4 标准库里的 asyncio.coroutine 装饰器(第18 章介绍)不会预激协程,因此能兼容 yield from 句法。

16.5 终止协程和异常处理
示例 16-7 暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和Ellipsis 等常量经常用作哨符值。Ellipsis 的优点是,数据流中不太常有这个值。我还见过有人把 StopIteration 类(类本身,而不是实例,也不抛出)作为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)。
从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。这两个方法是 throw 和 close。

16.7 使用yield from
第 14 章说过,yield from 可用于简化 for 循环中的 yield 表达式。
在 Beazley 与 Jones 的《Python Cookbook(第 3 版)中文版》一书中,“4.14 扁平化处理嵌套型的序列”一节有个稍微复杂(不过更有用)的 yield from 示例

from collections import Iterable
def flattern(items, ignore_type=(str, bytes)):
	for item in items:
		if isinstance(item, Iterable) and not isinstance(item, ignore_type):
			yield from flattern(item)
		else:
			yield item

items = [1,2,[3,4,[5,6],7],8]
for item in flattern(items):
	print(item)

items = ['www','bai',['du','com']]

for item in flattern(items):
	print(item)

【代码】Fluent Python_第2张图片
图 16-2:委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复
示例 16-17 展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成器。因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield表达式的简单生成器结束;不过,也能以任何可迭代的对象结束,如示例 16-16 所示。
任何 yield from 链条都必须由客户驱动,在最外层委派生成器上调用 next(…) 函数或 .send(…) 方法。可以隐式调用,例如使用 for 循环。

16.10 本章小结
本章最后举了一个离散事件仿真示例,说明如何使用生成器代替线程和回调,实现并发。那个出租车仿真系统虽然简单,但是首次一窥了事件驱动型框架(如 Tornado 和asyncio)的运作方式:在单个线程中使用一个主循环驱动协程执行并发活动。使用协程做面向事件编程时,协程会不断把控制权让步给主循环,激活并向前运行其他协程,从而执行各个并发活动。这是一种协作式多任务:协程显式自主地把控制权让步给中央调度程序。而多线程实现的是抢占式多任务。调度程序可以在任何时刻暂停线程(即使在执行一个语句的过程中),把控制权让给其他线程。

17.1.1 依序下载的脚本
sys.stdout.flush()
win下不需要,ubuntu下需要刷新标准输出,才能实时显示

17.1.2 使用concurrent.futures模块下载

import requests
import re
import os
import time

from concurrent import futures
from tqdm import tqdm

# 最大线程数
MAX_TASKS = 10


def download_one(bn):
    img = requests.get("http://imgsrc.baidu.com/forum/pic/item/" + bn)  # 请求图片原图地址
    fpic = open(os.path.join(r".\saito_asuka", bn), "wb")  # 新建图片文件
    for j in img.iter_content(100000):
        fpic.write(j)  # 写入文件
    fpic.close()  # 关闭文件
    # bn:basename 图片名
    return bn


def save_pic(pic_set):
    pic_set = list(pic_set)
    workers = min(MAX_TASKS, len(pic_set))
    with futures.ThreadPoolExecutor(workers) as executor:
    	# 适于io密集型任务,如果是cpu密集型任务想绕开GIL:
    	# with futures.ProcessPoolExecutor() as executor:
    	# 其默认参数为cpu数量(s.cpu_count(),也是最大线程数)       
		res = executor.map(download_one, sorted(pic_set))
        # 这里res为download_one返回值
    # print(list(res))
    return len(list(res))


def get_pic(web):
    pic = re.compile(r"sign=.*?\.jpg.*?")  # 正则表达式
    elem = pic.findall(web.text)  # findall查询得图片后半地址列表

    # 图片名称的集合
    pic_set = set()
    for i in elem:
        basename = i.split("/")[1]  # 取得地址中的文件名
        if basename not in pic_set:
            pic_set.add(basename)
    return pic_set


# 此方法监视进度,未被使用
def show(pic_set):
    # return pic_set
    return tqdm(pic_set)


def download_many(url):
    web = requests.get(url)
    pic_set = get_pic(web)
    return save_pic(pic_set)


def main():
    time0 = time.time()
    url = "http://tieba.baidu.com/p/5367293540"
    count = download_many(url)
    cost = time.time() - time0
    print("{} pics done in {:.2f}s".format(count, cost))


if __name__ == '__main__':
    main()

17.1.3 期物在哪里
executor.submit 方法和 futures.as_completed 函数
executor.map 调用换成两个 for 循环:一个用于创建并排定期物,另一个用于获取期物的结果。

def save_pic(pic_ls):
	# 这里发现需要排序,把pic_set改成了pic_ls
    pic_ls = pic_ls[:5]
    with futures.ThreadPoolExecutor(max_workers=6) as executor:
        to_do = []
        for bn in sorted(pic_ls):
            # submit排定执行时间,返回期物,
            # 将表示待执行的操作存入期物列表to_do
            future = executor.submit(download_one, bn)
            to_do.append(future)
            msg = '计划中: {}:{}'
            print(msg.format(bn, future))
        results = []
        for future in futures.as_completed(to_do):
            # as_completed在期物运行结束后产出期物(该future不会阻塞),res获取期物结果(是被操作函数返回值吧)
            res = future.result()
            msg = '{}结果:{}'
            print(msg.format(future, res))
            results.append(res)

    return len(results)

17.4 实验Executor.map方法

具体情况因人而异:对线程来说,你永远不知道某一时刻事件的具体排序;有可能在另一台设备中会看到loiter(1) 在 loiter(0) 结束之前开始,这是因为 sleep 函数总会释放 GIL。因此,即使休眠 0 秒, Python 也可能会切换到另一个线程。


移除GIL非常困难,让我们去购物吧!
至于GIL,不要认为它在那的存在就是静态的和未经分析过的。Antoine Pitrou 在Python 3.2中实现了一个新的GIL,并且带着一些积极的结果。这是自1992年以来,GIL的一次最主要改变。这个改变非常巨大,很难在这里解释清楚,但是从一个更高层次的角度来说,旧的GIL通过对Python指令进行计数来确定何时放弃GIL。这样做的结果就是,单条Python指令将会包含大量的工作,即它们并没有被1:1的翻译成机器指令。在新的GIL实现中,用一个固定的超时时间来指示当前的线程以放弃这个锁。在当前线程保持这个锁,且当第二个线程请求这个锁的时候,当前线程就会在5ms后被强制释放掉这个锁(这就是说,当前线程每5ms就要检查其是否需要释放这个锁)。当任务是可行的时候,这会使得线程间的切换更加可预测。


executor.submit 和 futures.as_completed 这个组合比 executor.map 更灵活,因为 submit 方法能处理不同的可调用对象和参数,而 executor.map 只能处理参数不同的同一个可调用对象。此外,传给 futures.as_completed 函数的期物集合可以来自多个 Executor 实例,例如一些由 ThreadPoolExecutor 实例创建,另一些由 ProcessPoolExecutor 实例创建。

你可能感兴趣的:(【代码】Fluent Python)