1. 程式人生 > >【程式碼】Fluent Python

【程式碼】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 "<pyshell#6>", line 1, in <module>
    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)
<iterator object at 0x000001BA5F099A20>
>>> 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 表示式,準備好作為活躍的協程使用)。

圖 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)


圖 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 也可能會切換到另一個執行緒。
executor.submit 和 futures.as_completed 這個組合比 executor.map 更靈活,因為 submit 方法能處理不同的可呼叫物件和引數,而 executor.map 只能處理引數不同的同一個可呼叫物件。此外,傳給 futures.as_completed 函式的期物集合可以來自多個 Executor 例項,例如一些由 ThreadPoolExecutor 例項建立,另一些由 ProcessPoolExecutor 例項建立。