《流暢的python》閱讀筆記

weapon發表於2017-10-16

起步

《流暢的python》是一本適合python進階的書, 裡面介紹的基本都是高階的python用法. 對於初學python的人來說, 基礎大概也就夠用了, 但往往由於夠用讓他們忘了深入, 去精通. 我們希望全面瞭解這個語言的能力邊界, 可能一些高階的特性並不能馬上掌握使用, 因此這本書是工作之餘, 還有餘力的人來閱讀, 我這邊就將其有用, 精妙的進階內容整理出來.

這本書有21個章節, 整理也是根據這些章節過來.

第一章: python資料模型

這部分主要介紹了python的魔術方法, 它們經常是兩個下劃線包圍來命名的(比如 __init__ , __lt__, __len__ ). 這些特殊方法是為了被python直譯器呼叫的, 這些方法會註冊到他們的型別中方法集合中, 相當於為cpython提供抄近路. 這些方法的速度也比普通方法要快, 當然在自己不清楚這些魔術方法的用途時, 不要隨意新增.

關於字串的表現形式是兩種, __str____repr__ . python的內建函式 repr 就是通過 __repr__ 這個特殊方法來得到一個物件的字串表示形式. 這個在互動模式下比較常用, 如果沒有實現 __repr__ , 當控制檯列印一個物件時往往是 <A object at 0x000> . 而 __str__ 則是 str() 函式時使用的, 或是在 print 函式列印一個物件的時候才被呼叫, 終端使用者友好.

兩者還有一個區別, 在字串格式化時, "%s" 對應了 __str__ . 而 "%r" 對應了 __repr__. __str____repr__ 在使用上比較推薦的是,前者是給終端使用者看,而後者則更方便我們除錯和記錄日誌.

更多的特殊方法: https://docs.python.org/3/reference/datamodel.html

第二章: 序列構成的陣列

這部分主要是介紹序列, 著重介紹陣列和元組的一些高階用法.

序列按照容納資料的型別可以分為:

  • 容器序列: list、tuple 和 collections.deque 這些序列能存放不同型別的資料
  • 扁平序列: str、bytes、bytearray、memoryview 和 array.array,這類序列只能容納一種型別.

如果按照是否能被修改可以分為:

  • 可變序列: list、bytearray、array.array、collections.deque 和 memoryview
  • 不可變序列: tuple、str 和 bytes

列表推導

列表推導是構建列表的快捷方式, 可讀性更好且效率更高.

例如, 把一個字串變成unicode的碼位列表的例子, 一般:

symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
    codes.append(ord(symbol))

使用列表推導:

symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]

能用列表推導來建立一個列表, 儘量使用推導, 並且保持它簡短.

笛卡爾積與生成器表示式

生成器表示式是能逐個產出元素, 節省記憶體. 例如:

>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):
... print(tshirt)

例項中列表元素比較少, 如果換成兩個各有1000個元素的列表, 顯然這樣組合的笛卡爾積是一個含有100萬元素的列表, 記憶體將會佔用很大, 而是用生成器表示式就可以幫忙省掉for迴圈的開銷.

具名元組

元組經常被作為 不可變列表 的代表. 經常只要數字索引獲取元素, 但其實它還可以給元素命名:

>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'

切片

列表中是以0作為第一個元素的下標, 切片可以根據下標提取某一個片段.

s[a:b:c] 的形式對 sab 之間以 c 為間隔取值。c 的值還可以為負, 負值意味著反向取值.

>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

第三章: 字典和集合

dict 型別不但在各種程式裡廣泛使用, 它也是 Python 語言的基石. 正是因為 dict 型別的重要, Python 對其的實現做了高度的優化, 其中最重要的原因就是背後的「雜湊表」 set(集合)和dict一樣, 其實現基礎也是依賴於雜湊表.

雜湊表也叫雜湊表, 對於dict型別, 它的key必須是可雜湊的資料型別. 什麼是可雜湊的資料型別呢, 它的官方解釋是:

如果一個物件是可雜湊的,那麼在這個物件的生命週期中,它的雜湊值是不變
的,而且這個物件需要實現 __hash__() 方法。另外可雜湊物件還要有
__qe__() 方法,這樣才能跟其他鍵做比較。如果兩個可雜湊物件是相等的,那麼它們的雜湊值一定是一樣的……

str, bytes, frozenset數值 都是可雜湊型別.

字典推導式

DIAL_CODE = [
    (86, 'China'),
    (91, 'India'),
    (7, 'Russia'),
    (81, 'Japan'),
]

### 利用字典推導快速生成字典
country_code = {country: code for code, country in DIAL_CODE}
print(country_code)

'''
OUT:
{'China': 86, 'India': 91, 'Russia': 7, 'Japan': 81}
'''

defaultdict:處理找不到的鍵的一個選擇

當某個鍵不在對映裡, 我們也希望也能得到一個預設值. 這就是 defaultdict , 它是 dict 的子類, 並實現了 __missing__ 方法.

import collections
index = collections.defaultdict(list)
for item in nums:
    key = item % 2
    index[key].append(item)

字典的變種

標準庫裡 collections 模組中,除了 defaultdict 之外的不同對映型別:

  • OrderDict: 這個型別在新增鍵的時候,會儲存順序,因此鍵的迭代順序總是一致的
  • ChainMap: 該型別可以容納數個不同的對映對像,在進行鍵的查詢時,這些物件會被當做一個整體逐個查詢,直到鍵被找到為止 pylookup = ChainMap(locals(), globals())
  • Counter: 這個對映型別會給鍵準備一個整數技術器,每次更行一個鍵的時候都會增加這個計數器,所以這個型別可以用來給雜湊表物件計數,或者當成多重集來用.
import collections
ct = collections.Counter('abracadabra')
print(ct)   # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
ct.update('aaaaazzz')
print(ct)   # Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(ct.most_common(2)) # [('a', 10), ('z', 3)]
  • UserDict: 這個類其實就是把標準 dict 用純 Python 又實現了一遍
import collections
class StrKeyDict(collections.UserDict):
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]
        
    def __contains__(self, key):
        return str(key) in self.data
        
    def __setitem__(self, key, item):
        self.data[str(key)] = item

不可變對映型別

說到不可變, 第一想到的肯定是元組, 但是對於字典來說, 要將key和value的對應關係變成不可變, types 模組的 MappingProxyType 可以做到:

from types import MappingProxyType
d = {1:'A'}
d_proxy = MappingProxyType(d)
d_proxy[1]='B' # TypeError: 'mappingproxy' object does not support item assignment

d[2] = 'B'
print(d_proxy) # mappingproxy({1: 'A', 2: 'B'})

d_proxy 是動態的, 也就是說對 d 所做的任何改動都會反饋到它上面.

集合論

集合的本質是許多唯一物件的聚集. 因此, 集合可以用於去重. 集合中的元素必須是可雜湊的, 但是 set 本身是不可雜湊的, 而 frozenset 本身可以雜湊.

集合具有唯一性, 與此同時, 集合還實現了很多基礎的中綴運算子. 給定兩個集合 a 和 b, a | b
回的是它們的合集, a & b 得到的是交集, 而 a - b 得到的是差集.

合理的利用這些特性, 不僅能減少程式碼的數量, 更能增加執行效率.

# 集合的建立
s = set([1, 2, 2, 3])

# 空集合
s = set()

# 集合字面量
s = {1, 2}

# 集合推導
s = {chr(i) for i in range(23, 45)}

第四章: 文字和位元組序列

本章討論了文字字串和位元組序列, 以及一些編碼上的轉換. 本章討論的 str 指的是python3下的.

字元問題

字串是個比較簡單的概念: 一個字串是一個字元序列. 但是關於 "字元" 的定義卻五花八門, 其中, "字元" 的最佳定義是 Unicode 字元 . 因此, python3中的 str 物件中獲得的元素就是 unicode 字元.

把碼位轉換成位元組序列的過程就是 編碼, 把位元組序列轉換成碼位的過程就是 解碼 :

>>> s = 'café'
>>> len(s)
4 
>>> b = s.encode('utf8') 
>>> b
b'caf\xc3\xa9'
>>> len(b)
5 
>>> b.decode('utf8') #'café

碼位可以認為是人類可讀的文字, 而字元序列則可以認為是對機器更友好. 所以要區分 .decode().encode() 也很簡單. 從位元組序列到人類能理解的文字就是解碼(decode). 而把人類能理解的變成人類不好理解的位元組序列就是編碼(encode).

位元組概要

python3有兩種位元組序列, 不可變的 bytes 型別和可變的 bytearray 型別. 位元組序列中的各個元素都是介於 [0, 255] 之間的整數.

處理編碼問題

python自帶了超過100中編解碼器. 每個編解碼器都有一個名稱, 甚至有的會有一些別名, 如 utf_8 就有 utf8, utf-8, U8 這些別名.

如果字元序列和預期不符, 在進行解碼或編碼時容易丟擲 Unicode*Error 的異常. 造成這種錯誤是因為目標編碼中沒有定義某個字元(沒有定義某個碼位對應的字元), 這裡說說解決這類問題的方式.

  • 使用python3, python3可以避免95%的字元問題.
  • 主流編碼嘗試下: latin1, cp1252, cp437, gb2312, utf-8, utf-16le
  • 留意BOM頭部 b'\xff\xfe' , UTF-16編碼的序列開頭也會有這幾個額外位元組.
  • 找出序列的編碼, 建議使用 codecs 模組

規範化unicode字串

s1 = 'café'
s2 = 'caf\u00e9'

這兩行程式碼完全等價. 而有一種是要避免的是, 在Unicode標準中 ée\u0301 這樣的序列叫 "標準等價物". 這種情況用NFC使用最少的碼位構成等價的字串:

>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False

改進後:

>>> from unicodedata import normalize
>>> s1 = 'café' # 把"e"和重音符組合在一起
>>> s2 = 'cafe\u0301' # 分解成"e"和重音符
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) == normalize('NFC', s2)
True
>>> normalize('NFD', s1) == normalize('NFD', s2)
True

unicode文字排序

對於字串來說, 比較的碼位. 所以在非 ascii 字元時, 得到的結果可能會不盡人意.

第五章: 一等函式

在python中, 函式是一等物件. 程式語言把 "一等物件" 定義為滿足下列條件:

  • 在執行時建立
  • 能賦值給變數或資料結構中的元素
  • 能作為引數傳給函式
  • 能作為函式的返回結果

在python中, 整數, 字串, 列表, 字典都是一等物件.

把函式視作物件

Python即可以函數語言程式設計,也可以物件導向程式設計. 這裡我們建立了一個函式, 然後讀取它的 __doc__ 屬性, 並且確定函式物件其實是 function 類的例項:

def factorial(n):
    '''
    return n
    '''
    return 1 if n < 2 else n * factorial(n-1)

print(factorial.__doc__)
print(type(factorial))
print(factorial(3))

'''
OUT

    return n

<class 'function'>
6
'''

高階函式

高階函式就是接受函式作為引數, 或者把函式作為返回結果的函式. 如 map, filter , reduce 等.

比如呼叫 sorted 時, 將 len 作為引數傳遞:

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
# ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

匿名函式

lambda 關鍵字是用來建立匿名函式. 匿名函式一些限制, 匿名函式的定義體只能使用純表示式. 換句話說, lambda 函式內不能賦值, 也不能使用while和try等語句.

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])
# ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

可呼叫物件

除了使用者定義的函式, 呼叫運算子即 () 還可以應用到其他物件上. 如果像判斷物件能否被呼叫, 可以使用內建的 callable() 函式進行判斷. python的資料模型中有7種可是可以被呼叫的:

  • 使用者定義的函式: 使用def語句或lambda表示式建立
  • 內建函式:如len
  • 內建方法:如dict.get
  • 方法:在類定義體中的函式
  • 類的例項: 如果類定義了 __call__ , 那麼它的例項可以作為函式呼叫.
  • 生成器函式: 使用 yield 關鍵字的函式或方法.

從定位引數到僅限關鍵字引數

就是可變引數和關鍵字引數:

def fun(name, age, *args, **kwargs):
    pass

其中 *args**kwargs 都是可迭代物件, 展開後對映到單個引數. args是個元組, kwargs是字典.

第六章: 使用一等函式實現設計模式

雖然設計模式與語言無關, 但這並不意味著每一個模式都能在每一個語言中使用. Gamma 等人合著的 《設計模式:可複用物件導向軟體的基礎》 一書中有 23 個模式, 其中有 16 個在動態語言中"不見了, 或者簡化了".

這裡不舉例設計模式, 因為書裡的模式不常用.

第七章: 函式裝飾器和閉包

函式裝飾器用於在原始碼中“標記”函式,以某種方式增強函式的行為。這是一項強大的功
能,但是若想掌握,必須理解閉包。

修飾器和閉包經常在一起討論, 因為修飾器就是閉包的一種形式. 閉包還是回撥式非同步程式設計和函數語言程式設計風格的基礎.

裝飾器基礎知識

裝飾器是可呼叫的物件, 其引數是另一個函式(被裝飾的函式). 裝飾器可能會處理被
裝飾的函式, 然後把它返回, 或者將其替換成另一個函式或可呼叫物件.

@decorate
def target():
    print('running target()')

這種寫法與下面寫法完全等價:

def target():
    print('running target()')
target = decorate(target)

裝飾器是語法糖, 它其實是將函式作為引數讓其他函式處理. 裝飾器有兩大特徵:

  • 把被裝飾的函式替換成其他函式
  • 裝飾器在載入模組時立即執行

要理解立即執行看下等價的程式碼就知道了, target = decorate(target) 這句呼叫了函式. 一般情況下裝飾函式都會將某個函式作為返回值.

變數作用域規則

要理解裝飾器中變數的作用域, 應該要理解閉包, 我覺得書裡將閉包和作用域的順序換一下比較好. 在python中, 一個變數的查詢順序是 LEGB (L:Local 區域性環境,E:Enclosing 閉包,G:Global 全域性,B:Built-in 內建).

base = 20
def get_compare():
    base = 10
    def real_compare(value):
        return value > base
    return real_compare
    
compare_10 = get_compare()
print(compare_10(5))

在閉包的函式 real_compare 中, 使用的變數 base 其實是 base = 10 的. 因為base這個變數在閉包中就能命中, 而不需要去 global 中獲取.

閉包

閉包其實挺好理解的, 當匿名函式出現的時候, 才使得這部分難以掌握. 簡單簡短的解釋閉包就是:

名字空間與函式捆綁後的結果被稱為一個閉包(closure).

這個名字空間就是 LEGB 中的 E . 所以閉包不僅僅是將函式作為返回值. 而是將名字空間和函式捆綁後作為返回值的. 多少人忘了理解這個 "捆綁" , 不知道變數最終取的哪和哪啊. 哎.

標準庫中的裝飾器

python內建了三個用於裝飾方法的函式: propertyclassmethodstaticmethod .
這些是用來豐富類的.

class A(object):
    @property
    def age():
        return 12

第八章: 物件引用、可變性和垃圾回收

變數不是盒子

很多人把變數理解為盒子, 要存什麼資料往盒子裡扔就行了.

a = [1,2,3]
b = a 
a.append(4)
print(b) # [1, 2, 3, 4]

變數 ab 引用同一個列表, 而不是那個列表的副本. 因此賦值語句應該理解為將變數和值進行引用的關係而已.

標識、相等性和別名

要知道變數a和b是否是同一個值的引用, 可以用 is 來進行判斷:

>>> a = b = [4,5,6]
>>> c = [4,5,6]
>>> a is b
True
>>> x is c
False

如果兩個變數都是指向同一個物件, 我們通常會說變數是另一個變數的 別名 .

在==和is之間選擇
運算子 == 是用來判斷兩個物件值是否相等(注意是物件值). 而 is 則是用於判斷兩個變數是否指向同一個物件, 或者說判斷變數是不是兩一個的別名, is 並不關心物件的值. 從使用上, == 使用比較多, 而 is 的執行速度比較快.

預設做淺複製

l1 = [3, [55, 44], (7, 8, 9)]

l2 = list(l1) # 通過構造方法進行復制 
l2 = l1[:]  #也可以這樣想寫
>>> l2 == l1
True
>>> l2 is l1
False

儘管 l2 是 l1 的副本, 但是複製的過程是先複製(即複製了最外層容器,副本中的元素是源容器中元素的引用). 因此在操作 l2[1] 時, l1[1] 也會跟著變化. 而如果列表中的所有元素是不可變的, 那麼就沒有這樣的問題, 而且還能節省記憶體. 但是, 如果有可變元素存在, 就可能造成意想不到的問題.

python標準庫中提供了兩個工具 copydeepcopy . 分別用於淺拷貝與深拷貝:

import copy
l1 = [3, [55, 44], (7, 8, 9)]

l2 = copy.copy(l1)
l2 = copy.deepcopy(l1)

函式的引數做引用時

python中的函式引數都是採用共享傳參. 共享傳參指函式的各個形式引數獲得實參中各個引用的副本. 也就是說, 函式內部的形參
是實參的別名.

這種方案就是當傳入引數是可變物件時, 在函式內對引數的修改也就是對外部可變物件進行修改. 但這種引數試圖重新賦值為一個新的物件時則無效, 因為這只是相當於把引數作為另一個東西的引用, 原有的物件並不變. 也就是說, 在函式內, 引數是不能把一個物件替換成另一個物件的.

不要使用可變型別作為引數的預設值

引數預設值是個很棒的特性. 對於開發者來說, 應該避免使用可變物件作為引數預設值. 因為如果引數預設值是可變物件, 而且修改了它的內容, 那麼後續的函式呼叫上都會收到影響.

del和垃圾回收

在python中, 當一個物件失去了最後一個引用時, 會當做垃圾, 然後被回收掉. 雖然python提供了 del 語句用來刪除變數. 但實際上只是刪除了變數和物件之間的引用, 並不一定能讓物件進行回收, 因為這個物件可能還存在其他引用.

在CPython中, 垃圾回收主要用的是引用計數的演算法. 每個物件都會統計有多少引用指向自己. 當引用計數歸零時, 意味著這個物件沒有在使用, 物件就會被立即銷燬.

符合Python風格的物件

得益於 Python 資料模型,自定義型別的行為可以像內建型別那樣自然。實現如此自然的
行為,靠的不是繼承,而是鴨子型別(duck typing):我們只需按照預定行為實現物件所
需的方法即可。

物件表示形式

每門物件導向的語言至少都有一種獲取物件的字串表示形式的標準方式。Python 提供了
兩種方式。

  • repr() : 以便於開發者理解的方式返回物件的字串表示形式。
  • str() : 以便於使用者理解的方式返回物件的字串表示形式。

classmethod 與 staticmethod

這兩個都是python內建提供了裝飾器, 一般python教程都沒有提到這兩個裝飾器. 這兩個都是在類 class 定義中使用的, 一般情況下, class 裡面定義的函式是與其類的例項進行繫結的. 而這兩個裝飾器則可以改變這種呼叫方式.

先來看看 classmethod , 這個裝飾器不是操作例項的方法, 並且將類本身作為第一個引數. 而 staticmethod 裝飾器也會改變方法的呼叫方式, 它就是一個普通的函式,

classmethodstaticmethod 的區別就是 classmethod 會把類本身作為第一個引數傳入, 其他都一樣了.

看看例子:

>>> class Demo:
... @classmethod
... def klassmeth(*args):
...     return args
... @staticmethod
... def statmeth(*args):
...     return args
...
>>> Demo.klassmeth()
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth()
()
>>> Demo.statmeth('spam')
('spam',)

格式化顯示

內建的 format() 函式和 str.format() 方法把各個型別的格式化方式委託給相應的 .__format__(format_spec) 方法. format_spec 是格式說明符,它是:

  • format(my_obj, format_spec) 的第二個引數
  • str.format() 方法的格式字串,{} 裡代換欄位中冒號後面的部分

Python的私有屬性和"受保護的"屬性

python中對於例項變數沒有像 private 這樣的修飾符來建立私有屬性, 在python中, 有一個簡單的機制來處理私有屬性.

class A:
    def __init__(self):
        self.__x = 1

a = A()
print(a.__x) # AttributeError: 'A' object has no attribute '__x'

print(a.__dict__)

如果屬性以 __name兩個下劃線為字首, 尾部最多一個下劃線 命名的例項屬性, python會把它名稱前面加一個下劃線加類名, 再放入 __dict__ 中, 以 __name 為例, 就會變成 _A__name .

名稱改寫算是一種安全措施, 但是不能保證萬無一失, 它能避免意外訪問, 但不能阻止故意做壞事.

只要知道私有屬性的機制, 任何人都能直接讀取和改寫私有屬性. 因此很多python程式設計師嚴格規定: 遵守使用一個下劃線標記物件的私有屬性 . Python 直譯器不會對使用單個下劃線的屬性名做特殊處理, 由程式設計師自行控制, 不在類外部訪問這些屬性. 這種方法也是所推薦的, 兩個下劃線的那種方式就不要再用了. 引用python大神的話:

絕對不要使用兩個前導下劃線,這是很煩人的自私行為。如果擔心名稱衝突,應該明
確使用一種名稱改寫方式(如 _MyThing_blahblah)。這其實與使用雙下劃線一
樣,不過自己定的規則比雙下劃線易於理解。

Python中的把使用一個下劃線字首標記的屬性稱為"受保護的"屬性

使用 slots 類屬性節省空間

預設情況下, python在各個例項中, 用 __dict__ 的字典儲存例項屬性. 因此例項的屬性是動態變化的, 可以在執行期間任意新增屬性. 而字典是消耗記憶體比較大的結構. 因此當物件的屬性名稱確定時, 使用 __slots__ 可以節約記憶體.

class Vector2d:
    __slots__ = ('__x', '__y')
    typecode = 'd'
    # 下面是各個方法(因排版需要而省略了)

在類中定義 __slots__ 屬性的目的是告訴直譯器:"這個類中的所有例項屬性都在這兒
了!" 這樣, Python 會在各個例項中使用類似元組的結構儲存例項變數, 從而避免使用消
耗記憶體的 __dict__ 屬性. 如果有數百萬個例項同時活動, 這樣做能節省大量記憶體.

第十章: 序列的修改、雜湊和切片

協議和鴨子型別

在python中, 序列型別不需要使用繼承, 只需要符合序列協議的方法即可. 這裡的協議就是實現 __len____getitem__ 兩個方法. 任何類, 只要實現了這兩個方法, 它就滿足了序列操作, 因為它的行為像序列.

協議是非正式的, 沒有強制力, 因此你知道類的具體使用場景, 通常只要實現一個協議的部分. 例如, 為了支援迭代, 只需實現 __getitem__ 方法, 沒必要提供 __len__ 方法, 這也就解釋了 鴨子型別 :

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,
那麼這隻鳥就可以被稱為鴨子

可切片的序列

切片(Slice)是用來獲取序列某一段範圍的元素. 切片操作也是通過 __getitem__ 來完成的:

class Vector:
    # 省略了很多行
    # ...
    def __len__(self):
        return len(self._components)
    # 省略了很多
    def __getitem__(self, index):
        cls = type(self)  # 獲取例項的型別

        if isinstance(index, slice):  # 如果index引數值是切片的物件
            # 呼叫Vector的構造方法,建立一個新的切片後的Vector類
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):  # 如果引數是整數型別
            return self._components[index]  # 我們就對陣列進行切片
        else:  # 否則我們就丟擲異常
            msg = '{cls.__name__} indices must be integers'
            raise TypeError(msg.format(cls=cls))

動態存取屬性

通過訪問分量名來獲取屬性:

shortcut_names = 'xyzt'
def __getattr__(self, name):
        cls = type(self) # 獲取型別
        if len(name) == 1: # 判斷屬性名是否在我們定義的names中
            pos = cls.shortcut_names.find(name)
            if 0 <= pos < len(self._components):
                return self._components[pos]
        msg = '{} objects has no attribute {}'
        raise AttributeError(msg.format(cls, name))
        
test = Vector([3, 4, 5])
print(test.x)
print(test.y)
print(test.z)
print(test.c)

雜湊和快速等值測試

實現 __hash__ 方法。加上現有的 __eq__ 方法,這會把例項變成可雜湊的物件.

當序列是多維是時候, 我們有一個效率更高的方法:

def __eq__(self, other):
        if len(self) != len(other): # 首先判斷長度是否相等
            return False
            for a, b in zip(self, other): # 接著逐一判斷每個元素是否相等 
                if a != b:
                    return False
        return True
        
# 我們也可以寫的漂亮點

def __eq__(self, other):
    return (len(self) == len(other)) and all(a == b for a, b in zip(self, other))

第十一章: 介面:從協議到抽象基類

這些協議定義為非正式的介面, 是讓程式語言實現多型的方式. 在python中, 沒有 interface 關鍵字, 而且除了抽象基類, 每個類都有介面: 所有類都可以自行實現 __getitem____add__ .

有寫規定則是程式設計師在開發過程中慢慢總結出來的, 如受保護的屬性命名採用單個前導下劃線, 還有一些編碼規範之類的.

協議是介面, 但不是正式的, 這些規定並不是強制性的, 一個類可能只實現部分介面, 這是允許的.

既然有非正式的協議, 那麼有沒有正式的協議呢? 有, 抽象基類就是一種強制性的協議.

抽象基類要求其子類需要實現定義的某個介面, 且抽象基類不能例項化.

Python文化中的介面和協議

引入抽象基類之前, python就已經非常成功了, 即使現在也很少使用抽象基類. 通過鴨子型別和協議, 我們把協議定義為非正式介面, 是讓python實現多型的方式.

另一邊面, 不要覺得把公開資料屬性放入物件的介面中不妥, 如果需要, 總能實現讀值和設值方法, 把資料屬性變成特性. 物件公開方法的自己, 讓物件在系統中扮演特定的角色. 因此, 介面是實現特定角色的方法集合.

序列協議是python最基礎的協議之一, 即便物件只實現那個協議最基本的一部分, 直譯器也會負責地處理.

水禽和抽象基類

鴨子型別在很多情況下十分有用, 但是隨著發展, 通常由了更好的方式.

近代, 屬和種基本是根據表型系統學分類的, 鴨科屬於水禽, 而水禽還包括鵝, 鴻雁等. 水禽是對某一類表現一致進行的分類, 他們有一些統一"描述"部分.

因此, 根據分類的演化, 需要有個水禽型別, 只要 cls 是抽象基類, 即 cls 的元類是 abc.ABCMeta , 就可以使用 isinstance(obj, cls) 來進行判斷.

與具類相比, 抽象基類有很多理論上的優點, 被註冊的類必須滿足抽象基類對方法和簽名的要求, 更重要的是滿足底層語義契約.

標準庫中的抽象基類

大多數的標準庫的抽象基類在 collections.abc 模組中定義. 少部分在 numbersio 包中有一些抽象基類. 標準庫中有兩個 abc 模組, 這裡只討論 collections.abc .

這個模組中定義了 16 個抽象基類.

Iterable、Container 和 Sized
各個集合應該繼承這三個抽象基類,或者至少實現相容的協議。Iterable 通過 __iter__ 方法支援迭代,Container 通過 __contains__ 方法支援 in 運算子,Sized
通過 __len__ 方法支援 len() 函式。

Sequence、Mapping 和 Set
  這三個是主要的不可變集合型別,而且各自都有可變的子類。

MappingView
  在 Python3 中,對映方法 .items().keys().values() 返回的物件分別是
ItemsView、KeysView 和 ValuesView 的例項。前兩個類還從 Set 類繼承了豐富的接
口。

Callable 和 Hashable
  這兩個抽象基類與集合沒有太大的關係,只不過因為 collections.abc 是標準庫中
定義抽象基類的第一個模組,而它們又太重要了,因此才把它們放到 collections.abc
模組中。我從未見過 CallableHashable 的子類。這兩個抽象基類的主要作用是為內
置函式 isinstance 提供支援,以一種安全的方式判斷物件能不能呼叫或雜湊。

Iterator
  注意它是 Iterable 的子類。
  

第十二章: 繼承的優缺點

很多人覺得多重繼承得不償失, 那些不支援多繼承的程式語言好像也沒什麼損失.

子類化內建型別很麻煩

python2.2 以前, 內建型別(如list, dict)是不能子類化的. 它們是不能被其他類所繼承的, 原因是內建型別是C語言實現的, 不會呼叫使用者定義的類覆蓋的方法.

至於內建型別的子類覆蓋的方法會不會隱式呼叫, CPython 官方也沒有制定規則. 基本上, 內建型別的方法不會呼叫子類覆蓋的方法. 例如, dict 的子類覆蓋的 __getitem__ 方法不會覆蓋內建型別的 get() 方法呼叫.

多重繼承和方法解析順序

任何實現多重繼承的語言都要處理潛在的命名衝突,這種衝突由不相關的祖先類實現同名
方法引起。這種衝突稱為“菱形問題”,如圖.

20171010144742.png

Python 會按照特定的順序遍歷繼承
圖。這個順序叫方法解析順序(Method Resolution Order,MRO)。類都有一個名為
mro 的屬性,它的值是一個元組,按照方法解析順序列出各個超類,從當前類一直
向上,直到 object 類。

20171010145104.png

第十三章: 正確過載運算子

在python中, 大多數的運算子是可以過載的, 如 == 對應了 __eq__ , + 對應 __add__ .

某些運算子不能過載, 如 is, and, or, and.

第十四章: 可迭代的物件、迭代器和生成器

迭代是資料處理的基石. 掃描記憶體中放不下的資料集時, 我們要找到一種惰性獲取資料的方式, 即按需一次獲取一個資料. 這就是 迭代器模式 .

python中有 yield 關鍵字, 用於構建 生成器(generator), 其作用用於迭代器一樣.

所有的生成器都是迭代器, 因為生成器完全實現了迭代器的介面.

檢查物件 x 是否迭代, 最準確的方法是呼叫 iter(x) , 如果不可迭代, 則丟擲 TypeError 異常. 這個方法比 isinstance(x, abc.Iterable) 更準確, 因為它還考慮到遺留的 __getitem__ 方法.

可迭代的物件與迭代器的對比

我們需要對可迭代的物件進行一下定義:

使用 iter 內建函式可以獲取迭代器的物件。如果物件實現了能返回迭代器的
iter 方法,那麼物件就是可迭代的。序列都可以迭代;實現了 getitem
法,而且其引數是從零開始的索引,這種物件也可以迭代。

我們要明確可迭代物件和迭代器之間的關係: 從可迭代的物件中獲取迭代器.

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

  • __next__: 返回下一個可用的元素, 如果沒有元素了, 丟擲 StopIteration 異常.
  • __iter__: 返回 self , 以便咋應該使用可迭代物件的地方使用迭代器.

典型的迭代器

為了清楚地說明可迭代物件與迭代器之間的重要區別, 我們將兩者分開, 寫成兩個類:

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) # 建立一個迭代器


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 # 索引+1
        return word # 返回單詞

    def __iter__(self):
        return self     # 返回self

這個例子主要是為了區分可迭代物件和迭代器, 這種情況工作量一般比較大, 程式設計師也不願這樣寫.

構建可迭代物件和迭代器經常會出現錯誤, 原因是混淆了二者. 要知道, 可迭代的物件有個 __iter__ 方法, 每次都例項化一個新的迭代器; 而迭代器是要實現 __next__ 方法, 返回單個元素, 同時還要提供 __iter__ 方法返回迭代器本身.

可迭代物件一定不能是自身的迭代器. 也就是說, 可迭代物件必須實現 __iter__ 方法, 但不能實現 __next__ 方法.

小結下, 迭代器可以迭代, 但是可迭代物件不是迭代器.

生成器函式

實現相同功能, 覆蓋python習慣的方式, 就是用生成器代替迭代器 SentenceIterator . 將上個例子改成生成器的方式:

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 word in self.words:  # 迭代例項的words
            yield word  # 生成單詞
        return

在這個例子中, 迭代器其實就是生成器物件, 每次呼叫 __iter__ 都會自動建立, 因為這裡的 __iter__ 方法是生成器函式.

生成器函式的工作原理
只要python函式的定義體中有 yield 關鍵字, 改函式就是生成器函式. 呼叫生成器函式時, 會返回一個生成器物件. 也就是說, 生成器函式是生成器工廠.

普通函式與生成器函式的唯一區別就是, 生成器函式裡面有 yield 關鍵字.

生成器函式會建立一個生成器物件, 包裝生成器函式的定義體. 吧生成器傳給 next(...) 函式時, 生成器函式會向前, 執行函式體中下一個 yield 語句, 返回產出的值, 並在函式定義體的當前位置暫停.

惰性實現

惰性的方式就是索性把所有資料都產出, 這是區別於 next(...) 一次生成一次元素的.

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

生成器表示式

生成器表示式可以理解為列表推導的惰性版本: 不會迫切地構建列表, 而是返回一個生成器, 按需惰性生成元素. 也就是, 如果列表推導是產出列表的工廠, 那麼生成器表示式就是產出生成器的工廠.

def gen_AB():
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end.')

res1 = [x*3 for x in gen_AB()]
for i in res1:
    print('-->', i)

可以看出, 生成器表示式會產出生成器, 因此可以使用生成器表示式減少程式碼:

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 (match.group() for match in RE_WORD.finditer(self.text))

這裡的 __iter__ 不是生成器函式了, 而是使用生成器表示式構建生成器, 最終的效果一樣. 呼叫 __iter__ 方法會得到一個生成器物件.

生成器表示式是語法糖, 完全可以替換生成器函式.

標準庫中的生成器函式

標準庫提供了很多生成器, 有用於逐行迭代純文字檔案的物件, 還有出色的 os.walk 函式. 這個函式在遍歷目錄樹的過程中產出檔名, 因此遞迴搜尋檔案系統像 for 迴圈那樣簡單.

標準庫中的生成器大多在 itertoolsfunctools 中, 表格中不代表所有.

用於過濾的生成器函式

模組 函式 說明
itertools compress(it, selector_it) 並行處理兩個可迭代的物件;如果 selector_it 中的元素是真值,產出 it 中對應的元素
itertools dropwhile(predicate, it) 處理 it,跳過 predicate 的計算結果為真值的元素,然後產出剩下的各個元素(不再進一步檢查)
(內建) filter(predicate, it) 把 it 中的各個元素傳給 predicate,如果 predicate(item) 返回真值,那麼產出對應的元素;如果 predicate 是 None,那麼只產出真值元素

用於對映的生成器函式

模組 函式 說明
itertools accumulate(it, [func]) 產出累積的總和;如果提供了 func,那麼把前兩個元素傳給它,然後把計算結果和下一個元素傳給它,以此類推,最後產出結果
(內建) enumerate(iterable, start=0) 產出由兩個元素組成的元組,結構是 (index, item),其中 index 從 start 開始計數,item 則從 iterable 中獲取
(內建) map(func, it1, [it2, ..., itN]) 把 it 中的各個元素傳給func,產出結果;如果傳入 N 個可迭代的物件,那麼 func 必須能接受 N 個引數,而且要並行處理各個可迭代的物件

合併多個可迭代物件的生成器函式

模組 函式 說明
itertools chain(it1, ..., itN) 先產出 it1 中的所有元素,然後產出 it2 中的所有元素,以此類推,無縫連線在一起
itertools chain.from_iterable(it) 產出 it 生成的各個可迭代物件中的元素,一個接一個,無縫連線在一起;it 應該產出可迭代的元素,例如可迭代的物件列表
(內建) zip(it1, ..., itN) 並行從輸入的各個可迭代物件中獲取元素,產出由 N 個元素組成的元組,只要有一個可迭代的物件到頭了,就默默地停止

新的句法:yield from

如果生成器函式需要產出另一個生成器生成的值, 傳統的方式是巢狀的 for 迴圈, 例如, 我們要自己實現 chain 生成器:

>>> def chain(*iterables):
...     for it in iterables:
...         for i in it:
...             yield i
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

chain 生成器函式把操作依次交給接收到的可迭代物件處理. 而改用 yield from 語句可以簡化:

>>> def chain(*iterables):
...     for i in iterables:
...         yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

可以看出, yield from i 取代一個 for 迴圈. 並且讓程式碼讀起來更流暢.

可迭代的歸約函式

有些函式接受可迭代物件, 但僅返回單個結果, 這類函式叫規約函式.

模組 函式 說明
(內建) sum(it, start=0) it 中所有元素的總和,如果提供可選的 start,會把它加上(計算浮點數的加法時,可以使用 math.fsum 函式提高精度)
(內建) all(it) it 中的所有元素都為真值時返回 True,否則返回 False;all([]) 返回 True
(內建) any(it) 只要 it 中有元素為真值就返回 True,否則返回 False;any([]) 返回 False
(內建) max(it, [key=,] [default=]) 返回 it 中值最大的元素;*key 是排序函式,與 sorted 函式中的一樣;如果可迭代的物件為空,返回 default
functools reduce(func, it, [initial]) 把前兩個元素傳給 func,然後把計算結果和第三個元素傳給 func,以此類推,返回最後的結果;如果提供了 initial,把它當作第一個元素傳入

第十五章: 上下文管理器和 else 塊

本章討論的是其他語言不常見的流程控制特性, 正因如此, python新手往往忽視或沒有充分使用這些特性. 下面討論的特性有:

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

with 語句會設定一個臨時的上下文, 交給上下文管理器物件控制, 並且負責清理上下文. 這麼做能避免錯誤並減少程式碼量, 因此API更安全, 而且更易於使用. 除了自動關閉檔案之外, with 塊還有其他很多用途.

else 子句先做這個,選擇性再做那個的作用.

if語句之外的else塊

這裡的 else 不是在在 if 語句中使用的, 而是在 for while try 語句中使用的.

for i in lst:
    if i > 10:
        break
else:
    print("no num bigger than 10")

else 子句的行為如下:

  • for : 僅當 for 迴圈執行完畢時(即 for 迴圈沒有被 break 語句中止)才執行 else 塊。
  • while : 僅當 while 迴圈因為條件為假值而退出時(即 while 迴圈沒有被 break 語句中止)才執行 else 塊。
  • try : 僅當 try 塊中沒有異常丟擲時才執行 else 塊。

在所有情況下, 如果異常或者 return , breakcontinue 語句導致控制權跳到了複合語句的住塊外, else 子句也會被跳過.

這一些情況下, 使用 else 子句通常讓程式碼更便於閱讀, 而且能省去一些麻煩, 不用設定控制標誌作用的變數和額外的if判斷.

上下文管理器和with塊

上下文管理器物件的目的就是管理 with 語句, with 語句的目的是簡化 try/finally 模式. 這種模式用於保證一段程式碼執行完畢後執行某項操作, 即便那段程式碼由於異常, return 或者 sys.exit() 呼叫而終止, 也會執行執行的操作. finally 子句中的程式碼通常用於釋放重要的資源, 或者還原臨時變更的狀態.

上下文管理器協議包含 __enter____exit__ 兩個方法. with 語句開始執行時, 會在上下文管理器上呼叫 __enter__ 方法, 待 with 語句執行結束後, 再呼叫 __exit__ 方法, 以此扮演了 finally 子句的角色.

with 最常見的例子就是確保關閉檔案物件.

上下文管理器呼叫 __enter__ 沒有引數, 而呼叫 __exit__ 時, 會傳入3個引數:

  • exc_type : 異常類(例如 ZeroDivisionError)
  • exc_value : 異常例項。有時會有引數傳給異常構造方法,例如錯誤訊息,這些引數可以使用 exc_value.args 獲取
  • traceback : traceback 物件

contextlib模組中的實用工具

在ptyhon的標準庫中, contextlib 模組中還有一些類和其他函式,使用範圍更廣。

  • closing: 如果物件提供了 close() 方法,但沒有實現 __enter__/__exit__ 協議,那麼可以使用這個函式構建上下文管理器。
  • suppress: 構建臨時忽略指定異常的上下文管理器。
  • @contextmanager: 這個裝飾器把簡單的生成器函式變成上下文管理器,這樣就不用建立類去實現管理器協議了。
  • ContextDecorator: 這是個基類,用於定義基於類的上下文管理器。這種上下文管理器也能用於裝飾函式,在受管理的上下文中執行整個函式。
  • ExitStack: 這個上下文管理器能進入多個上下文管理器。with 塊結束時,ExitStack 按照後進先出的順序呼叫棧中各個上下文管理器的 __exit__ 方法。如果事先不知道 with 塊要進入多少個上下文管理器,可以使用這個類。例如,同時開啟任意一個檔案列表中的所有檔案。

顯然,在這些實用工具中,使用最廣泛的是 @contextmanager 裝飾器,因此要格外留心。這個裝飾器也有迷惑人的一面,因為它與迭代無關,卻要使用 yield 語句。

使用@contextmanager

@contextmanager 裝飾器能減少建立上下文管理器的樣板程式碼量, 因為不用定義 __enter____exit__ 方法, 只需要實現一個 yield 語句的生成器.

import sys
import contextlib
@contextlib.contextmanager
def looking_glass():

    original_write = sys.stdout.write
    def reverse_write(text):
        original_write(text[::-1])
    sys.stdout.write = reverse_write
    yield 'JABBERWOCKY'
    sys.stdout.write = original_write

with looking_glass() as f:
    print(f)        # YKCOWREBBAJ
    print("ABCD")   # DCBA
    

yield 語句起到了分割的作用, yield 語句前面的所有程式碼在 with 塊開始時(即直譯器呼叫 __enter__ 方法時)執行, yield 語句後面的程式碼在 with 塊結束時(即呼叫 __exit__ 方法時)執行.

第十六章: 協程

為了理解協程的概念, 先從 yield 來說. yield item 會產出一個值, 提供給 next(...) 呼叫方; 此外還會做出讓步, 暫停執行生成器, 讓呼叫方繼續工作, 直到需要使用另一個值時再呼叫 next(...) 從暫停的地方繼續執行.

從句子語法上看, 協程與生成器類似, 都是通過 yield 關鍵字的函式. 可是, 在協程中, yield 通常出現在表示式的右邊(datum = yield), 可以產出值, 也可以不產出(如果yield後面沒有表示式, 那麼會產出None). 協程可能會從呼叫方接收資料, 不過呼叫方把資料提供給協程使用的是 .send(datum) 方法. 而不是 next(...) . 通常, 呼叫方會把值推送給協程.

生成器呼叫方是一直索要資料, 而協程這是呼叫方可以想它傳入資料, 協程也不一定要產出資料.

不管資料如何流動, yield 都是一種流程控制工具, 使用它可以實現寫作式多工: 協程可以把控制器讓步給中心排程程式, 從而啟用其他的協程.

生成器如何進化成協程

協程的底層框架實現後, 生成器API中增加了 .send(value) 方法. 生成器的呼叫方可以使用 .send(...) 來傳送資料, 傳送的資料會變成生成器函式中 yield 表示式的值. 因此, 生成器可以作為協程使用. 除了 .send(...) 方法, 還新增了 .throw(...).close() 方法, 用來讓呼叫方丟擲異常和終止生成器.

用作協程的生成器的基本行為

>>> def simple_coroutine():
...     print('-> coroutine started')
...     x = yield
...     print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
...
StopIteration

yield 表示式中, 如果協程只需從呼叫那接受資料, 那麼產出的值是 None . 與建立生成器的方式一樣, 呼叫函式得到生成器物件. 協程都要先呼叫 next(...) 函式, 因為生成器還沒啟動, 沒在 yield 出暫定, 所以一開始無法傳送資料. 如果控制器流動到協程定義體末尾, 會像迭代器一樣丟擲 StopIteration 異常.

使用協程的好處是不用加鎖, 因為所有協程只在一個執行緒中執行, 他們是非搶佔式的. 協程也有一些狀態, 可以呼叫 inspect.getgeneratorstate(...) 來獲得, 協程都是這4個狀態中的一種:

  • 'GEN_CREATED' 等待開始執行。
  • 'GEN_RUNNING' 直譯器正在執行。
  • 'GEN_SUSPENDED' 在 yield 表示式處暫停。
  • 'GEN_CLOSED' 執行結束。

只有在多執行緒應用中才能看到這個狀態。此外,生成器物件在自己身上呼叫 getgeneratorstate 函式也行,不過這樣做沒什麼用。

為了更好理解繼承的行為, 來看看產生兩個值的協程:

>>> from inspect import getgeneratorstate
>>> def simple_coro2(a):
...     print('-> Started: a =', a)
...     b = yield a
...     print('-> Received: b =', b)
...     c = yield a + b
...     print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> getgeneratorstate(my_coro2) # 協程處於未啟動的狀態
'GEN_CREATED'
>>> next(my_coro2)              # 向前執行到yield表示式, 產出值 a, 暫停並等待 b 賦值
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2) # 協程處於暫停狀態
'GEN_SUSPENDED'
>>> my_coro2.send(28)           # 數字28發給協程, yield 表示式中 b 得到28, 協程向前執行, 產出 a + b 值
-> Received: b = 28
42
>>> my_coro2.send(99)           # 同理, c 得到 99, 但是由於協程終止, 導致生成器物件丟擲 StopIteration 異常
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2) # 協程處於終止狀態
'GEN_CLOSED'

關鍵的一點是, 協程在 yield 關鍵字所在的位置暫停執行. 對於 b = yield a 這行程式碼來說, 等到客戶端程式碼再啟用協程時才會設定 b 的值. 這種方式要花點時間才能習慣, 理解了這個, 才能弄懂非同步程式設計中 yield 的作用. 對於例項的程式碼中函式 simple_coro2 的執行過程可以分為三個階段:

20171011110645.png

示例:使用協程計算移動平均值

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

這是一個動態計算平均值的協程程式碼, 這個無限迴圈表明, 它會一直接收值然後生成結果. 只有當呼叫方在協程上呼叫 .close() 方法, 或者沒有該協程的引用時, 協程才會終止.

協程的好處是, 無需使用例項屬性或者閉包, 在多次呼叫之間都能保持上下文.

預激協程的裝飾器

如果沒有執行 next(...) , 協程沒什麼用. 為了簡化協程的用法, 有時會使用一個預激裝飾器.

from functools import wraps
def coroutine(func):
    """裝飾器:向前執行到第一個`yield`表示式,預激`func`"""
    @wraps(func)
    def primer(*args,**kwargs): # 呼叫 primer 函式時,返回預激後的生成器
        gen = func(*args,**kwargs) # 呼叫被裝飾的函式,獲取生成器物件。
        next(gen)               # 預激生成器
        return gen              # 返回生成器
    return primer

終止協程和異常處理

協程中未處理的異常會向上冒泡, 傳給 next() 函式或者 send() 的呼叫方. 如果這個異常沒有處理, 會導致協程終止.

>>> coro_avg.send(40)
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('spam') # 傳入會產生異常的值
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro_avg.send(60)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

這要求在協程內部要處理這些異常, 另外, 客戶端程式碼也可以顯示的傳送異常給協程, 方法是 throwclose :

coro_avg.throw(ZeroDivisionError)

協程內部如果不能處理這個異常, 就會導致協程終止.

close 是致使在暫停的 yield 表示式處丟擲 GeneratorExit 異常. 協程內部當然允許處理這個異常, 但收到這個異常時一定不能產出值, 不然直譯器會丟擲 RuntimeError 異常.

讓協程返回值

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total/count
        return (count, average)

coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
try:
    coro_avg.send(None)         # 傳送 None 讓協程終止
except StopIteration as exc:
    result = exc.value

為了返回值, 協程必須正常終止, 而正常終止的的協程會丟擲 StopIteration 異常, 因此需要呼叫方處理這個異常.

使用yield from

yield from 是全新的語法結構. 它的作用比 yield 多很多.

>>> def gen():
...     for c in 'AB':
...         yield c
...     for i in range(1, 3):
...         yield i
...
>>> list(gen())
['A', 'B', 1, 2]

可以改寫成:

>>> def gen():
...     yield from 'AB'
...     yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]

在生成器 gen 中使用 yield form subgen() 時, subgen 會得到控制權, 把產出的值傳給 gen 的呼叫方, 既呼叫方可以直接呼叫 subgen. 而此時, gen 會阻塞, 等待 subgen 終止.

yield from x 表示式對 x 物件所做的第一件事是, 呼叫 iter(x) 獲得迭代器. 因此, x 物件可以是任何可迭代物件.

這個語義過於複雜, 來看看作者 Greg Ewing 的解釋:

“把迭代器當作生成器使用,相當於把子生成器的定義體內聯在 yield from 表示式
中。此外,子生成器可以執行 return 語句,返回一個值,而返回的值會成為 yield
from 表示式的值。”

子生成器是從 yield from <iterable> 中獲得的生成器. 而後, 如果呼叫方使用 send() 方法, 其實也是直接傳給子生成器. 如果傳送的是 None , 那麼會呼叫子生成器的 __next__() 方法. 如果不是 None , 那麼會呼叫子生成器的 send() 方法. 當子生成器丟擲 StopIteration 異常, 那麼委派生成器恢復執行. 任何其他異常都會向上冒泡, 傳給委派生成器.

生成器在 return expr 表示式中會觸發 StopIteration 異常.

第十七章: 使用期物處理併發

"期物" 是什麼概念呢? 期物指一種物件, 表示非同步執行的操作. 這個概念是 concurrent.futures 模組和 asyncio 包的基礎.

示例:網路下載的三種風格

為了高效處理網路io, 需要使用併發, 因為網路有很高的延遲, 所以為了不浪費 CPU 週期去等待.

以一個下載網路 20 個圖片的程式看, 序列下載的耗時 7.18s . 多執行緒的下載耗時 1.40s, asyncio的耗時 1.35s . 兩個併發下載的指令碼之間差異不大, 當對於序列的來說, 快了很多.

阻塞型I/O和GIL

CPython直譯器不是執行緒安全的, 因此有全域性解釋鎖(GIL), 一次只允許使用一個執行緒執行 python 位元組碼, 所以一個python程式不能同時使用多個 CPU 核心.

python程式設計師編寫程式碼時無法控制 GIL, 然而, 在標準庫中所有執行阻塞型I/O操作的函式, 在登臺作業系統返回結果時都會釋放GIL. 這意味著IO密集型python程式能從中受益.

使用concurrent.futures模組啟動程式

一個python程式只有一個 GIL. 多個python程式就能繞開GIL, 因此這種方法就能利用所有的 CPU 核心. concurrent.futures 模組就實現了真正的平行計算, 因為它使用 ProcessPoolExecutor 把工作交個多個python程式處理.

ProcessPoolExecutorThreadPoolExecutor 類都實現了通用的 Executor 介面, 因此使用 concurrent.futures 能很輕鬆把基於執行緒的方案轉成基於程式的方案.

def download_many(cc_list):
    workers = min(MAX_WORKERS, len(cc_list))
    with futures.ThreadPoolExecutor(workers) as executor:
        res = executor.map(download_one, sorted(cc_list))

改成:

def download_many(cc_list):
    with futures.ProcessPoolExecutor() as executor:
        res = executor.map(download_one, sorted(cc_list))

ThreadPoolExecutor.__init__ 方法需要 max_workers 引數,指定執行緒池中執行緒的數量; 在 ProcessPoolExecutor 類中, 這個引數是可選的.

第十八章: 使用 asyncio 包處理併發

併發是指一次處理多件事。
並行是指一次做多件事。
二者不同,但是有聯絡。
一個關於結構,一個關於執行。
併發用於制定方案,用來解決可能(但未必)並行的問題。—— Rob Pike Go 語言的創造者之一

並行是指兩個或者多個事件在同一時刻發生, 而併發是指兩個或多個事件在同一時間間隔發生. 真正執行並行需要多個核心, 現在筆記本一般有 4 個 CPU 核心, 但是通常就有超過 100 個程式同時執行. 因此, 實際上大多數程式都是併發處理的, 而不是並行處理. 計算機始終執行著 100 多個程式, 確保每個程式都有機會取得發展, 不過 CPU 本身同時做的事情不會超過四件.

本章介紹 asyncio 包, 這個包使用事件迴圈驅動的協程實現併發. 這個庫有龜叔親自操刀. asyncio 大量使用 yield from 表示式, 因此不相容 python3.3 以下的版本.

執行緒與協程對比

一個藉由 threading 模組使用執行緒, 一個藉由 asyncio 包使用協程實現來進行比對.

import threading
import itertools
import time

def spin(msg, done):  # 這個函式會在單獨的執行緒中執行
    for char in itertools.cycle('|/-\\'):  # 這其實是個無限迴圈,因為 itertools.cycle 函式會從指定的序列中反覆不斷地生成元素
        status = char + ' ' + msg
        print(status)
        if done.wait(.1):  # 如果程式被通知等待, 那就退出迴圈
            break

def slow_function():  # 假設這是耗時的計算
    # pretend waiting a long time for I/O
    time.sleep(3)  # 呼叫 sleep 函式會阻塞主執行緒,不過一定要這麼做,以便釋放 GIL,建立從屬執行緒
    return 42

def supervisor():  # 這個函式設定從屬執行緒,顯示執行緒物件,執行耗時的計算,最後殺死執行緒。
    done = threading.Event()
    spinner = threading.Thread(target=spin,
                               args=('thinking!', done))
    print('spinner object:', spinner)  # 顯示從屬執行緒物件。輸出類似於 <Thread(Thread-1, initial)>
    spinner.start()  # 啟動從屬執行緒
    result = slow_function()  # 執行 slow_function 函式,阻塞主執行緒。同時,從屬執行緒以動畫形式顯示旋轉指標
    done.set()  # 改變 signal 的狀態;這會終止 spin 函式中的那個 for 迴圈
    spinner.join()  # 等待 spinner 執行緒結束
    return result

if __name__ == '__main__':
    result = supervisor()
    print('Answer:', result)

這是使用 threading 的案例, 讓子執行緒在 3 秒內不斷列印, 在python中, 沒有提供終止執行緒的API. 若想關閉執行緒, 必須給執行緒傳送訊息.

下面看看使用 @asyncio.coroutine 裝飾器替代協程, 實現相同的行為:

import asyncio
import itertools

@asyncio.coroutine  # 交給 asyncio 處理的協程要使用 @asyncio.coroutine 裝飾
def spin(msg):
    for char in itertools.cycle('|/-\\'):
        status = char + ' ' + msg
        print(status)
        try:
            yield from asyncio.sleep(.1)  # 使用 yield from asyncio.sleep(.1) 代替 time.sleep(.1),這樣的休眠不會阻塞事件迴圈。
        except asyncio.CancelledError:  # 如果 spin 函式甦醒後丟擲 asyncio.CancelledError 異常,其原因是發出了取消請求,因此退出迴圈。
            break

@asyncio.coroutine
def slow_function():  # slow_function 函式是協程,在用休眠假裝進行 I/O 操作時,使用 yield from 繼續執行事件迴圈。
    # pretend waiting a long time for I/O
    yield from asyncio.sleep(3)  # yield from asyncio.sleep(3) 表示式把控制權交給主迴圈,在休眠結束後恢復這個協程。
    return 42

@asyncio.coroutine
def supervisor():  # supervisor 函式也是協程
    spinner = asyncio.async(spin('thinking!'))  # asyncio.async(...) 函式排定 spin 協程的執行時間,使用一個 Task 物件包裝spin 協程,並立即返回。
    print('spinner object:', spinner)
    result = yield from slow_function()  # 驅動 slow_function() 函式。結束後,獲取返回值。
                                         # 同時,事件迴圈繼續執行,因為slow_function 函式最後使用 yield from asyncio.sleep(3) 表示式把控制權交回給了主迴圈。
    spinner.cancel()  # Task 物件可以取消;取消後會在協程當前暫停的 yield 處丟擲 asyncio.CancelledError 異常。協程可以捕獲這個異常,也可以延遲取消,甚至拒絕取消。
    return result

if __name__ == '__main__':
    loop = asyncio.get_event_loop()  # 獲取事件迴圈的引用
    result = loop.run_until_complete(supervisor())  # 驅動 supervisor 協程,讓它執行完畢;這個協程的返回值是這次呼叫的返回值。
    loop.close()
    print('Answer:', result)

asyncio 包使用的協程是比較嚴格的定義, 適合 asyncio API 的協程在定義體中必須使用 yield from , 而不是使用 yield . 此外, asyncio 的協程要由呼叫方驅動, 例如 asyncio.async(...) , 從而驅動協程. 最後由 @asyncio.coroutine 裝飾器應用在協程上.

這兩種 supervisor 實現之間的主要區別概述如下:

  • asyncio.Task 物件差不多與 threading.Thread 物件等效。“Task物件像是實現協作式多工的庫(例如 gevent)中的綠色執行緒(green thread)”。
  • Task 物件用於驅動協程,Thread 物件用於呼叫可呼叫的物件。
  • Task 物件不由自己動手例項化,而是通過把協程傳給 asyncio.async(...) 函式或 loop.create_task(...) 方法獲取。
  • 獲取的 Task 物件已經排定了執行時間(例如,由 asyncio.async 函式排定);Thread 例項則必須呼叫 start 方法,明確告知讓它執行。
  • 線上程版 supervisor 函式中,slow_function 函式是普通的函式,直接由執行緒呼叫。在非同步版 supervisor 函式中,slow_function 函式是協程,由 yield from 驅動。
  • 沒有 API 能從外部終止執行緒,因為執行緒隨時可能被中斷,導致系統處於無效狀態。如果想終止任務,可以使用 Task.cancel() 例項方法,在協程內部丟擲 CancelledError 異常。協程可以在暫停的 yield 處捕獲這個異常,處理終止請求。
  • supervisor 協程必須在 main 函式中由 loop.run_until_complete 方法執行。

多執行緒程式設計是比較困難的, 因為排程程式任何時候都能中斷執行緒, 必須記住保留鎖, 去保護程式中重要部分, 防止多執行緒在執行的過程中斷.

而協程預設會做好全方位保護, 以防止中斷. 我們必須顯示產出才能讓程式的餘下部分執行. 對協程來說, 無需保留鎖, 而在多個執行緒之間同步操作, 協程自身就會同步, 因為在任意時刻, 只有一個協程執行.

從期物、任務和協程中產出

asyncio 包中, 期物和協程關係緊密, 因為可以使用 yield fromasyncio.Future 物件中產出結果. 也就是說, 如果 foo 是協程函式, 或者是返回 FutureTask 例項的普通函式, 那麼可以用 res = yield from foo() .

為了執行這個操作, 必須排定協程的執行時間, 然後使用 asyncio.Task 物件包裝協程. 對協程來說, 獲取 Task 物件主要有兩種方式:

  • asyncio.async(coro_or_future, *, loop=None) : 這個函式統一了協程和期物:第一個引數可以是二者中的任何一個。如果是 Future 或 Task 物件,那就原封不動地返回。如果是協程,那麼 async 函式會呼叫 loop.create_task(...) 方法建立 Task 物件。loop 關鍵字引數是可選的,用於傳入事件迴圈;如果沒有傳入,那麼 async 函式會通過呼叫 asyncio.get_event_loop() 函式獲取迴圈物件。
  • BaseEventLoop.create_task(coro) : 這個方法排定協程的執行時間,返回一個 asyncio.Task 物件。如果在自定義的 BaseEventLoop 子類上呼叫,返回的物件可能是外部庫(如 Tornado)中與 Task 類相容的某個類的例項。

asyncio 包中有多個函式會自動(使用 asyncio.async 函式) 把引數指定的協程包裝在 asyncio.Task 物件中.

使用asyncio和aiohttp包下載

asyncio 包只直接支援 TCP 和 UDP. 如果像使用 HTTP 或其他協議, 就需要藉助第三方包. 使用的幾乎都是 aiohttp 包. 以下載圖片為例:

import asyncio
import aiohttp
from flags import BASE_URL, save_flag, show, main

@asyncio.coroutine
def get_flag(cc): # 協程應該使用 @asyncio.coroutine 裝飾。
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = yield from aiohttp.request('GET', url) # 阻塞的操作通過協程實現
    image = yield from resp.read()                # 讀取響應內容是一項單獨的非同步操作
    return image

@asyncio.coroutine
def download_one(cc): # download_one 函式也必須是協程,因為用到了 yield from
    image = yield from get_flag(cc)
    show(cc)
    save_flag(image, cc.lower() + '.gif')
    return cc
def download_many(cc_list):
    loop = asyncio.get_event_loop() # 獲取事件迴圈底層實現的引用
    to_do = [download_one(cc) for cc in sorted(cc_list)] # 呼叫 download_one 函式獲取各個國旗,然後構建一個生成器物件列表
    wait_coro = asyncio.wait(to_do) # 雖然函式的名稱是 wait,但它不是阻塞型函式。wait 是一個協程,等傳給它的所有協程執行完畢後結束
    res, _ = loop.run_until_complete(wait_coro) # 執行事件迴圈,直到 wait_coro 執行結束
    loop.close()  # 關閉事件迴圈
    return len(res)
if __name__ == '__main__':
    main(download_many)

asyncio.wait(...) 協程引數是一個由期物或協程構成的可迭代物件, wait 會分別把各個協程裝進一個 Task 物件. 最終的結果是, wait 處理的所有物件都通過某種方式變成 Future 類的例項. wait 是協程函式, 因此返回的是一個協程或生成器物件. 為了驅動協程, 我們把協程傳給 loop.run_until_complete(...) 方法.

loop.run_until_complete 方法的引數是一個期物或協程. 如果是協程, run_until_complete 方法與 wait 函式一樣, 把協程包裝進一個 Task 物件中. 因為協程都是由 yield from 驅動, 這正是 run_until_complete 對 wait 返回返回的 wait_coro 物件所做的事. 執行結束後返回兩個元素, 第一個是是結束的期物, 第二個是未結束的期物.

避免阻塞型呼叫

有兩種方法能避免阻塞型呼叫中止整個應用程式的程式:

  • 在單獨的執行緒中執行各個阻塞型操作
  • 把每個阻塞型操作轉換成非阻塞的非同步呼叫使用

多執行緒是可以的, 但是會消耗比較大的記憶體. 為了降低記憶體的消耗, 通常使用回撥來實現非同步呼叫. 這是一種底層概念, 類似所有併發機制中最古老最原始的那種--硬體中斷. 使用回撥時, 我們不等待響應, 而是註冊一個函式, 在發生某件事時呼叫. 這樣, 所有的呼叫都是非阻塞的.

非同步應用程式底層的事件迴圈能依靠基礎設定的中斷, 執行緒, 輪詢和後臺程式等待等, 確保多個併發請求能取得進展並最終完成, 這樣才能使用回撥. 事件迴圈獲得響應後, 會回過頭來呼叫我們指定的回撥. 如果做法正確, 事件迴圈和應用程式碼公共的主執行緒絕不會阻塞.

把生成器當做協程使用是非同步程式設計的另一種方式. 對事件迴圈來說, 呼叫回撥與在暫停的協程上呼叫 .send() 效果差不多.

使用Executor物件,防止阻塞事件迴圈

訪問本地檔案會阻塞, 而CPython底層在阻塞型I/O呼叫時會釋放 GIL, 因此另一個執行緒可以繼續.

因為 asyncio 事件不是通過多執行緒來完成, 因此 save_flag 用來儲存圖片的函式阻塞了與 asyncio 事件迴圈共用的唯一執行緒, 因此儲存檔案時, 真個應用程式都會凍結. 這個問題的解決辦法是, 使用事件迴圈物件的 run_in_executor 方法.

asyncio 的事件迴圈背後維護者一個 ThreadPoolExecutor 物件, 我們可以呼叫 run_in_executor 方法, 把可呼叫的物件發給它執行:

@asyncio.coroutine
    def download_one(cc, base_url, semaphore, verbose):
    try:
        with (yield from semaphore):
            image = yield from get_flag(base_url, cc)
    except web.HTTPNotFound:
        status = HTTPStatus.not_found
        msg = 'not found'
    except Exception as exc:
        raise FetchError(cc) from exc
    else:
        loop = asyncio.get_event_loop() # 獲取事件迴圈物件的引用
        loop.run_in_executor(None, # run_in_executor 方法的第一個引數是 Executor 例項;如果設為 None,使用事件迴圈的預設 ThreadPoolExecutor 例項。
        save_flag, image, cc.lower() + '.gif') # 餘下的引數是可呼叫的物件,以及可呼叫物件的位置引數 
        status = HTTPStatus.ok
        msg = 'OK'
    if verbose and msg:
        print(cc, msg)
    return Result(status, cc)

第十九章: 動態屬性和特性

在python中, 資料的屬性和處理資料的方法都可以稱為 屬性 . 除了屬性, pythpn 還提供了豐富的 API, 用於控制屬性的訪問許可權, 以及實現動態屬性, 如 obj.attr 方式和 __getattr__ 計算屬性.

動態建立屬性是一種超程式設計,

使用動態屬性轉換資料

通常, 解析後的 json 資料需要形如 feed['Schedule']['events'][40]['name'] 形式訪問, 必要情況下我們可以將它換成以屬性訪問方式 feed.Schedule.events[40].name 獲得那個值.

from collections import abc
class FrozenJSON:
    """一個只讀介面,使用屬性表示法訪問JSON類物件
    """
    def __init__(self, mapping):
        self.__data = dict(mapping)
    def __getattr__(self, name):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON.build(self.__data[name]) # 從 self.__data 中獲取 name 鍵對應的元素
    @classmethod
    def build(cls, obj):
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        else: # 如果既不是字典也不是列表,那麼原封不動地返回元素
            return obj

使用 new 方法以靈活的方式建立物件

我們通常把 __init__ 成為構造方法, 這是從其他語言借鑑過來的術語. 其實, 用於構造例項的特殊方法是 __new__ : 這是個類方法, 必須返回一個例項. 返回的例項將作為以後的 self 傳給 __init__ 方法.

第二十章: 屬性描述符

描述符是實現了特性協議的類, 這個協議包括 __get__, __set____delete__ 方法. 通常, 可以實現部分協議.

覆蓋型與非覆蓋型描述符對比

python存取屬性的方式是不對等的. 通過例項讀取屬性時, 通常返回的是例項中定義的屬性, 但是, 如果例項中沒有指定的屬性, 那麼會從獲取類屬性. 而例項中屬性賦值時, 通常會在例項中建立屬性, 根本不影響類.

這種不對等的處理方式對描述符也有影響. 根據是否定義 __set__ 方法, 描述符可分為兩大類: 覆蓋型描述符和與非覆蓋型描述符.

實現 __set__ 方法的描述符屬於覆蓋型描述符, 因為雖然描述符是類屬性, 但是實現 __set__ 方法的話, 會覆蓋對例項屬性的賦值操作. 因此作為類方法的 __set__ 需要傳入一個例項 instance . 看個例子:

def print_args(*args): # 列印功能
    print(args)

class Overriding:  # 設定了 __set__ 和 __get__
    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(repr(self)))

覆蓋型描述符

obj = Managed()
obj.over        # ('get', <__main__.Overriding object>, <__main__.Managed object>, <class '__main__.Managed'>)
obj.over = 7    # ('set', <__main__.Overriding object>, <__main__.Managed object>, 7)
obj.over        # ('get', <__main__.Overriding object>, <__main__.Managed object>, <class '__main__.Managed'>)

名為 over 的例項屬性, 會覆蓋讀取和賦值 obj.over 的行為.

沒有 __get__ 方法的覆蓋型描述符

obj = Managed()
obj.over_no_get
obj.over_no_get = 7  # ('set', <__main__.OverridingNoGet object>, <__main__.Managed object>, 7)
obj.over_no_get

只有在賦值操作的時候才回覆蓋行為.

方法是描述符

python的類中定義的函式屬於繫結方法, 如果使用者定義的函式都有 __get__ 方法, 所以依附到類上, 就相當於描述符.

obj.spamManaged.spam 獲取的是不同的物件. 前者是 <class method> 後者是 <class function> .

函式都是非覆蓋型描述符. 在函式上呼叫 __get__ 方法時傳入例項作為 self , 得到的是繫結到那個例項的方法. 呼叫函式的 __get__ 時傳入的 instance 是 None , 那麼得到的是函式本身. 這就是形參 self 的隱式繫結方式.

描述符用法建議

使用特性以保持簡單
內建的 property 類建立的是覆蓋型描述符, __set____get__ 都實現了.

只讀描述符必須有 set 方法
如果要實現只讀屬性, __get____set__ 兩個方法必須都定義, 柔則, 例項的同名屬性會覆蓋描述符.

用於驗證的描述符可以只有 set 方法
什麼是用於驗證的描述符, 比方有個年齡屬性, 但它只能被設定為數字, 這時候就可以只定義 __set__ 來驗證值是否合法. 這種情況不需要設定 __get__ , 因為例項屬性直接從 __dict__ 中獲取, 而不用去觸發 __get__ 方法.

第二十一章: 類超程式設計

類超程式設計是指在執行時建立或定製類的技藝. 在python中, 類是一等物件, 因此任何時候都可以使用函式建立類, 而無需使用 class 關鍵字. 類裝飾器也是函式, 不過能夠審查, 修改, 甚至把被裝飾的類替換成其他類.

元類是類超程式設計最高階的工具. 什麼是元類呢? 比如說 str 是建立字串的類, int 是建立整數的類. 那麼元類就是建立類的類. 所有的類都由元類建立. 其他 class 只是原來的"例項".

本章討論如何在執行時建立類.

類工廠函式

標準庫中就有一個例子是類工廠函式--具名元組( collections.namedtuple ). 我們把一個類名和幾個屬性傳給這個函式, 它會建立一個 tuple 的子類, 其中的元素通過名稱獲取.

假設我們建立一個 record_factory , 與具名元組具有相似的功能:

>>> Dog = record_factory('Dog', 'name weight owner')
>>> rex = Dog('Rex', 30, 'Bob')
>>> rex
Dog(name='Rex', weight=30, owner='Bob')
>>> rex.weight = 32
>>> Dog.__mro__
(<class 'factories.Dog'>, <class 'object'>)

我們要做一個在執行時建立類的, 類工廠函式:

def record_factory(cls_name, field_names):
    try:
        field_names = field_names.replace(',', ' ').split()  # 屬性拆分
    except AttributeError:  # no .replace or .split
        pass  # assume it's already a sequence of identifiers
    field_names = tuple(field_names)  # 使用屬性名構建元組,這將成為新建類的 __slots__ 屬性

    def __init__(self, *args, **kwargs):  # 這個函式將成為新建類的 __init__ 方法
        attrs = dict(zip(self.__slots__, args))
        attrs.update(kwargs)
        for name, value in attrs.items():
            setattr(self, name, value)

    def __iter__(self):  # 實現 __iter__ 函式, 變成可迭代物件
        for name in self.__slots__:
            yield getattr(self, name)

    def __repr__(self):  # 生成友好的字串表示形式
        values = ', '.join('{}={!r}'.format(*i) for i
                           in zip(self.__slots__, self))
        return '{}({})'.format(self.__class__.__name__, values)

    cls_attrs = dict(__slots__ = field_names,  # 組建類屬性字典
                     __init__  = __init__,
                     __iter__  = __iter__,
                     __repr__  = __repr__)

    return type(cls_name, (object,), cls_attrs)  # 呼叫元類 type 構造方法,構建新類,然後將其返回

type 就是元類, 例項的最後一行會構造一個類, 類名是 cls_name, 唯一直接的超類是 object .

在python中做超程式設計時, 最好不要用 execeval 函式. 這兩個函式會帶來嚴重的安全風險.

元類基礎知識

元類是製造類的工廠, 不過不是函式, 本身也是類. 元類是用於構建類的類.

為了避免無限回溯, type 是其自身的例項. object 類和 type 類關係很獨特, objecttype 的例項, 而 typeobject 的子類.

元類的特殊方法 prepare

type 構造方法以及元類的 __new____init__ 方法都會收到要計算的類的定義體, 形式是名稱到屬性的映像. 在預設情況下, 這個對映是字典, 屬性在類的定義體中順序會丟失. 這個問題的解決辦法是, 使用python3引入的特殊方法 __prepare__ , 這個方法只在元類中有用, 而且必須宣告為類方法(即要使用 @classmethod 裝飾器定義). 直譯器呼叫元類的 __new__ 方法之前會先呼叫 __prepare__ 方法, 使用類定義體中的屬性建立對映.

__prepare__ 的第一個引數是元類, 隨後兩個引數分別是要構建類的名稱和基類組成的原則, 返回值必須是對映.

class EntityMeta(type):
    """Metaclass for business entities with validated fields"""

    @classmethod
    def __prepare__(cls, name, bases):
        return collections.OrderedDict()  # 返回一個空的 OrderedDict 例項,類屬性將儲存在裡面。

    def __init__(cls, name, bases, attr_dict):
        super().__init__(name, bases, attr_dict)
        cls._field_names = []  # 中建立一個 _field_names 屬性
        for key, attr in attr_dict.items():
            if isinstance(attr, Validated):
                type_name = type(attr).__name__
                attr.storage_name = '_{}#{}'.format(type_name, key)
                cls._field_names.append(key)

class Entity(metaclass=EntityMeta):
    """Business entity with validated fields"""

    @classmethod
    def field_names(cls):  # field_names 類方法的作用簡單:按照新增欄位的順序產出欄位的名稱
        for name in cls._field_names:
            yield name

結語

python是一門即容易上手又強大的語言.

相關文章