【程式碼】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 例項建立。