標籤:

《Fluent Python》雜記

Python is a language for consenting adults. —Alan Runyan

1.Python 數據模型

Python data model 可以看下 python 文檔關於 data model 的討論

2. 序列構成的數組

容器序列(存放引用):list、tuple、collections.deque 扁平序列(存放值):str、bytes、bytearray、memoryview、array.array

python2.7 列表推導有變數泄露問題,所以推導的臨時變數不要和外部重名

python運行原理可視化: www.pythontutor.com

t = (1, 2, [1,2])nt[2] += [1,2] # t 變成 (1, 2, [1,2,1,2]) 同時拋出異常,用dis模塊查看n# t[2].extend([1,2]) 沒問題n

盡量不要把可變類型放在 tuple 里;增量賦值不是原子的; += *= 對於可變和不可變對象區分對待,不可變對象會生成新對象(str除外,cpython優化過)

python 使用的排序演算法 Timsort 是穩定的 內存視圖:memoryview:讓用戶在不複製內容的情況下操作同一個數組的不同切片。

collections.deque 線程安全

3 字典和集合

可散列:如果一個對象是可散列的,在這個對象的生命周期中, 它的散列值是不變的。而且需要實現__hash__,__eq__

class StrKeyDict0(dict):n """如果一個類繼承了dict,然後這個集成類提供了__missing__方法,n 那麼__getitem__找不到鍵的時候,會自動調用它,而不是拋出Keyerrorn """nn def __missing__(self, key):n if isinstance(key, str): # 如果 str 的 key 還找不到就拋出 KeyError,沒有這句會無限遞歸n raise KeyError(key)n return self[str(key)]nn def get(self, key, default):n try:n return self[key]n except KeyError: # 說明 __missing__ 也失敗了n return defaultnn def __contains__(self, key):n """這個方法也是必須的,因為繼承來的 __contains__ 沒有找到也會去掉用__missing__"""n return key in self.keys() or str(key) in self.keys()n

dict 變種:

  • collections.OrderedDict: 保持 key 的順序
  • collections.ChainMap: 容納多個不同的映射對象
  • collections.Counter: 計數器
  • collections.UserDict : 其實就是把標準 dict 用純 python 實習一遍

import UserDictnnnclass StrKeyDict(UserDict):nn def __missing__(self, key):n if isinstance(key, str): # 如果 str 的 key 還找不到就拋出 KeyError,沒有這句會無限遞歸n raise KeyError(key)n return self[str(key)]nn def __setitem__(self, key, item):n self.data[str(key)] = itemnn def __contains__(self, key):n return str(key) in self.datan

不可變映射類型: types.MappingProxyType (>=python3.3)

不要在迭代欄位和set 的同時修改它。可以先迭代獲取需要的內容後放到一個新的dict里。 dict 實現是稀疏列表。

dict特點:

  • 元素可散列
  • 內存開銷大
  • 鍵查詢很快
  • 鍵次序取決於添加順序
  • 往字典里添加新鍵可能會改變已有鍵的順序

set特點:

  • 元素必須可散列
  • 消耗內存
  • 高效判斷是否存在一個元素
  • 元素次序取決於添加順序
  • 往字典里添加新元素可能會改變已有元素的次序

4 文本和位元組序列

人類使用文本,計算機使用位元組序列

字元的標識(碼位),十進位數字,在unicode 中以4-6個十六進位數字表示 字元的具體表示取決於使用的編碼,編碼是在碼位和位元組序列之間轉換時使用的演算法 編碼:碼位-> 位元組序列 解碼:位元組序列 -> 碼位

Unicode 三明治:我們可以用一個簡單的原則處理編碼問題: 位元組序列->字元串->位元組序列。就是說程序中應當僅處理字元串,當需要保存到文件系統或者傳輸的時候,編碼為位元組序列

BOM:用來標記位元組序

UnicodeEncodeError:字元串轉成二進位序列。文本轉成位元組序列時,如果目標編碼沒有定義某個字元就會拋異常

UnicodeDecodeError: 二進位轉成字元串。遇到無法轉換的位元組序列

chardet 檢測文件編碼

處理文本:在多系統中運行的代碼需要指定打開和寫入的編碼,不要依賴默認的編碼。除非想判斷編碼,否則不要在二進位模式中打開文本文件。

使用 unicodedata.normalize 函數對 unicode 規範化(標準等價物)。保存文本之前用 normalize(NFC, user_text) 清洗字元串

Unicode 排序:unicode collation algorith, UCA 使用 PyUCA 庫。

雙模式 API:根據接受的參數是位元組序列或字元串自動處理。re 和 os 模塊

cpython 16 位窄構建(narrow build) 32 位寬構建 (wild build) sys.maxunicode。窄構建無法處理 U+FFFF 以上碼位

5 一等函數

高階函數(higher-order function): 接受函數作為參數,或者把函數作為結果返回的函數。比如 map,filter,reduce 等(大部分可以被列表推導替代)

匿名函數:lambda 用於創建匿名函數。不過 lambda 定義體中無法賦值,也無法使用 while, try 等python 語句

可調用對象:內置的 callable() 函數判斷是否可以調用

用戶定義的可調用類型:任何 python 對象只要是先了 __call__ 方法都可以表現得像函數

函數內省:使用 inspect 模塊提取函數的簽名、參數等

python3 函數註解: def clip(text:str, max_len:int > 0=80) -> str: 註解會存儲在 函數的 __annotations__(一個dict) 屬性中。註解對 python 解釋器沒有任何意義,只是給 IDE、框架、裝飾器等工具使用。

支持函數式編程:

  • operator 模塊:常用的有 attrgetter、itemgetter、methodcaller
  • functools 模塊:reduce、partial(基於一個函數創建一個新的可調用對象,把原函數的某些參數固定)、dispatch、wraps

6 使用一等函數實現設計模式

程序設計語言會影響人們理解問題的出發點。

本章舉了兩個例子說明動態語言是如何簡化設計模式的。(我個人感覺舉的例子不是很好吧,有點過度設計的感覺) 之前曾經總結過使用 Python 實現設計模式,感興趣的可以參考:

python-web-guide.readthedocs.io

7 函數裝飾器和閉包

裝飾器:可調用的對象,其參數是另一個函數。(說白了就是以函數作為參數的函數)兩個特性:

  • 能把被裝飾的函數替換為其他函數
  • 裝飾器在載入模塊時立即執行,通常是在導入時(即python載入模塊時)。被裝飾的函數只有在明確調用時運行

裝飾器語法糖:

# 等價於 target = decorate(target)n@decoratendef target():n print(hehe)n

閉包:閉包指延伸了作用域的函數,其中包含函數定義體中引用、但是不在定義體中定義的非全局變數。比如被裝飾的函數能訪問裝飾器函數中定義的變數(非全局的)

自由變數:

def make_averager():n series = []nn def averager(new_value):n # series 在 averager 中叫做自由變數(free variable),指未在本地作用域中綁定的變數n series.append(new_value)n total = sum(series)n return total / len(series)n

nonlocal 聲明:先來看個例子:

def make_averager():n count = 0n total = 0nn def averager(new_value):n # 直接運行到這裡會報錯,UnboundLocalError,因為對於非可變類型,會隱式創建局部變數 count,count 不是自由變數了n count += 1n total += new_valuen return totaln

使用 python3 引入的 nonlocal 刻意把變數標記為自由變數。

def make_averager():n count = 0n total = 0nn def averager(new_value):n nonlocal count, total # python2 可以用 [count] 把需要修改的變數存儲為可變對象n count += 1n total += new_valuen return totaln

functools.wraps 裝飾器:把相關屬性從被裝飾函數複製到裝飾器函數中

標準庫中的裝飾器:property、classmethod、staticmehtod、functools.lru_cache、functools.singledispatch

- lru_cache: 採用 least recent used 演算法實現的緩存裝飾器n- singledispatch: 為函數提供重載功能。被其裝飾的函數會成為泛函數(generic function):根據第一個參數的類型,用不同的方式執行相同操作的一組函數。替代多個 if/else isinstance 判斷類型執行不同分之n

疊放裝飾器:

# 下邊等價於 f = d1(d2(f))n@d1n@d2ndef f():n print(f)n

參數化裝飾器: 創建一個裝飾器工廠函數,把參數傳給它,返回一個裝飾器,然後再把它應用到要裝飾的函數上。

registry = set() # <1>nndef register(active=True): # <2> 工廠函數n def decorate(func): # <3>n print(running register(active=%s)->decorate(%s)n % (active, func))n else:n registry.discard(func) # <5>nn return func # <6>n return decorate # <7>nn@register(active=False) # <8>ndef f1():n print(running f1())nn@register() # <9> # 即使沒有參數,工廠函數必須寫成調用的形式ndef f2():n print(running f2())n

使用 class 實現裝飾器:看上邊多重嵌套的裝飾器是不是有點不太優雅, 其實複雜的裝飾器筆者更喜歡用 class 實現。還記得 __call__ 方法嗎,改寫下上邊這個例子

class register(object):n registry = set()nn def __init__(self, active=True):n self.active = activenn def __call__(self, func):n print(running register(active=%s)->decorate(%s)n % (self.active, func))n if self.active:n self.registry.add(func)n else:n self.registry.discard(func)nn return funcn

8 對象引用、可變性和垃圾回收

變數:我們可以把變數理解為對象的標註(便利貼),多個標註就是別名。變數保存的是對象的引用

比較對象: 判斷是同一個對象: id(obj1) == id(obj2) 或者 obj1 is obj2。比較兩個對象的值用 obj1 == obj2 (obj1.eq(obj2))。你會發現一般我們用 some_obj is None 來判斷一個對象是否是 None,說明 None 是個單例對象

元祖的相對不可變性:元祖的不可變指的是保存的引用不可變,與引用的對象無關。比如如果元祖的元素是個 list,我們是可以修改這個 list 的。這也會導致有些元祖無法散列

默認做淺複製:構造函數或者 [:] 方法默認是淺複製。如果元素都是不可變的,淺複製沒有問題。

深拷貝: copy.deepcopy 和 copy.copy 能為任意對象做深複製和淺複製。我們可以自定義 __copy__() __deepcopy__() 控制拷貝行為

函數的參數作為引用時:python 唯一支持的傳參模式是共享傳參(call by sharing),指函數的各個形式參數獲得實參中各個引用的副本。也就是說,函數內部的形參是實參的別名。這個方案的結果就是,函數可能會修改作為參數傳入的可變對象,但是無法修改那些對象的標識(即不能把一個對象替換成另一個對象)。(筆者覺得這章解釋非常好,之前網上一大堆討論python究竟是值傳遞還是引用傳遞的都是不準確的)

不要使用可變類型作為參數的默認值:這個坑寫 py 寫多的人應該都碰到過。函數默認值在定義函數時計算(通常是載入模塊時),因此默認值變成了函數對象的屬性。如果默認值是可變對象,而且修改了它的值,後續的函數調用就會受影響。一般我們用 None 作為佔位符。

def func(l=None): # 不要寫 def func(l=[]):n # 使用 None 作為佔位符(pylint 默認會提示可變類型參數作為默認值,所以俺經常安利用 pylint 檢測代碼,防範風險)n l = None or []n

所以,一般對於一個函數,要麼確認是要修改可變參數,要麼返回新的值(使用參數的拷貝),請不要兩者同時做。(筆者在小書 web guide 中明確提醒過)

del 和 垃圾回收: del 語句刪除名稱,而不是對象(刪除引用而不是對象)。只有對象變數保存的是對象的最後一個引用的時候,才會被回收。Cpython 中垃圾回收主要使用的是引用計數。不要輕易自定義 __del__ 方法,很難用對。

弱引用(weakref):有時候需要引用對象,而又不讓對象存在的時間超過所需時間,經常用在緩存中。弱引用不會增加對象的引用數量,不會妨礙所指的對象被當做垃圾回收

  • WeakValueDictionary: 一種可變映射,值是對象的弱引用。還有 WeakKeyDictionary、WeakSet、finalize。

Python 對不可變類型施加的把戲(CPython 實現細節)

  • 對於元祖 t, t[:] 和 tuple(t) 不會創建副本,返回的是引用(這點和list 不同)。str, bytes 和 frozenset 也有這種行為

9 符合 Python 風格的對象

鴨子類型(duck typing): 只需按照預定行為實現對象所需的方法即可。

對象的表示形式: repr() 讓開發者理解的方式返回對象的字元串表示。str() 用戶理解的方式返回對象的字元串表示

classmethod 和 staticmehtod: classmethod 定義操作類而不是操作實例的方法,第一個參數是類本身,最常見的用途是定義定義備選構造方法(返回 cls(*))。staticmehtod 方法就是普通函數,只是碰巧在類的定義體中。

Python的私有屬性和『受保護』屬性:python沒有 private修飾符,可以通過雙下劃線 __attr 的形式定義,python 的子類會在存儲屬性名的時候在前面加上一個下劃線和類名。這個語言特性成為名稱改寫(name mangling)。通常受保護的屬性使用一個下劃線作為前綴,算是一種命名約定,調用者不應該在類外部訪問這種屬性。

python 沒有訪問控制和 java 設計迥然不同,本章最後的雜談討論了這兩種設計。在 python 中,我們可以先使用公開屬性,等需要時再變成特性。

使用__slots__類屬性節省空間:默認情況下,python在各個中名為__dict__的字典存儲實力屬性,當生成大量對象時字典會消耗大量內存(底層是稀疏數組),通過__slots__類屬性,讓解釋器在元祖而不是字典中存儲實例屬性,能大大節省內存。(不支持繼承)

  • 每個子類都需要定義 __slots__,解釋器會忽略繼承的 __slots__屬性
  • 實例只能擁有 __slots__ 屬性,除非把 __dict__ 也加到 __slots__ 里(這樣就失去了節省內存的功效)

覆蓋類屬性:python有個獨特的特性,類屬性可以為實例屬性提供默認值

10 序列的修改、散列和切片

協議和鴨子類型: python中我們刻意創建序列類型而無需使用繼承,只需實現符合序列協議的方法。

鴨子類型: 在面向對象編程中,協議是非正式的介面,只在文檔中定義,在代碼中不定義。例如 python 序列協議只需要實現 __lens____getitem__ 兩個方法。只關心行為而不關心類型。

我們可以模仿 python 對象的內置方法來編寫符合 python 風格的類。(具體的大家還是看下書中的代碼示例吧,這一章舉得例子不錯)

11 介面:從協議到抽象基類

使用猴子補丁在運行時實現協議:

運行時修改類或者模塊,而不改動源碼。可以在運行時讓類實現協議

抽象基類:collections.abc 模塊

  • Iterable,Container 和 Sized::Iterable 通過 __iter__ 支持迭代,Container 通過 __contains__支持 in 操作符, Sized 通過 __len__ 支持 len() 函數
  • Sequence, Mapping and Set :不可變集合類型
  • MappingView: 映射方法 .items(),.kesy(),.values() 返回的對象分別是 ItemsView,KeysView 和 ValuesView 實例
  • Callable 和 Hashable: 主要作用市委內置 isinstance 提供支持,以丠??種安全的方式判斷對象能不能調用或散列。python 提供了callable 內置函數卻沒有提供 hashable() ,用 isinstance(obj, Hashable) 判斷
  • Iterator
  • numbers包:Number, Complex,Real,Rational,Integral

定義並使用抽象基類

import abcnclass Base(abc.ABC): # py3, py2 中使用 __metaclass__ = abc.ABCMetan @abc.abstractmethod # 該裝飾器應該放在最裡層n def some_method(self): # 這裡可以只有 docstring 省略函數體n """抽象方法,在抽象基類出現之前抽象方法用 Raise NotImplementedError 語句表明由子類實現"""n

使用 register 方法註冊虛擬子類:

在抽象基類上調用 register 方法註冊其虛擬子類,issubclass 和 isinstance 都能識別,但是註冊的的類不會從抽象基類中繼承任何方法和屬性。查看虛擬子類的 __mro__ 會發現抽象基類不在其中(沒繼承其屬性和方法)

__subclasshook__ : 即使不註冊,抽象基類也能把一個類識別為虛擬子類。定義 __subclasshook__ 方法動態識別子類。參考 abc.Sized 源碼

強類型和弱類型:

如果一門語言很少隱式轉換類型,說明它是強類型語言(java/c++/python)。如果經常這麼做,是弱類型語言(php,javascript,perl)。強類型能及早發現缺陷

靜態和動態類型:

在編譯時期檢查類型的語言是靜態語言,運行時檢查類型的語言是動態語言。靜態類型需要類型聲明(有些現代語言使用類型推導避免部分類型聲明)。靜態類型便於編譯器和 IDE 及早分析代碼、找出錯誤和提供其他服務(優化、重構等)。動態類型便於代碼重用,代碼行數更少,而且能讓介面自然成為協議而不提早實行。

12 繼承的優缺點

子類化內置類型:

內置類型的方法不會調用子類覆蓋的方法。不要子類化C語言實現的內置類型(list,dict等),用戶自定義的類應該繼承自 collections 模塊。collections.UserDict, UserList and UserString

多重繼承和方法解析順序

任何支持多重繼承的語言都要處理潛在的明明衝突問題,菱形繼承問題。python 會按照 方法解析順序MRO(method resolution order) 遍歷繼承圖。類都有一個 __mro__ 屬性,它的值是一個tuple,按照順序列出各個超類,直到 object 類。MRO 根據 C3 演算法計算

處理多重繼承

多重繼承增加了可選方案和複雜度

  • 把介面繼承和實現繼承區分開。明確一開始為什麼創建子類。1.繼承介面,創建子類型,實現『是什麼』關係。2.繼承實現,重用代碼。通過繼承重用代碼是實現細節,通常可換成組合和委託。介面繼承是框架的支柱
  • 使用抽象基類顯示錶示介面
  • 通過 mixin 重用代碼。mixin 不定義新類型, 只是打包方法,便於重用。mixin 類絕對不能實例化,應該提供某方面的特定行為,只是實現少量關係非常緊密的方法
  • 明確指名 mixin。類應該以 mixin 後綴
  • 抽象基類可以作為 mixin,但是反過來不成立
  • 不要子類化多個具體類。具體類的超類中除了一個具體類,其他都應該是抽象基類或者 mixin

class MyConcreteClass(Alpha, Beta, Gamma):n """ 如果 Alpha 是具體類,Beta 和 Gamma 必須是抽象基類或者 mixin"""n passn

  • 創建聚合類。django 中的 ListView,tinker中的 Widget

class Widget(BaseWidget, Pack, Place, Grid):n passn

  • 優先使用組合而非繼承。子類化是一種緊耦合,不要過度使用

13 正確重載運算符

python 不允許用戶隨意創建運算符,禁止重載內置類型的運算符。python支持運算符重載是其在科學計算領域使用廣泛的原因。

  • 一元運算符:始終返回一個新對象。
  • NotImplemented 是個特殊的單例值,如果中綴運算符特殊方法不能處理給定的操作數,要把它返回給解釋器。NotImplementedError 是一種異常,抽象類中的方法把它 raise 出,提醒子類必須覆蓋。

def __add__(self, other):

try:

pairs = itertools.zip_longest(self, other, fillvalue=0.0)

return Vector(a + b for a, b in pairs)

except TypeError:

# 返回 NotImplemented 解釋器會嘗試調用 反向運算符方法 __radd__

return NotImplemented

def __radd__(self, other):

return self + other

  • 增量賦值運算符不會修改不可變目標,而是新建實例,然後重新綁定。

14 可迭代對象、迭代器和生成器

解釋器需要迭代對象 x 時,會自動調用 iter(x)。內置的 iter 有以下作用:

  • 檢查對象是否實現了 __iter__ ,如果實現了就調用它獲取一個迭代器
  • 如果沒有實現 __iter__ 方法,但是實現了 __getitem__ 方法,python 會創建一個迭代器,嘗試按照順序(從索引0)獲取元素
  • 如果嘗試失敗,拋出 TypeError 異常

可迭代對象:

如果對象實現了能返回迭代器的 __iter__ 方法,就是可迭代的。序列都可以迭代;實現了__getitem__ 方法,而且其參數是從 0 開始的索引,這種對象也可以迭代。

標準迭代器介面有兩個方法:

  • __next__: 返回下一個可用的元素,沒有元素拋出 StopIteration
  • __iter__: 返回 self,以便在應該使用可迭代對象的地方使用迭代器,例如 for 循環中

檢查對象是否是迭代器的最好方法是調用 isinstance(x, abc.Iterator)

迭代器:

實現了無參數的 __next__ 方法,返回序列中下一個元素;如果沒有元素了,拋出 StopIteration 異常。python中的迭代器還實現了 __iter__ 方法,因此迭代器也可以迭代。

二者區別:

迭代器可以迭代,但是可迭代的對象不是迭代器。可迭代的對象一定不能是自身的迭代器。也就是說,可迭代對象必須實現 __iter__ ,但是不能實現 __next__

生成器函數

只要 python 的函數體中有 yield 關鍵字,該函數就是生成器函數。調用生成器函數會返回一個生成器對象。生成器函數是生成器工廠

標準庫的中生成器函數

  • 用於過濾的生成器函數: itertools.takewhile/compress/dropwhile/filter/filterfalse/islice/
  • 用於映射的生成器函數: 內置的 enumerate/map itertools.accumulate/starmap
  • 用於合併的生成器函數:itertools.chain/from_iterable/product/zip_longest 內置的 zip
  • 從一個元素產生多個值,擴展輸入的可迭代對象: itertools.combinations/combinations_with_replacement/count/cycle/permutations/repeat
  • 產出輸入可迭代對象的全部元素,以某種方式排列:itertools.groupby/tee 內置的 reversed

可迭代的規約函數

歸約函數:接受一個可迭代的對象,返回單個結果。all/any/max/mini/functools.reduce/sum all/any 有短路特性

把生成器當協程

.send() 方法致使生成器前進到下一個 yield 語句,還允許使用生成器的客戶把數據發給自己,不管傳給 send 方法什麼參數, 那個參數都會成為生成器函數定義體中對應的 yield 表達式的值。

15 上下文管理器和 else 塊

EAFP vs LBYL

  • EAFP: easier to ask for forgiveness than permission
  • LBYL: look before you leap

上下文管理器和 with 塊

with 語句用來簡化 try/finally 模式。經常用在管理事務,維護鎖、條件和信號,給對象打補丁等。

class LookingGlass:nn def __enter__(self): # <1>n import sysn self.original_write = sys.stdout.write # <2>n sys.stdout.write = self.reverse_write # <3>n return JABBERWOCKY # <4>nn def reverse_write(self, text): # <5>n self.original_write(text[::-1])nn def __exit__(self, exc_type, exc_value, traceback): # <6>n import sys # <7>n sys.stdout.write = self.original_write # <8>n if exc_type is ZeroDivisionError: # <9>n print(Please DO NOT divide by zero!)n return True # <10>n

contextlib 模塊中的實用工具

@contextmanager 裝飾器能減少創建上下文管理器的樣板代碼。只需要實現一個 yield 語句的生成器,生成想讓 __enter__ 方法返回的值。

@contextlib.contextmanagerndef looking_glass():n import sysn original_write = sys.stdout.writenn def reverse_write(text):n original_write(text[::-1])nn sys.stdout.write = reverse_writen msg = # <1>n try:n yield JABBERWOCKY # 產出一個值,這個值會綁定到with語句中的 as 子句的目標變數上n except ZeroDivisionError: # <2>n msg = Please DO NOT divide by zero!n finally:n sys.stdout.write = original_write # <3>n if msg:n print(msg) # <4>n

16 協程

句法上看,協程和生成器類似,都是定義體中包含 yield 關鍵字的函數。但在協程中,yield 通常出現在表達式右邊(datum = yield),可以產出值,也可以不產出。如果yield 關鍵字後邊沒有表達式,那麼生成器產出 None。調用方可以用 send 方法把數據提供給協程。從根本上把 yield 視作控制流程的方式。

生成器如何進化成協程

python2.5 之後yield 關鍵在能在表達式中使用,而且生成器 api 中增加了 .send(value) 方法。生成器的調用方可以用 send 發送數據,發送的數據會成為生成器函數中 yield 表達式的值。因此生成器能當做協程使用。協程是指一個過程,這個過程與調用方協作,產出由調用方提供的值。

協程使用 next 函數預激(prime),即讓協程向前執行到第一個 yield 表達式。

預激(prime)協程的裝飾器

啟動協程之前需要 prime,方法是調用 send(None) 或者 next() 。為了簡化協程的語法,有時候會使用一個 預激 裝飾器。 比如 tornado.gen 裝飾器。yield from 調用協程會自動 預激

from functools import wrapsnndef coroutine(func):n """向前執行到第一個 yield 表達式,預激 func """n @wraps(func)n def primer(*args, **kwargs):n gen = func(*args, **kwargs) # 獲取生成器對象n next(gen) # primen return genn return primern

終止協程和異常處理

協程中未處理的異常會向上冒泡

  • generator.throw(exc_type)
  • generator.close()

讓協程返回值

協程中 return 表達式的值會偷偷傳給調用方,賦值給 StopIteration 異常的一個屬性 value

try:n coro.send(None)nexcept StopIteration as exc:n result = exc.valuen

yield from(python3)

RESULT = yield from EXPR 等效代碼如下,雖然比較複雜, 但是能幫助我們理解 yield from 如何工作

_i = iter(EXPR) # EXPR 是任何可迭代對象ntry:n _y = next(_i) # 預激(prime) 子生成器nexcept StopIteration as _e:n _r = _e.value # 如果拋出 StopIteration 獲取 value 屬性(返回值)nelse:n while 1: # 運行這個循環時,委派生成器會阻塞,只作為調用方和子生成器之間的通道n try:n _s = yield _y # 產出字生成器當前產出元素;等待調用方發送 _s 中保存的值n except GeneratorExit as _e: # 用於關閉委派生成器和子生成器n try:n _m = _i.closen except AttributeError: # 子生成器是任何可迭代對象,所以可能沒有 close 方法n passn else:n _m()n raise _en except BaseException as _e: # 處理調用方通過 throw 方法傳入的異常n _x = sys.exc_info()n try:n _m = _i.thrown except AttributeError: # 子生成器是任何可迭代對象,所以可能沒有 throw 方法n raise _en else: # 如果子生成器有 throw 方法,調用它並傳入調用方發來的異常n try:n _y = _m(*_x)n except StopIteration as _e:n _r = _e.valuen breakn else: # 如果產出值時沒有異常n try: 嘗試讓子生成器向前執行n if _s is None: # <11>n _y = next(_i)n else:n _y = _i.send(_s)n except StopIteration as _e: # <12>n _r = _e.valuen breaknnRESULT = _r # 返回的值是 _r,即整個 yield from 表達式的值n

17 使用 concurrent.futures 處理並發

python3.2 後引入了 concurrent.futers 模塊用來處理並發。該模塊引入了 TreadPoolExecutor 和 ProcessPoolExecutor 類,這兩個類實現的介面能分別在不同的線程和進程中執行可調用的對象。

from concurrent import futuresnnfrom flags import save_flag, get_flag, show, main # <1>nnMAX_WORKERS = 20 # <2>nnndef download_one(cc): # <3>n image = get_flag(cc)n show(cc)n save_flag(image, cc.lower() + .gif)n return ccnnndef download_many(cc_list):n workers = min(MAX_WORKERS, len(cc_list)) # <4>n with futures.ThreadPoolExecutor(workers) as executor: # <5>n res = executor.map(download_one, sorted(cc_list)) # <6>nn return len(list(res)) # <7>nnnif __name__ == __main__:n main(download_many) # <8>n

Future(期物)(中文版翻譯感覺這個名字怪怪的)

concurrent.futures.Future: Feature 類的實例都表示可能已經完成或者尚未完成的延遲計算,可以調用它的 result() 方法獲取結果

def download_many(cc_list):n cc_list = cc_list[:5] # <1>n with futures.ThreadPoolExecutor(max_workers=3) as executor: # <2>n to_do = []n for cc in sorted(cc_list): # <3>n future = executor.submit(download_one, cc) # <4>n to_do.append(future) # <5>n msg = Scheduled for {}: {}n print(msg.format(cc, future)) # <6>nn results = []n for future in futures.as_completed(to_do): # <7>n res = future.result() # <8>n msg = {} result: {!r}n print(msg.format(future, res)) # <9>n results.append(res)nn return len(results)n

阻塞型 IO 和 GIL

GIL 一次只允許一個線程執行 python 位元組碼。但是標準庫中所有執行阻塞型 I/O 操作的函數,在等待操作系統返回的結果時都會釋放 GIL,這意味著python 在這個層次上能使用多線程,一個 python 線程等待網路請求時,阻塞型 I/O 會釋放(sleep 函數也會) GIL,運行另一個線程。因此儘管有 GIL,python 線程還是能在 IO 密集型應用中發揮作用。

concurrent.futures.ProcessPoolExecutor 繞開 GIL

18 使用 asyncio 處理並發

asyncio 使用事件循環驅動的協程實現並發

import asyncionnimport aiohttp # <1>nnfrom flags import BASE_URL, save_flag, show, main # <2>nnn@asyncio.coroutine # <3>ndef get_flag(cc):n url = {}/{cc}/{cc}.gif.format(BASE_URL, cc=cc.lower())n resp = yield from aiohttp.request(GET, url) # <4>n image = yield from resp.read() # <5>n return imagennn@asyncio.coroutinendef download_one(cc): # <6>n image = yield from get_flag(cc) # <7>n show(cc)n save_flag(image, cc.lower() + .gif)n return ccnnndef download_many(cc_list):n loop = asyncio.get_event_loop() # <8>n to_do = [download_one(cc) for cc in sorted(cc_list)] # <9>n wait_coro = asyncio.wait(to_do) # <10>n res, _ = loop.run_until_complete(wait_coro) # <11>n loop.close() # <12>nn return len(res)nnnif __name__ == __main__:n main(download_many)n

避免阻塞型調用

兩種方式避免阻塞型調用中止整個應用程序的進程:

  • 在單獨的線程中運行各個阻塞型操作
  • 把每個阻塞型調用操作轉成非阻塞的非同步調用

在 asyncio 中使用 Executor 對象,防止阻塞事件循環

python 訪問本地文件系統會阻塞,硬碟IO 阻塞會浪費幾百萬個 cpu 周期。解決方法是使用時間循環對象的 run_in_executor 方法。

@asyncio.coroutinendef download_one(cc, base_url, semaphore, verbose):n try:n with (yield from semaphore):n image = yield from get_flag(base_url, cc)n except web.HTTPNotFound:n status = HTTPStatus.not_foundn msg = not foundn except Exception as exc:n raise FetchError(cc) from excn else:n loop = asyncio.get_event_loop() # 獲取事件循環對象的引用n loop.run_in_executor(None, # None 使用默認的 TrreadPoolExecutor 實例n save_flag, image, cc.lower() + .gif) # 傳入可調用對象n status = HTTPStatus.okn msg = OKnn if verbose and msg:n print(cc, msg)nn return Result(status, cc)nasyncio 的事件循環背後維護一個 ThreadPoolExecutor 對象,我們可以調用 run_in_executor 方法, 把可調用的對象發給它執行。n

從回調到 Futures 和 協程

回調地獄:如果一個操作需要依賴之前操作的結果,那就得嵌套回調。

python 中的回調地獄:

def stage1(response1):n request2 = step1(response1)n api_call2(request2, stage2)nnndef stage2(response2):n request3 = step2(response2)n api_call3(request3, stage3)nnndef stage3(response3):n step3(response3)nnnapi_call1(request1, step1)n

使用 協程 和 yield from 結構做非同步編程,無需用回調

@asyncio.coroutinendef three_stages(request1):n response1 = yield from api_call1()n request2 = step1(response1)n response2 = yield from api_call2(request2)n request3 = step2(response2)n response3 = yield from api_call3(request3)n step3(response3)nn# 協程不能直接調用,必須用事件循環顯示指定協程的執行時間,或者在其他排定了執行時間的協程中使用 yield from 表達式把它激活nloop.create_task(three_stages(request1))n

何時使用 yield from:基本原則很簡單,yield from 只能用於 協程 和 asyncio.Future 實例(包括 Task 實例)。有些肆意混淆了協程和普通函數的 api 比較棘手。

驅動協程:只有驅動協程,協程才能做事,而驅動 asyncio.coroutine 裝飾的協程有兩種方法,要麼使用 yield from,要麼傳給 asyncio 包中某個參數為協程或者 Futures 的函數,例如 run_until_complete

使用 asyncio 包編寫伺服器

可以使用 asyncio 編寫 tcp/udp 伺服器,使用 aiohttp 編寫 web 伺服器。具體看各自的文檔吧。

19 動態屬性(attribute)和特性(property)

python 中,數據的屬性和處理數據的方法統稱為屬性(attribute),方法是可調用的屬性。特性(property)是不改變類介面的前提下,使用存取方法(讀值和設值)修改數據屬性。

統一訪問原則:不管服務是由存取還是計算實現的,一個模塊提供的所有服務都應該統一的方式使用。

使用動態屬性轉換數據

使用動態屬性訪問數據

from collections import abcnnclass FrozenJSON:n """A read-only fa?ade for navigating a JSON-like objectn using attribute notationn """nn def __init__(self, mapping):n self.__data = dict(mapping) # <1>nn def __getattr__(self, name): # <2>n if hasattr(self.__data, name):n return getattr(self.__data, name) # <3>n else:n return FrozenJSON.build(self.__data[name]) # <4>nn @classmethodn def build(cls, obj): # <5>n if isinstance(obj, abc.Mapping): # <6>n return cls(obj)n elif isinstance(obj, abc.MutableSequence): # <7>n return [cls.build(item) for item in obj]n else: # <8>n return objn

處理無效屬性名

def __init__(self, mapping):n self.__data = {}n for key, value in mapping.items():n if keyword.iskeyword(key): # <1>n key += _ # 和 python 重名的關鍵字加上下劃線n self.__data[key] = valuen

使用 __new__ 以靈活的方式創建對象

實際上用來構建對象的方法是 __new____init__ 是初始化方法。__new__ 必須返回一個實例,作為 __init__ 方法的第一個參數。

def __new__(cls, arg): # <1>n if isinstance(arg, abc.Mapping):n return super().__new__(cls) # <2>n elif isinstance(arg, abc.MutableSequence): # <3>n return [cls(item) for item in arg]n else:n return argn

使用特性驗證屬性

class LineItem:nn def __init__(self, description, weight, price):n self.description = descriptionn self.weight = weight # <1>n self.price = pricenn def subtotal(self):n return self.weight * self.pricenn @property # <2>n def weight(self): # <3>n return self.__weight # <4>nn @weight.setter # <5>n def weight(self, value):n if value > 0:n self.__weight = value # <6>n else:n raise ValueError(value must be > 0) # <7>n

解析 property

property 簽名

class property(fget=None, fset=None, fdel=None, doc=None)n

  • 特性會覆蓋實例屬性。特性都是【類屬性】,但是特性管理的其實是實例屬性的存取。obj.attr 這樣的表達式不會從 obj 開始尋找 attr,而是從 obj.__class__ 開始,且僅當類中沒有 attr 的屬性時, python 才會在 obj 實例中尋找。

定義一個特性工廠函數

def quantity(storage_name): # <1>nn def qty_getter(instance): # <2>n return instance.__dict__[storage_name] # <3>nn def qty_setter(instance, value): # <4>n if value > 0:n instance.__dict__[storage_name] = value # <5>n else:n raise ValueError(value must be > 0)nn return property(qty_getter, qty_setter) # <6>nnnclass LineItem:n weight = quantity(weight) # <1>n price = quantity(price) # <2>nn def __init__(self, description, weight, price):n self.description = descriptionn self.weight = weight # <3>n self.price = pricenn def subtotal(self):n return self.weight * self.price # <4>n

處理屬性刪除操作

class BlackKnight:nn def __init__(self):n self.members = [an arm, another arm,n a leg, another leg]n self.phrases = ["Tis but a scratch.",n "Its just a flesh wound.",n "Im invincible!",n "All right, well call it a draw."]nn @propertyn def member(self):n print(next member is:)n return self.members[0]nn @member.deletern def member(self):n text = BLACK KNIGHT (loses {})n-- {}n print(text.format(self.members.pop(0), self.phrases.pop(0)))n

處理屬性的重要屬性和函數

影響屬性處理方式的特殊屬性

  • __class__: 對象所屬類的引用。 obj.__class__ 與 type(obj) 作用相同。python的某些特殊方法比如 __getattr__,只在對象的類中尋找,而不在實例中尋找
  • __dict__: 存儲對象或者類的可寫屬性。
  • __slots__: 字元串tuple,限制允許有的屬性。

處理屬性的內置函數

  • dir: 列出對象的大多數屬性
  • getattr: 從 obj 對象中獲取對應???稱的屬性。獲取的屬性可能來自對象所屬的類或者超類。
  • hasattr: 判斷對象中存在指定的屬性
  • setattr: 創建新屬性或者覆蓋現有屬性
  • vars: 返回對象的 __dict__ 屬性

處理屬性的特殊方法

  • __delatttr__(self, name) 使用 del 刪除屬性就會調用這個方法
  • __dir__(self): 把對象傳給 dir 函數時候調用
  • __getattr__: 僅當獲取指定的屬性失敗,搜索過 obj、Class、和超類之後調用
  • __getattribute__: 嘗試獲取指定的屬性時總會調用這個方法,尋找的屬性是特殊屬性或者特殊方法時候除外。為了防止獲取 obj 的屬性無限遞歸, __getattribute__ 方法的實現要使用super().__getattribute__(obj, name)
  • __setattr__: 嘗試設置指定的屬性總會調用20 屬性描述符

描述符是對多個屬性運用相同存儲邏輯的一種方式。例如 orm 中的欄位類型是描述符。描述符是實現了特定協議的類,這個協議包括 __get__ __set__ __delete__ 方法。描述符的用法是創建一個實例,作為另一個類的類屬性。

class Quantity: # <1>nn def __init__(self, storage_name):n self.storage_name = storage_name # <2>nn def __set__(self, instance, value): # <3>n if value > 0:n instance.__dict__[self.storage_name] = value # <4>n else:n raise ValueError(value must be > 0)nnnclass LineItem:n weight = Quantity(weight) # <5>n price = Quantity(price) # <6>nn def __init__(self, description, weight, price): # <7>n self.description = descriptionn self.weight = weightn self.price = pricenn def subtotal(self):n return self.weight * self.pricen

覆蓋型描述符:實現 __set__ 方法的描述符屬於覆蓋型描述符。 非覆蓋型描述符:沒有實現 __set__方法的描述符。

方法是描述符

描述符用法建議:

  • 使用 property 以保持簡單: 內置的 property 實現的其實是覆蓋型描述符
  • 只讀描述符必須有 __set__ 方法: 如果使用描述符類實現只讀屬性, __get__ __set__ 兩個方法必須定義,否則實例的同名屬性會遮蓋描述符。只讀屬性的 __set__ 只需拋出 AttributeError 異常,並提供合適的錯誤消息
  • 用於驗證的描述符可以只有 __set__
  • 只有 __get__ 方法的描述符可以實現高效緩存
  • 非特殊的方法可以被實例屬性覆蓋

21 類元編程

元編程是指在運行時創建或者定製類的技術。除非開發框架,否則不要編寫元類 類裝飾器能以較為簡單的的方式做到需要使用元類去做的事情-創建時定製類。缺點是無法繼承

導入時和運行時比較

python 中的 import 不只是聲明,進程首次導入模塊時,還會運行所導入模塊中的全部頂層代碼。導入時,解釋器會執行執行每個類的定義體. (原書有段代碼示例非常好地解釋了導入的問題)

元類基礎

感覺這一章寫得不如筆者之前寫的一篇博客《簡單的python元類》好理解。

class EntityMeta(type):n """Metaclass for business entities with validated fields"""nn def __init__(cls, name, bases, attr_dict):n super().__init__(name, bases, attr_dict) # <1>n for key, attr in attr_dict.items(): # <2>n if isinstance(attr, Validated):n type_name = type(attr).__name__n attr.storage_name = _{}#{}.format(type_name, key)nnclass Entity(metaclass=EntityMeta): # <3>n """Business entity with validated fields"""n

元類的特殊方法 __prepare__

某些應用中可能想知道屬性定義的順序,解決辦法是使用 python3 引入的 __prepare__ 。這個特殊方法只在元類中有用,而且必須是類方法。解釋器調用元類的 __new__ 之前會先調用 __prepare__,使用類定義體中的屬性創建映射。 元類構建新類時, __prepare__ 方法返回的映射會傳給 __new__ 的最後一個參數,然後再傳給 __init__ 方法。

class EntityMeta(type):n """Metaclass for business entities with validated fields"""nn @classmethodn def __prepare__(cls, name, bases): # py3, must be a class methodn return collections.OrderedDict() # <1> return empty OrderedDict, where the class attritubes will be storedn def __init__(cls, name, bases, attr_dict):n super().__init__(name, bases, attr_dict)n cls._field_names = [] # <2>n for key, attr in attr_dict.items(): # <3> # in ordern if isinstance(attr, Validated):n type_name = type(attr).__name__n attr.storage_name = _{}#{}.format(type_name, key)n cls._field_names.append(key) # <4>nnnclass Entity(metaclass=EntityMeta):n """Business entity with validated fields"""nn @classmethodn def field_names(cls): # <5>n for name in cls._field_names:n yield namen

元類使用場景

  • 驗證屬性
  • 一次把裝飾器依附到多個方法上
  • 序列化對象或者轉換數據
  • 對象關係映射(ORM框架)
  • 基於對象的持久存儲
  • 動態轉換使用其他語言編寫的類結構

類作為對象

  • cls.__bases__: 類的基類組成的元祖
  • cls.__qualname__: py3 引入,值是類或函數的限定名稱,即從模塊的全局作用域到類的點分路徑
  • cls.__subclasses__(): 返回一個list,包含類的直接子類。其實現使用弱引用,防止超類和子類之間出現循環引用。這個方法返回的列表是內存里現存的子類。
  • cls.mro(): 構建類時,如果需要獲取存儲在類屬性 __mro__ 中的超類元組,解釋器會調用這個方法。元類可以覆蓋這個方法。

推薦閱讀:

TAG:Python |