天天看點

《流暢的python》學習筆記及書評《流暢的python》學習筆記

《流暢的python》學習筆記

文章目錄

  • 《流暢的python》學習筆記
    • 寫在前面
    • 1. Python資料模型
      • 1.1 特殊方法
    • 2. 序列構成的數組
      • 2.1 内置序列類型
      • 2.2 清單推導和生成器表達式
      • 2.3 元組不僅僅是不可變的清單
      • 2.4 切片
      • 2.5 增量指派
      • 2.6 排序
      • 2.7 數組、記憶體視圖、NumPy和隊列
    • 3. 字典和集合
      • 3.1 泛映射類型和字典推導
      • 3.2 常見的映射方法
      • 3.3 映射的彈性鍵查詢
      • 3.4 字典的變種
      • 3.5 集合
      • 3.6 dict和set的背後
    • 4. 文本和位元組序列
      • 4.1 字元和位元組
      • 4.2 編碼和解碼
      • 4.3 處理文本檔案
      • 4.4 規範化Unicode字元串
      • 4.5 支援字元串和位元組序列的雙模式API
    • 5. 一等函數
      • 5.1 把函數視作對象
      • 5.2 函數的參數和注解
      • 5.3 支援函數式程式設計的包
    • 6. 使用一等函數實作涉及模式
    • 7. 函數裝飾器和閉包
      • 7.1 裝飾器基礎
      • 7.2 閉包
      • 7.3 裝飾器
    • 8. 對象引用、可變性和垃圾回收
      • 8.1 變量不是盒子
      • 8.2 函數的參數作為引用
      • 8.3 del、垃圾回收和弱引用
    • 9. 符合Python風格的對象
      • 9.1 對象的表示形式
      • 9.2 格式化顯示
      • 9.3 Python的私有屬性和受保護的屬性
    • 10. 序列的修改、散列和切片
    • 11. 接口:從協定到抽象基類
    • 12. 繼承的優缺點
    • 13. 正确重載運算符
    • 14. 可疊代的對象、疊代器和生成器
    • 15. 上下文管理器和else塊
    • 16. 協程
    • 17. 使用期物處理并發
    • 18. 使用asyncio包處理并發
    • 19. 動态屬性和特性
    • 20. 屬性描述符
    • 21. 類元程式設計

寫在前面

《流暢的python》學習筆記及書評《流暢的python》學習筆記
  • 讀後感 優點:
    1. 翻譯滿昏!絕對滿昏!💯,你看下面黃色部分,這翻譯絕了,感覺我才是文化沙漠,什麼“親者快,仇者痛”,我這輩子沒見過這麼進階的用法。
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
    2. 我被作者舉的例子驚到了(見2.4),真的很有水準,翻譯和作者本身都很厲害

      比如下面這一句

      t = (1, 2, [30, 40])
      t[2] += [50, 60]
                 
      這兩行代碼的執行結果:
      1. 抛出異常:

        因為 tuple 不支援對它的元素指派,是以會抛出 TypeError 異常。

      2. t[2]

        的值發生了修改:

        t = (1, 2, [30, 40, 50, 60])

    3. 在講排序的時候,講到

      sorted

      list.sort

      背後使用的排序算法是Timsort,這個算法的作者是Tim Peters
      • 這個算法的相關代碼在Google對Sun的侵權案中,當作了呈堂證供。
      • 這個算法的作者也是

        import this

        ,python之禅的作者。
      • 我靠,太離譜了,世界線收束,雞皮疙瘩都起來了。
    4. 每一章的小結真的寫的太好了,友善回顧這一章講了啥,也友善自己查漏補缺。
    5. 延伸閱讀也是驚豔啊,作者很明顯博覽群書,基礎紮實。
    6. 作者吹了一波《Python Cookbook(第三版)》和《Python Cookbook(第二版)》,我準備去學習學習!
    7. 作者每章的小結寫的很不錯,每次因為知識點需要複查書籍的時候,可以先看對應章節的 本章小結 ,再查。
  • 讀後感 缺點:
    1. 讀到11章和12章的時候,就開始有點吃力了,不知道是不是我的知識儲備不夠。一些抽象的知識點作者和翻譯都有點力不從心(作為讀者的角度),就是好像作者想把這個點用比喻的方式說清楚,但是又很難把他心中的了解表達出來,甚至很多地方都是直接進行教條化的描述,對于翻譯來說,就更困難了。

      【對不起,我面向對象學的太差了嗚嗚嗚】

      比如 P540 中:優先使用對象組合,而不是類繼承,還有,組合和委托可以代替混入,把行為提供給不同的類,但是不能取代接口繼承去定義類型層次結構。

      對于 組合、委托、混入、繼承等名詞的解釋不夠到位,這幾個名詞,我就對繼承還可以有深入的了解,其他的三個名詞出現的時候,一臉懵逼

    2. 從16章協程開始,我就開始絕望了起來,有點整不明白,咬牙硬吃到18章asyncio的一些知識點的時候,就是懵懵懂懂的,在 yield 和 yield from 中學傻了。在18.5章的時候,不知道為什麼突然出現了個semaphore,十分突兀,就開始不知所雲了起來。
  • 傳送門:
    1. 清單、元組、數組、雙向隊列的方法和屬性
    2. dict

      collections.defaultdict

      collections.OrderedDict

      的方法清單
    3. 集合的數學運算、集合的比較運算符、集合類型的其他方法
    4. 使用者定義函數的屬性
    5. 利用

      inspect.signature

      提取函數簽名
    6. __set__,__setattr__,__getattr__

      等幾個方法差別小記
    7. 屬性、特性、描述符

1. Python資料模型

  1. collections.nametuple

    :用來建構隻有少數屬性但是沒有方法的對象,比如資料庫條目。
  2. __getitem__

    :方法可以實作切片效果
    def __getitem__(self, position):
            return self._cards[position]
               

1.1 特殊方法

  1. 如何使用特殊方法
    1. 首先,特殊方法的存在是為了被python解析器調用的,而不是被我們調用的
    2. 其次,

      my_object.__len__()

      這種寫法應該修正為

      len(my_object)

      ,在執行

      len(my_object)

      的時候,如果my_object是一個自定義類的對象,那麼python會自己去調用其中的,由我們自己實作的

      __len__

      方法
    3. 如果是python内置的類型,如清單list、字元串str、位元組序列bytearray等,Cpython會抄近道,

      __len__

      實際上會直接傳回PyVarObject裡的ob_size屬性。其中

      PyVarObject

      表示記憶體中長度可變的内置對象的C語言結構體。

      直接讀取這個值比調用一個方法要快很多。

    4. 很多時候,特殊方法的調用是隐式的,比如

      for i in x

      這個語句,背後其實調用的是

      iter(x)

      ,而這個函數的背後則是

      x.__iter__()

      方法。(前提是這個方法在x中被實作了)
    5. 不要想當然的随意添加特殊方法,說不定以後python會用到這個名字。
  2. repr

    :能把一個對象用字元串的形式表達出來以便辨認,「字元串表示形式」。

    __repr__

    所傳回的字元串應該準确、無歧義,并且盡可能表達出如何用代碼建立出這個被列印的對象。

    __repr__

    __str__

    的差別在于,後者是在

    str()

    函數被使用,或者是在用

    print

    函數列印一個對象的時候才能被調用的,并且它傳回的字元串對終端使用者更友好。

    如果你隻想實作這兩個特殊方法中的一個,

    __repr__

    是更好的選擇,因為如果一個對象沒有

    __str__

    函數,而 Python 又需要調用它的時候,解釋器會用

    __repr__

    作為替代。
  3. bool(x)

    的背後是調用

    x.__bool__()

    的結果;如果不存在

    __bool__

    方法,那麼

    bool(x)

    會嘗試調用

    x.__len__()

    。若傳回 0,則 bool 會傳回 False;否則傳回True。
  4. 跟運算符無關的特殊方法
    1. 字元串/位元組序清單示形式:

      __repr__

      __str__

      __format__

      __bytes__

    2. 數值轉換:

      __abs__

      __bool__

      __complex__

      __int__

      __float__

      __hash__

      __index__

    3. 集合模拟:

      __len__

      __getitem__

      __setitem__

      __delitem__

      __contains__

    4. 疊代枚舉:

      __iter__

      __reversed__

      __next__

    5. 可調用模拟:

      __call__

    6. 上下文管理器:

      __enter__

      __exit__

    7. 執行個體建立和銷毀:

      __new__

      __init__

      __del__

    8. 屬性管理:

      __getattr__

      __getattribute__

      __setattr__

      __delattr__

      __dir__

    9. 屬性描述符:

      __get__

      __set__

      __delete__

    10. 跟類相關的服務:

      __prepare__

      __instancecheck__

      __subclasscheck__

  5. 跟運算符相關的特殊方pos法
    1. 一進制運算符:

      __neg__

      -

      )、

      __pos__

      +

      )、

      __abs__

      abs()

    2. 衆多比較運算符:

      __lt__

      <

      )、

      __le__

      <=

      )、

      __eq__

      ==

      )、

      __ne__

      !=

      )、

      __gt__

      >

      )、

      __ge__

      >=

    3. 算數運算符:

      __add__

      +

      )、

      __sub__

      -

      )、

      __mul__

      *

      )、

      __truediv__

      /

      )、

      __floordiv__

      //

      )、

      __mod__

      %

      )、

      __divmod__

      divmod()

      )、

      __pow__

      **

      pow()

      )、

      __round__

      round()

    4. 反向算數運算符:

      __radd__

      __rsub__

      __rmul__

      __rtruediv__

      __rfloordiv__

      __rmod__

      __rdivmod__

    5. 增量指派算數運算符:

      __iadd__

      __isub__

      __imul__

      __itruediv__

      __ifloordiv__

      __imod__

      __ipow__

    6. 位運算符:

      __invert__

      ~

      )、

      __lshift__

      <<

      )、

      __rshift__

      >>

      )、

      __and__

      &

      )、

      __or__

      |

      )、

      __xor__

      ^

    7. 反向位運算符:

      __rlshift__

      __rrshift__

      __rand__

      __rxor__

      __ror__

    8. 增量指派位運算符:

      __ilshift__

      __irshift__

      __iand__

      __ixor__

      __ior__

  6. 為什麼len不是一個普通方法:「實用勝于純粹」,如果 x 是一個内置類型的執行個體,那麼

    len(x)

    的速度會非常快。背後的原因是 CPython 會直接從一個 C 結構體裡讀取對象的長度,完全不會調用任何方法。擷取一個集合中元素的數量是一個很常見的操作,在

    str、list、memoryview

    等類型上,這個操作必須高效。

換句話說,len 之是以不是一個普通方法,是為了讓 Python 自帶的資料結構可以走後門,abs 也是同理。但是多虧了它是特殊方法,我們也可以把 len 用于自定義資料類型

2. 序列構成的數組

2.1 内置序列類型

  1. 容器序列:list、tuple、collections.deque。可以存放不同類型的資料。
  2. 扁平序列:str、bytes、bytearray、memoryview、array.array。隻能容納一種類型。
  3. 容器序列存放的是它們所包含的任意類型的對象的引用,而扁平序列裡存放的是值而不是引用。換句話說,扁平序列其實是一段連續的記憶體空間。由此可見扁平序列其實更加緊湊,但是它裡面隻能存放諸如字元、位元組和數值這種基礎類型。
  4. 可變序列:list、bytearray、array.array、collections.deque、memoryview
  5. 不可變序列:tuple、str、bytes
  6. 可變序列(MutableSequence)和不可變序列(Sequence)的差異
    《流暢的python》學習筆記及書評《流暢的python》學習筆記

2.2 清單推導和生成器表達式

  1. Python 會忽略代碼裡 []、{} 和 () 中的換行,是以如果你的代碼裡有多行的清單、清單推導、生成器表達式、字典這一類的,可以省略不太好看的續行符 \。
  2. 清單推導不會再有變量洩漏的問題
    x = 'ABC'
    dummy = [ord(x) for x in x]
    print(x, dummy)
    
    # ABC [65, 66, 67]
               
  3. 生成器表達式的文法跟清單推導差不多,隻不過把方括号換成圓括号而已。

2.3 元組不僅僅是不可變的清單

  1. 除了用作不可變的清單,它還可以用于沒有字段名的記錄。
  2. 元組其實是對資料的記錄:元組中的每個元素都存放了記錄中一個字段的資料,外加這個字段的位置。正是這個位置資訊給資料賦予了意義。
  3. 在元組拆包中使用

    *

    也可以幫助我們把注意力集中在元組的部分元素上。用

    *

    來處理剩下的元素
    a, b, *rest = range(5)
    print(a, b, rest)
    
    # 0 1 [2, 3, 4]
               
  4. 在平行指派中,

    *

    字首隻能用在一個變量名前面,但是這個變量可以出現在指派表達式的任意位置
    a, *body, c, d = range(5)
    print(a, body, c, d)
    
    # 0 [1, 2] 3 4
               
  5. collections.namedtuple

    :建立一個具名元組需要兩個參數,一個是類名,另一個是類的各個字段的名字。後者可以是由數個字元串組成的可疊代對象,或者是由空格分隔開的字段名組成的字元串。
    1. _fields

      :一個包含這個類所有字段名稱的元組。
    2. _make()

      :通過接受一個可疊代對象來生成這個類的一個執行個體,它的作用等價于

      類(*參數元組)

      是一樣的。
    3. _asdict()

      :把具名元組以

      collections.OrderedDict

      的形式傳回,我們可以利用它來把元組裡的資訊友好地呈現出來。
  6. 清單、元組、數組、雙向隊列的方法和屬性
    方法和屬性 清單 元組 數組 雙向隊列 描述

    s.__add__(s2)

    ×

    s+s2

    ,拼接

    s.__iadd__(s2)

    ×

    s+=s2

    ,就地拼接

    s.append(e)

    × 在尾部添加一個新元素

    s.appendleft(e)

    × × × 添加一個元素到最左側(到第一個元素之前)

    s.byteswap

    × × × 翻轉數組内每個元素的位元組序列,轉換位元組序列

    s.clear()

    × × 删除所有元素

    s.__contains__(e)

    × s是否包含e

    s.copy()

    × × × 清單的淺複制

    s.__copy__()

    × ×

    copy.copy

    (淺複制)的支援

    s.count(e)

    e在s中出現的次數

    s.__deepcopy__()

    × × ×

    copy.deepcopy

    (深複制)的支援

    s.__delitem__(p)

    × 把位于p的元素删除

    s.extend(it)

    × 把可疊代對象it追加給s

    s.extendleft(i)

    × × × 将可疊代對象i中的元素添加到頭部

    s.frombytes(b)

    × × × 将壓縮成機器值得位元組序列讀出來添加到尾部

    s.fromfile(f,n)

    × × × 将二進制檔案f内含有機器值讀出來添加到尾部,最多添加n項

    s.fromlist(l)

    × × × 将清單裡的元素添加到尾部,如果其中任何一個元素導緻了

    TypeError

    異常,那麼所有的添加都會取消

    s.__getitem__()

    s[p]

    ,擷取位置p的元素

    s.__getnewargs__()

    × × × 在pickle中支援更加優化的序列化

    s.index(e)

    × 在s中找到元素e第一次出現的位置

    s.insert(p,e)

    × × 在位置p之前插入元素e

    s.itemsize

    × × × 數組中每個元素的長度是幾個位元組

    s.__iter__()

    擷取s的疊代器

    s.__len__()

    len(s)

    ,元素的數量

    s.__mul__()

    ×

    s*n

    ,n個s的重複拼接

    s.__imul__()

    × ×

    s*=n

    ,就地重複拼接

    s.__rmul__()

    ×

    n*s

    ,反向拼接

    *

    s.pop([p])

    × 删除最後或者是(可選的)位于p的元素,并傳回它的值(注意,在雙向隊列中不接受參數)

    s.popleft()

    × × × 移除第一個元素并傳回它的值

    s.remove(e)

    × 删除s中第一次出現的e

    s.reverse()

    × 就地把s的元素倒序排列

    s.__reversed__

    × × 傳回s的倒序疊代器

    s.rotate(n)

    × × × 把n個元素從隊列的一段移到另一端

    s.__setitem__(p,e)

    ×

    s[p]=e

    ,把元素e放在位置p,替代已經在那個位置的元素

    s.sort([key],[reverse])

    × × × 就地對s中的元素進行排序,可選的參數有鍵(key)和是否倒序(reverse)

    s.tobytes()

    × × × 把所有元素的機器值用bytes對象的形式傳回

    s.tofile(f)

    × × × 把所有元素以機器值的形式寫入一個檔案

    s.tolist()

    × × × 把數組轉換成清單,清單裡的元素類型是數字對象

    s.typecode

    × × × 傳回隻有一個字元的字元串,代表數組元素在C語言中的類型

2.4 切片

  1. 為什麼切片和區間會忽略最後一個元素:

    在切片和區間操作裡不包含區間範圍的最後一個元素是 Python 的風格,這個習慣符合 Python、C 和其他語言裡以 0 作為起始下标的傳統。這樣做帶來的好處如下。

    1. 當隻有最後一個位置資訊時,我們也可以快速看出切片和區間裡有幾個元素:

      range(3)

      my_list[:3]

      都傳回 3 個元素。
    2. 當起止位置資訊都可見時,我們可以快速計算出切片和區間的長度,用後一個數減去第一個下标(stop - start)即可。
    3. 這樣做也讓我們可以利用任意一個下标來把序列分割成不重疊的兩部分,隻要寫成

      my_list[:x]

      my_list[x:]

      就可以了。
  2. slice(a, b, c)

    :對

    seq[start:stop:step]

    進行求值的時候,Python 會調用

    seq.__getitem__(slice(start, stop, step))

  3. slice(start, stop, step)

    :使用方法
    a = slice(6, 40)
    item[a]
               
  4. 多元切片:如果要得到

    a[i, j]

    的值,Python 會調用

    a.__getitem__((i, j))

  5. x[i, ...]

    x[i, :, :, :]

    的縮寫
  6. 給切片指派:如果把切片放在指派語句的左邊,或把它作為

    del

    操作的對象,我們就可以對序列進行 嫁接 、 切除 或 就地修改 操作。
    1. 如果指派的對象是一個切片,那麼指派語句的右側必須是個可疊代對象。
    2. 即便隻有單獨一個值,也要把它轉換成可疊代的序列。

2.5 增量指派

  1. +

    *

    的陷阱:如果要生成二維序列:
    1. 不能:

      [['_']*3]*3

    2. 而要:

      [['_']*3 for i in range(3)]

  2. a += b

    1. 如果a實作了

      __iadd__

      方法,就相當于調用了

      a.extend(b)

    2. 如果a沒有實作

      __iadd__

      的話,

      a += b

      就跟

      a = a + b

      一樣了。首先計算

      a + b

      ,得到一個新的對象,然後指派給a。
    3. 也就是說,在這個表達式中,變量名會不會被關聯到新的對象,完全取決于這個類型有沒有實作

      __iadd__

      方法。
  3. 對不可變序列進行重複拼接操作的話,效率會很低,因為每次都有一個新對象,而解釋器需要把原來對象中的元素先指派到新的對象裡,然後再追加新的元素。

    str

    是一個例外,因為對字元串做

    +=

    實在是太普遍了,是以CPython對它做了優化,為str初始化記憶體的時候,程式會為它留出額外的可擴充空間,是以進行增量操作的時候,并不會涉及複制原有字元串到新位置這類操作。

2.6 排序

  1. Python中的排序算法:Timesort 是穩定的,意思是就算兩個元素比不出大小,在每次排序的結果裡他們的相對位置是固定的。
  2. list.sort

    :就地排序清單,也就是說不會把原清單複制一份,傳回值為

    None

  3. sorted

    :會建立一個清單作為傳回值
  4. key

    參數能讓你對一個混有數字字元和數值的清單進行排序。
    l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
    sorted(l, key=int)
    
    # [0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
               
  5. sorted

    list.sort

    背後的排序算法是 Timsort,它是一種自适應算法,會根據原始資料的順序特點交替使用插入排序和歸并排序,以達到最佳效率。這樣的算法被證明是很有效的,因為來自真實世界的資料通常是有一定的順序特點的。
  6. bisect

    來管理 已排序 的序列:二分法
    1. bisect

      函數其實是

      bisect_right

      函數的别名,後者還有個姊妹函數叫

      bisect_left

    2. bisect_left

      傳回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素會被放置于它相等的元素的前面
    3. bisect_right

      傳回的則是跟它相等的元素之後的位置
  7. 利用

    bisect

    進行評價分組
    def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
        i = bisect.bisect(breakpoints, score)
        return grades[i]
    [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]
    
    # ['F', 'A', 'C', 'C', 'B', 'A', 'A']
               
  8. bisect.insort(seq, item)

    把變量item插入到序列seq中,并能保持seq的升序順序。

2.7 數組、記憶體視圖、NumPy和隊列

  1. 如果我們需要一個隻包含數字的清單,那麼

    array.array

    list

    更高效。通過

    array.tofile

    array.fromfile

    進行檔案的儲存和讀取。
  2. memoryview

    :是一個内置類,它能讓使用者在不複制内容的情況下操作同一個數組的不同切片。
  3. memoryview.cast

    的概念跟數組子產品類似,能用不同的方式讀寫同一塊記憶體資料,而且内容位元組不會随意移動。

    memoryview.cast

    會把同一塊記憶體裡的内容打包成一個全新的

    memoryview

    對象給你。
  4. numpy

    1. 将一維數組轉化為二維:

      array.shape=(x, y)

    2. 将數組轉置:

      array.T

      array.transpose()

  5. collections.deque

    類(雙向隊列)是一個線程安全、可以快速從兩端添加或者删除元素的資料類型。
  6. dq = deque(range(10), maxlen=10)

    maxlen

    是一個可選參數,帶别找個隊列可以容納的元素的數量。
  7. dq.rotate(n)

    :隊列的旋轉操作接受一個參數n,當n>0時,隊列的最右邊的n個元素會被移動到隊列的左邊。當n<0時,最左邊的n個元素會被移動到右邊。
  8. append

    popleft

    都是原子操作,也就說是 deque 可以在多線程程式中安全地當作先進先出的棧使用,而使用者不需要擔心資源鎖的問題。
  9. 其他隊列的實作:
    1. queue

      提供了

      Queue

      LifoQueue

      PriorityQueue

      。在滿員的時候,這些類不會扔掉舊的元素來騰出位置。相反,如果隊列滿了,它就會被鎖住,直到另外的線程移除了某個元素而騰出了位置。這一特性讓這些類很适合用來控制活躍線程的數量。
    2. multiprocessing

      實作了自己的Queue,跟

      queue.Queue

      相似,是涉及給程序間通信用的。

      multiprocessing.JoinableQueue

      可以讓任務管理變得更友善。
    3. asyncio

      裡面有

      Queue

      LifoQueue

      PriorityQueue

      JoinableQueue

      ,這些類受到

      queue

      multiprocessing

      子產品的影響,但是為異步程式設計裡的任務管理提供了專門的便利。
    4. heapq

      沒有隊列類,而是提供了

      heappush

      heappop

      方法,讓使用者可以把可變序列當作堆隊列或者優先隊列來使用。
  10. 清單傾向于存放有通用特性的元素;元組則恰恰相反,經常用來存放不同類型的元素。

3. 字典和集合

3.1 泛映射類型和字典推導

  1. collections.abc

    子產品中有

    Mapping

    MutableMapping

    這兩個抽象基類,它們的作用是為 dict 和其他類似的類型定義形式接口
    《流暢的python》學習筆記及書評《流暢的python》學習筆記
  2. 可散列的資料類型(hashable)

    如果一個對象是可散列的,那麼在這個對象的生命周期中,它的散列值是不變的,而且這個對象需要實作

    __hash__()

    方法。另外可散列對象還要有

    __eq__()

    方法,這樣才能跟其他鍵做比較。如果兩個可散列對象是相等的,那麼它們的散列值一定是一樣的。

    簡單來說,如果一個對象是可散列的資料類型的話,那它應是不可變的。

  3. list等可變對象是不可散列的,因為随着資料的改變他們的哈希值會變化導緻進入錯誤的哈希表。
  4. 元組的話,隻有當一個元組包含的所有元素都是可散列類型的情況下,它才是可散列的。
  5. 一般使用者自定義的類型的對象都是可散列的,散列值就是它們的

    id()

    函數的傳回值,是以所有這些對象在比較的時候都是不相等的。如果一個對象實作了

    __eq__()

    方法,并且在方法中用到了這個對象的内部狀态的話,那麼隻有當所有這些内部狀态都是不可變的情況下,這個對象才是可散列的。
  6. Python 裡所有的不可變類型都是可散列的,這個說法其實是不準确的,比如雖然元組本身是不可變序列,它裡面的元素可能是其他可變類型的引用。
  7. 字典推導:

3.2 常見的映射方法

dict

collections.defaultdict

collections.OrderedDict

的方法清單

方法 dict defaultdict OrderedDIct 描述

d.clear()

移除所有元素

d.__contains__(k)

檢查k是否在d中

d.copy()

淺複制

d.__copy()__

× × 用于支援

copy.copy

d.default_factory

× ×

__missing__

函數中被調用的函數,用以給未找到的元素設定值

d.__delitem__(k)

del d[k]

,移除鍵位k的元素

d.fromkeys(it, [initial])

将疊代器it裡的元素設定為映射裡的鍵,如果initial參數,就把它作為這些鍵對應的值(預設是None)

d.get(k, [default])

傳回鍵k對應的值,如果字典裡沒有鍵k,則傳回None或者default

d.__getitem__(k)

讓字典d能用

d[k]

的形式傳回鍵k對應的值

d.items()

傳回d裡所有的鍵值對

d.__iter__()

擷取鍵的疊代器

d.keys()

擷取所有的鍵

d.__len__()

可以用

len(d)

的形式得到字典裡鍵值對的數量

d.__missing__(k)

× ×

__getitem__

找不到對應鍵的時候,這個方法會被調用

d.move_to_end(k, [last])

× × 把鍵位k的元素移動到最靠前或者最靠後的位置(last的預設值是True)

d.pop(k, [default])

傳回鍵k所對應的值,然後移除這個鍵值對。如果沒有這個鍵,傳回None或者default

d.popitem()

随機傳回一個鍵值對并從字典裡移除它

d.__reversed__()

× × 傳回倒序的鍵的疊代器

d.setdefault(k, [default])

若字典裡有鍵k,則把它對應的值設定位default,然後傳回這個值;若無,則讓

d[k]=default

,然後傳回default

d.__setitem__

實作

d[k]=v

操作,把k對應的值設為v

d.update(m, [**kwargs])

m可以是映射或者鍵值對疊代器,用來更新d裡對應的條目

d.values

傳回字典裡的所有值
  1. 用setdefault處理找不到的鍵
    • 使用

      d.get(k, default)

      來代替

      d[k]

      ,可以防止報錯
    • 字典處理優化
      • 好的方法
      • 差的方法
        if key not in my_dict:
        	my_dict[key] = []
        my_dict[key].append(new_value)
                   
      • 二者的效果是一樣的,隻不過後者至少要進行兩次鍵查詢——如果鍵不存在的話,就是三次,用

        setdefault

        隻需要一次就可以完成整個操作。

3.3 映射的彈性鍵查詢

  1. 場景:有時候為了友善起見,就算某個鍵在映射裡不存在,我們也希望在通過這個鍵讀取值的時候能得到一個預設值。
  2. 方法:
    1. collections.defaultdict

      • 把list構造方法作為

        default_factory

        來建立一個

        defaultdict

      • 如果在建立

        defaultdict

        的時候沒有指定

        default_factory

        ,查詢不存在的鍵會觸發KeyError
      • defaultdict

        裡的

        default_factory

        隻會在

        __getitem__

        裡被調用,在其他的方法裡完全不會發揮作用。比如

        dd[k]

        會建立預設值并傳回該預設值,

        dd.get(k)

        就會傳回None
      • 所有這一切的背後是基于特殊方法

        __missing__

        實作的,它會在

        defaultdict

        遇到找不到的鍵的時候調用

        default_factory

        ,而實際上這個特性是所有映射類型都可以選擇去支援的
      """建立一個從單詞到其出現情況的映射"""
      
      import sys
      import re
      from collections import defaultdict
      
      WORD_RE = re.compile(r'\w+')
      
      # index = {}
      index = defaultdict(list)
      with open(sys.argv[1], encoding='utf-8') as fp:
          for line_no, line in enumerate(fp, 1):
              for match in WORD_RE.finditer(line):
                  word = match.group()
                  column_no = match.start() + 1
                  location = (line_no, column_no)
                  
                  ''' 這其實是一種很不好的實作,這樣寫隻是為了證明論點
                  occurrences = index.get(word, [])
                  occurrences.append(location)
                  index[word] = occurrences
                  '''
                  
                  ''' 下面是好的實作,用到了setdefault函數
                  index.setdefault(word, []).append(location)
                  '''
                  
                  ''' 通過collections.defaultdict實作,取得key沒有的時候,建立這個key,并賦予預設值'''
                  index[word].append(location)
                  
      # 以字母順序列印出結果
      for word in sorted(index, key=str.upper):
          print(word, index[word])
                 
    2. 自定義一個

      dict

      子類,然後在子類中實作

      __missing__

      方法
      • k in my_dict.keys()

        這種操作在Python3中是很快的,而且即便映射類型對象很龐大也沒關系。這是因為

        dict.keys()

        的傳回值是一個視圖。
      • 視圖就像一個集合,而且跟字典類似的是,在視圖裡查找一個元素的速度很快。
        class StrKeyDict0(dict):
            def __missing__(self, key):
                if isinstance(key, str):
                    raise KeyError(key)
                return self[str(key)]
            
            def get(self, key, default=None):
                try:
                    return self[key]
                except KeyError:
                    return default
            def __contains__(self, key):
                return key in self.keys() or str(key) in self.keys()
                   

3.4 字典的變種

  1. collections.OrderedDict

    :這個類型在添加鍵的時候會保持順序,是以鍵的疊代次序總是一緻的。

    OrderedDict

    popitem

    方法預設删除并傳回的是字典裡的最後一個元素,但是如果像

    my_odict.popitem(last=False)

    這樣調用它,那麼它删除并傳回第一個被添加進去的元素。
  2. collections.ChainMap

    :該類型可以容納數個不同的映射對象,然後在進行鍵查找操作的時候,這些對象會被當作一個整體被逐個查找,直到鍵被找到為止。這個功能在給有嵌套作用域的語言做解釋器的時候很有用,可以用一個映射對象來代表一個作用域的上下文。
  3. collections.Counter

    :這個映射類型會給鍵準備一個整數計數器。每次更新一個鍵的時候都會增加這個計數器。是以這個類型可以用來給可散清單對象計數,或者是當成多重集來用——多重集合就是集合裡的元素可以出現不止一次。
  4. my_dict.most_common(n)

    :統計字典中出現次數最多的資料
    ct = Counter({'a': 10, 'b': 2, 'r': 2, 'c': 1, 'd': 1, 'z': 3})
    ct.most_common(2)
    
    # [('a', 10), ('z', 3)]
               
  5. colllections.UserDict

    :這個類其實就是把标準

    dict

    用純 Python 又實作了一遍。跟

    OrderedDict

    ChainMap

    Counter

    這些開箱即用的類型不同,

    UserDict

    是讓使用者繼承寫子類的。
    • 更傾向于從

      UserDict

      而不是從

      dict

      繼承的主要原因是,後者有時會在某些方法的實作上走一些捷徑,導緻我們不得不在它的子類中重寫這些方法,但是 UserDict 就不會帶來這些問題。
  6. 不可變映射類型:

    types.MappingProxyType

    。如果給這個類一個映射,它會傳回一個隻讀的映射視圖。雖然是個隻讀視圖,但是它是動态的。這意味着如果對原映射做出了改動,我們通過這個視圖可以觀察到,但是無法通過這個視圖對原映射做出修改。

3.5 集合

  1. 集合可以去重
  2. 集合中的元素必須是可散列的,set 類型本身是不可散列的,但是frozenset 可以。是以可以建立一個包含不同 frozenset 的 set。
  3. 求交集:

    &

    intersection

  4. 空集:

    set()

    ,而不是

    {}

    ,因為

    {}

    是一個空字典
  5. 除了空集,集合的字元串表示形式總是以

    {...}

    的形式出現。
  6. {1, 2, 3}

    的速度快于

    set([1, 2, 3])

    ,因為後者的話,Python必須先從set這個名字來查詢構造方法,然後建立一個清單,最後再把這個清單傳入到構造方法裡。但是如果是像

    {1, 2, 3

    這樣的字面量,Python 會利用一個專門的叫作 BUILD_SET 的位元組碼來建立集合。
  7. MutableSet和它的超類的UML類圖
    《流暢的python》學習筆記及書評《流暢的python》學習筆記
  8. 集合的數學運算:這些方法或者會生成新集合,或者會在條件允許的情況下就地修改集合
    數學符号 Python運算符 方法 描述
    S ∩ Z S \cap Z S∩Z

    s&z

    s.__and__(z)

    s和z的交集
    S ∩ Z S \cap Z S∩Z

    z&s

    s.__rand__(z)

    反向&操作
    S ∩ Z S \cap Z S∩Z

    z&s

    s.intersection(it, ...)

    把可疊代的it和其他所有參數轉化為集合,然後求它們與s的交集
    S ∩ Z S \cap Z S∩Z

    s&=z

    s.__iand__(z)

    把s更新為s和z的交集
    S ∩ Z S \cap Z S∩Z

    s&=z

    s.intersection_update(it, ...)

    把可疊代的it和其他所有參數轉化為集合,然後求得它們與s的交集,然後把s更新成這個交集
    S ∪ Z S \cup Z S∪Z

    s|z

    s.__or__(z)

    s和z的并集
    S ∪ Z S \cup Z S∪Z

    z|s

    s.__ror__(z)

    |的反向操作
    S ∪ Z S \cup Z S∪Z

    z|s

    s.union(it, ...)

    把可疊代的it和其他所有參數轉化為集合,然後求它們和s的并集
    S ∪ Z S \cup Z S∪Z

    s|=z

    s.__ior__(z)

    把s更新為s和z的并集
    S ∪ Z S \cup Z S∪Z

    s|=z

    s.update(it, ...)

    把可疊代的it和其他所有參數轉化為集合,然後求它們和s的并集,并把s更新成這個并集
    S ∖ Z S \setminus Z S∖Z

    s-z

    s.__sub__(z)

    s和z的差集,或者叫作相對補集
    S ∖ Z S \setminus Z S∖Z

    z-s

    s.__rsub__(z)

    -的反向操作
    S ∖ Z S \setminus Z S∖Z

    z-s

    s.difference(it, ...)

    把可疊代的it和其他所有參數轉化為集合,然後求它們和s的差集
    S ∖ Z S \setminus Z S∖Z

    s-=z

    s.__isub__(z)

    把s更新為它與z的差集
    S ∖ Z S \setminus Z S∖Z

    s-=z

    s.difference_update(it, ...)

    把可疊代的it和其他所有參數轉化為集合,求它們和s的差集,然後把s更新成這個差集
    S ∖ Z S \setminus Z S∖Z

    s-=z

    s.symmetric_difference(it)

    求s和set(it)的對稱差集
    S ⨁ Z S \bigoplus Z S⨁Z

    s^z

    s.__xor__(z)

    求s和z的對稱差集
    S ⨁ Z S \bigoplus Z S⨁Z

    z^s

    s.__rxor__(z)

    ^的反向操作
    S ⨁ Z S \bigoplus Z S⨁Z

    z^s

    s.symmetric_difference_update(it, ...)

    把可疊代的it和其他所有參數轉化為集合,然後求它們和s的對稱差集,最後把s更新成該結果
    S ⨁ Z S \bigoplus Z S⨁Z

    s^=z

    s.__ixor__(z)

    把s更新成它與z的對稱差集
  9. 集合的比較運算符,傳回值是布爾類型
    數學符号 Python運算符 方法 描述

    s.isdisjoint(z)

    檢視s和z是否不相交(沒有共同元素)
    e ∈ S e \in S e∈S

    e in s

    s.__contains__(e)

    元素e是否屬于s
    S ⊆ Z S \subseteq Z S⊆Z

    s <= z

    s.__le__(z)

    s是否為z的子集
    S ⊆ Z S \subseteq Z S⊆Z

    s <= z

    s.issubset(it)

    把可疊代的it轉化為集合,然後檢視s是否為它的子集
    S ⊂ Z S \subset Z S⊂Z

    s < z

    s.__lt__(z)

    s是否為z的真子集
    S ⊇ Z S \supseteq Z S⊇Z

    s >= z

    s.__ge__(z)

    s是否為z的父集
    S ⊇ Z S \supseteq Z S⊇Z

    s >= z

    s.issuperset(it)

    把可疊代的it轉化為集合,然後檢視s是否為它的父集
    S ⊃ Z S \supset Z S⊃Z

    s > z

    s.__gt__(z)

    s是否為z的真父集
  10. 集合類型的其他方法
    方法 set frozenset 描述

    s.add(e)

    × 把元素e添加到s中

    s.clear()

    × 移除掉s中的所有元素

    s.copy()

    對s淺複制

    s.discard(e)

    × 如果s裡有e這個元素的話,把它移除

    s.__iter__()

    傳回s的疊代器

    s.__len__()

    len(s)

    s.pop()

    × 從s中移除一個元素并傳回它的值,若s為空,則抛出KeyError異常

    s.remove(e)

    × 從s中移除e元素,若e元素不存在,則抛出KeyError異常

3.6 dict和set的背後

  1. 如果兩個對象在比較的時候相等,那麼它們的散列值必須相等。
  2. 散列函數用于将鍵值經過處理後轉化為散列值。具有以下特性:
    1. 散列函數計算得到的散列值是非負整數
    2. 如果 key1 == key2,則 hash(key1) == hash(key2)
    3. 如果 key1 != key2,則 hash(key1) != hash(key2)
  3. 散列沖突:簡單來說,指的是 key1 != key2 的情況下,通過散列函數處理,hash(key1) == hash(key2),這個時候,我們說發生了散列沖突。設計再好的散列函數也無法避免散列沖突,原因是散列值是非負整數,總量是有限的,但是現實世界中要處理的鍵值是無限的,将無限的資料映射到有限的集合,肯定避免不了沖突。
  4. 從字典中取值的算法流程圖
    《流暢的python》學習筆記及書評《流暢的python》學習筆記
  5. 用元組取代字典就能節省空間的原因有兩個:
    1. 是避免了散清單所耗費的空間
    2. 無需把記錄中字段的名字在每個元素裡都存一遍
  6. 在使用者自定義的類型中,

    __slots__

    屬性可以改變執行個體屬性的存儲方式,由dict變成tuple
  7. 使用散清單給dict帶來的優勢和限制都有哪些
    1. 鍵必須是可散列的
    2. 字典在記憶體上的開銷巨大
    3. 鍵查詢很快
    4. 鍵的次序取決于添加順序
    5. 往字典裡添加新鍵可能會改變已有鍵的順序

      無論何時往字典裡添加新的鍵,Python 解釋器都可能做出為字典擴容的決定。擴容導緻的結果就是要建立一個更大的散清單,并把字典裡已有的元素添加到新表裡。這個過程中可能會發生新的散列沖突,導緻新散清單中鍵的次序變化。

  8. 集合的特點:
    1. 集合裡的元素必須是可散列的
    2. 集合很消耗記憶體
    3. 可以很高效地判斷元素是否存在于某個集合
    4. 元素的次序取決于被添加到集合裡的次序
    5. 往集合裡添加元素,可能會改變集合裡已有元素的次序

4. 文本和位元組序列

4.1 字元和位元組

  1. 把碼位轉換成位元組序列的過程是編碼;把位元組序列轉換成碼位的過程是解碼。
  2. 把位元組序列變成人類可讀的文本字元串就是解碼(

    decode

    ),而把字元串變成用于存儲或傳輸的位元組序列就是編碼(

    encode

    )。
  3. bytes

    對象可以從

    str

    對象使用給定的編碼構造,各個元素是

    range(256)

    内的整數。

    bytes

    對象的切片還是

    bytes

    對象,即使是隻有一個字元的切片。
  4. bytearray

    對象沒有字面量句法,而是以

    bytearray()

    和位元組序列字面量參數的形式顯示。

    bytearray

    對象的切片還是

    bytearray

    對象。
  5. 這裡比較特殊,因為

    my_bytes[0]

    擷取的是一個整數,而

    my_bytes[:1]

    傳回的是一個長度為1的

    bytes

    對象。
  6. 雖然二進制序列其實是整數序列,但是它們的字面量表示法表明其中有ASCII 文本。是以,各個位元組的值可能會使用下列三種不同的方式顯示。
    1. 可列印的 ASCII 範圍内的位元組(從空格到 ~),使用 ASCII 字元本身。
    2. 制表符、換行符、回車符和

      \

      對應的位元組,使用轉義序列

      \t

      \n

      \r

      \\

    3. 其他位元組的值,使用十六進制轉義序列(例如,

      \x00

      是空位元組)。
  7. 二進制序列有個類方法是

    str

    沒有的,名為

    fromhex

    ,它的作用是解析十六進制數字對(數字對之間的空格是可選的),建構二進制序列:
    bytes.fromhex('31 4B CE A9')
    # b'1K\xce\xa9'
               
  8. 使用緩沖類對象建構二進制序列是一種低層操作,可能涉及類型轉換:
import array
numbers = array.array('h', [-2, -1, 0, 1, 2])
octets = bytes(numbers)

# b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
           
  1. 使用緩沖類對象建立

    bytes

    bytearray

    對象時,始終複制源對象中的位元組序列。與之相反,

    memoryview

    對象允許在二進制資料結構之間共享記憶體。
  2. memoryview

    對象的切片是一個新

    memoryview

    對象,而且不會

    複制位元組序列

  3. 如果使用

    mmap

    子產品把圖像打開為記憶體映射檔案,那麼會複制少量位元組

4.2 編碼和解碼

  1. 某些編碼(如

    ASCII

    和多位元組的

    GB2312

    )不能表示所有

    Unicode

    字元
  2. UTF 編碼的設計目的就是處理每一個

    Unicode

    碼位
  3. 典型編碼:
    1. latin1(即 iso8859_1):一種重要的編碼,是其他編碼的基礎,例如 cp1252 和Unicode(注意,latin1 與 cp1252 的位元組值是一樣的,甚至連碼位也相同)。
    2. cp1252:Microsoft 制定的 latin1 超集,添加了有用的符号,例如彎引号和€(歐元);有些 Windows 應用把它稱為“ANSI”,但它并不是 ANSI 标準。
    3. cp437:IBM PC 最初的字元集,包含框圖符号。與後來出現的 latin1 不相容。
    4. gb2312:用于編碼簡體中文的陳舊标準;這是亞洲語言中使用較廣泛的多位元組編碼之一。
    5. utf-8:目前 Web 中最常見的 8 位編碼;與 ASCII 相容(純 ASCII 文本是有效的 UTF-8 文本)。
    6. utf-16le:UTF-16 的 16 位編碼方案的一種形式;所有 UTF-16 支援通過轉義序列(稱為“代理對”,surrogate pair)表示超過 U+FFFF 的碼位。
  4. UnicodeEncodeError

    :多數非 UTF 編解碼器隻能處理 Unicode 字元的一小部分子集。把文本轉換成位元組序列時,如果目标編碼中沒有定義某個字元,那就會抛出

    UnicodeEncodeError 異常,除非把 errors 參數傳給編碼方法或函數,對錯誤進行特殊處理。

    無法編碼時:

    1. error='ignore'

      處理方式悄無聲息地跳過無法編碼的字元;這樣做通常很是不妥。
    2. 編碼時指定

      error='replace'

      ,把無法編碼的字元替換成 ‘?’;資料損壞了,但是使用者知道出了問題。
    3. error='xmlcharrefreplace'

      把無法編碼的字元替換成 XML 實體。
    4. 編解碼器的錯誤處理方式是可擴充的。你可以為 errors 參數注冊額外的字元串,方法是把一個名稱和一個錯誤處理函數傳給

      codecs.register_error

      函數。
  5. UnicodeDecodeError

    :把二進制序列轉換成文本時,遇到無法轉換的位元組序列時會抛出UnicodeDecodeError;另一方面,很多陳舊的 8 位編碼——如 ‘cp1252’、‘iso8859_1’ 和’koi8_r’——能解碼任何位元組序列流而不抛出錯誤,例如随機噪聲。是以,如果程式使用錯誤的 8 位編碼,解碼過程悄無聲息,而得到的是無用輸出。
  6. 亂碼字元稱為鬼符(gremlin)或 mojibake(文字化け,“變形文本”的日文)。
  7. 使用預期之外的編碼加載子產品時抛出的SyntaxError
    1. Python 3 為所有平台設定的預設編碼都是 UTF-8
    2. Python 3 允許在源碼中使用非 ASCII 辨別符
  8. Chardet

    :識别所支援的30中編碼。解決問題:如何找出位元組序列的編碼?
  9. 二進制序列編碼文本通常不會明确指明自己的編碼,但是 UTF 格式可以在文本内容的開頭添加一個位元組序标記。
  10. BOM:位元組序标記,byte-order-mark,指明編碼時使用 Intel CPU 的小位元組序
  11. 在小位元組序裝置中,各個碼位的最低有效位元組在前面:字母 ‘E’ 的碼位是 U+0045(十進制數 69),在位元組偏移的第 2 位和第 3 位編碼為 69 和0。
  12. 在大位元組序 CPU 中,編碼順序是相反的;‘E’ 編碼為 0 和 69。
  13. 為了避免混淆,UTF-16 編碼在要編碼的文本前面加上特殊的不可見字元 ZERO WIDTH NO-BREAK SPACE(U+FEFF)。在小位元組序系統中,這個字元編碼為 b’\xff\xfe’(十進制數 255, 254)。因為按照設計,U+FFFE 字元不存在,在小位元組序編碼中,位元組序列 b’\xff\xfe’ 必定是 ZERO WIDTH NO-BREAK SPACE,是以編解碼器知道該用哪個位元組

    序。

  14. UTF-16 有兩個變種:UTF-16LE,顯式指明使用小位元組序;UTF-16BE,顯式指明使用大位元組序。如果使用這兩個變種,不會生成 BOM。
  15. 與位元組序有關的問題隻對一個字(word)占多個位元組的編碼(如 UTF-16 和 UTF-32)有影響。UTF-8 的一大優勢是,不管裝置使用哪種位元組序,生成的位元組序列始終一緻,是以不需要 BOM。
  16. 盡管如此,某些Windows 應用(尤其是 Notepad)依然會在 UTF-8 編碼的檔案中添加

    BOM;而且,Excel 會根據有沒有 BOM 确定檔案是不是 UTF-8 編碼,否則,它假設内容使用 Windows 代碼頁(codepage)編碼。UTF-8 編碼的 U+FEFF 字元是一個三位元組序列:

    b'\xef\xbb\xbf'

    。是以,如果檔案以這三個位元組開頭,有可能是帶有 BOM 的 UTF-8 檔案。然而,Python 不會因為檔案以 b’\xef\xbb\xbf’ 開頭就自動假定它是 UTF-8編碼的。

4.3 處理文本檔案

  1. 處理文本的最佳實踐是 Unicode三明治 。
    1. 要盡早把輸入(例如讀取檔案時)的位元組序列解碼成字元串。
    2. 在程式的業務邏輯中隻能處理字元串對象。在其他處理過程中,一定不能編碼或解碼。
    3. 對輸出來說,則要盡量晚地把字元串編碼成位元組序列。
    《流暢的python》學習筆記及書評《流暢的python》學習筆記
  2. 如果打開檔案是為了寫入,但是沒有指定編碼參數,會使用區域設定中的預設編碼,而且使用那個編碼也能正确讀取檔案。
  3. 如果腳本要生成檔案,而位元組的内容取決于平台或同一平台中的區域設定,那麼就可能導緻相容問題。
  4. 探索編碼預設值
    import sys, locale
    
    expressions = """
    locale.getpreferredencoding()
    type(my_file)
    my_file.encoding
    sys.stdout.isatty()
    sys.stdout.encoding
    sys.stdin.isatty()
    sys.stdin.encoding
    sys.stderr.isatty()
    sys.stderr.encoding
    sys.getdefaultencoding()
    sys.getfilesystemencoding()
    """
    
    my_file = open('dummy', 'w')
    
    for expression in expressions.split():
        
        value = eval(expression)
        print(expression.rjust(30), '->', repr(value))
               
    輸出:
    locale.getpreferredencoding() -> 'cp936'
                     type(my_file) -> <class '_io.TextIOWrapper'>
                  my_file.encoding -> 'cp936'
               sys.stdout.isatty() -> False
               sys.stdout.encoding -> 'UTF-8'
                sys.stdin.isatty() -> False
                sys.stdin.encoding -> 'cp936'
               sys.stderr.isatty() -> False
               sys.stderr.encoding -> 'UTF-8'
          sys.getdefaultencoding() -> 'utf-8'
       sys.getfilesystemencoding() -> 'utf-8'
               
  5. 如果打開檔案時沒有指定

    encoding

    參數,預設值由

    locale.getpreferredencoding()

    提供
  6. 如果設定了

    PYTHONIOENCODING

    環境變量,

    sys.stdout/stdin/stderr

    的編碼使用設定的值;否則,繼承自所在的控制台;如果輸入/輸出重定向到檔案,則由

    locale.getpreferredencoding()

    定義
  7. Python在二進制資料和字元串之間轉換時,内部使用

    sys.getdefaultencoding()

    獲得的編碼;Python3很少如此,但仍有發生。這個設定不能修改。
  8. sys.getfilesystemencoding()

    用于編解碼檔案名(不是檔案内容)。把字元串參數作為檔案名傳給

    open()

    函數時就會使用它;如果傳入的檔案名參數是位元組序列,那就不經改動直接傳給 OS API。

4.4 規範化Unicode字元串

  1. 因為 Unicode 有組合字元(變音符号和附加到前一個字元上的記号,列印時作為一個整體),是以字元串比較起來很複雜。
    s1 = 'café'
    s2 = 'cafe\u0301'
    
    s1, s2
    len(s1), len(s2)
    s1 == s2
    
    # ('café', 'café')
    # (4, 5)
    # False
               
  2. 在Unicode 标準中,‘é’ 和 ‘e\u0301’ 這樣的序列叫“标準等價物”(canonical equivalent),應用程式應該把它們視作相同的字元。但是,Python 看到的是不同的碼位序列,是以判定二者不相等。
  3. 這個問題的解決方案是使用

    unicodedata.normalize

    函數提供的Unicode 規範化。這個函數的第一個參數是這 4 個字元串中的一個:‘NFC’、‘NFD’、‘NFKC’ 和 ‘NFKD’。
    from unicodedata import normalize
    
    len(normalize('NFC', s1)), len(normalize('NFC', s3))
    # (4, 4)
    len(normalize('NFD', s1)), len(normalize('NFD', s3))
    # (5, 5)
    normalize('NFC', s1) == normalize('NFC', s2)
    # True
    normalize('NFD', s1) == normalize('NFD', s3)
    # True
               
  4. 西方鍵盤通常能輸出組合字元,是以使用者輸入的文本預設是 NFC 形式。不過,安全起見,儲存文本之前,最好使用

    normalize('NFC', user_text)

    清洗字元串。
  5. NFC 也是 W3C 的“Character Model for the World Wide Web: String Matching and Searching”規範推薦的規範化形式。
  6. 使用 NFC 時,有些單字元會被規範成另一個單字元。這兩個字元在視覺上是一樣的,但是比較時并不相等,是以要規範化,防止出現意外。
  7. 在另外兩個規範化形式(NFKC 和 NFKD)的首字母縮略詞中,字母 K 表示 “compatibility”(相容性)。這兩種是較嚴格的規範化形式,對“相容字元”有影響。雖然 Unicode 的目标是為各個字元提供 “規範的” 碼位,但是為了相容現有的标準,有些字元會出現多次。

    微符号是一個 “相容字元”。

  8. 使用 NFKC 和 NFKD 規範化形式時要小心,而且隻能在特殊情況中使用,例如搜尋和索引,而不能用于持久存儲,因為這兩種轉換會導緻資料損失。
  9. 大小寫折疊:

    str.casefold()

    ,就是把所有文本變成小寫,再做些其他轉換,與

    str.lower()

    基本一緻,但是存在例外:微符号 ‘μ’ 會變成小寫的希臘字母“μ”(在多數字型中二者看起來一樣);德語 Eszett(“sharp s”,ß)會變成“ss”
  10. 去掉變音符号的優勢:
    1. 搜尋友善:人們有時很懶,或者不知道怎麼正确使用變音符号,而且拼寫規則會随時間變化,是以實際語言中的重音經常變來變去。
    2. URL可讀性:去掉變音符号還能讓 URL 更易于閱讀,至少對拉丁語系語言是如此。
  11. 變音符号對排序有影響的情況很少發生,隻有兩個詞之間唯有變音符号不同時才有影響。此時,帶有變音符号的詞排在正常詞的後面。
  12. 在 Python 中,非 ASCII 文本的标準排序方式是使用

    locale.strxfrm

    函數,根據 locale 子產品的文檔,這個函數會“把字元串轉換成适合所在區域進行比較的形式”。
  13. PyUCA:Unicode 排序算法(Unicode Collation Algorithm,UCA)的純 Python 實作。PyUCA 沒有考慮區域設定。如果想定制排序方式,可以把自定義的排序表路徑傳給 Collator() 構造方法。PyUCA 預設使用項目自帶的

    allkeys.txt

    import pyuca
    
    coll = pyuca.Collator()
    fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
    sorted_fruits = sorted(fruits, key=coll.sort_key)
    sorted_fruits
               
  14. Unicode 标準提供了一個完整的資料庫(許多格式化的文本檔案),不僅包括碼位與字元名稱之間的映射,還有各個字元的中繼資料,以及字元之間的關系。

    Unicode 資料庫記錄了字元是否可以列印、是不是字母、是不是數字,或者是不是其他數值符号。unicodedata 子產品中有幾個函數用于擷取字元的中繼資料。例如,字元在标準中的官方名稱是不是組合字元(如結合波形符構成的變音符号等),以及符号對應的人類可讀數值(不是碼位)。

  15. import unicodedata
    import re
    
    re_digit = re.compile(r'\d')
    sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'
    
    for char in sample:
        print(
            'U+%04x' % ord(char),
            char.center(6),
            're_dig' if re_digit.match(char) else '-',
            'isdig' if char.isdigit() else '-',
            'isnum' if char.isnumeric() else '-',
            format(unicodedata.numeric(char), '5.2f'),
            unicodedata.name(char),
            sep='\t'
        )
               
    《流暢的python》學習筆記及書評《流暢的python》學習筆記

4.5 支援字元串和位元組序列的雙模式API

  1. 可以使用正規表達式搜尋字元串和位元組序列,但是在後一種情況中,ASCII 範圍外的位元組不會當成數字群組成單詞的字母。
    1. 字元串模式 r’\d+’ 能比對泰米爾數字和 ASCII 數字。
    2. 位元組序列模式 rb’\d+’ 隻能比對 ASCII 位元組中的數字。
    3. 字元串模式 r’\w+’ 能比對字母、上标、泰米爾數字和 ASCII 數字。
    4. 位元組序列模式 rb’\w+’ 隻能比對 ASCII 位元組中的字母和數字。
  2. 字元串正規表達式有個 re.ASCII 标志,它讓\w、\W、\b、\B、\d、\D、\s 和 \S 隻比對 ASCII 字元。
  3. 為了便于手動處理字元串或位元組序列形式的檔案名或路徑名,os 子產品提供了特殊的編碼和解碼函數。
    1. fsencode(filename)

      :如果 filename 是 str 類型(此外還可能是 bytes 類型),使用

      sys.getfilesystemencoding()

      傳回的編解碼器把 filename 編碼成位元組序列;否則,傳回未經修改的 filename 位元組序列。
    2. fsdecode(filename)

      :如果 filename 是 bytes 類型(此外還可能是 str 類型),使用

      sys.getfilesystemencoding()

      傳回的編解碼器把 filename 解碼成字元串;否則,傳回未經修改的 filename 字元串。
    3. surrogateescape

      :在 Unix 衍生平台中,這些函數使用 surrogateescape 錯誤處理方式以避免遇到意外位元組序列時卡住。Windows 使用的錯誤處理方式是 strict。

      這種錯誤處理方式會把每個無法解碼的位元組替換成 Unicode 中 U+DC00 到 U+DCFF 之間的碼位(Unicode 标準把這些碼位稱為“Low Surrogate Area”),這些碼位是保留的,沒有配置設定字元,供應用程式内部使用。編碼時,這些碼位會轉換成被替換的位元組值。

    4. 在 Python 3.3 之前,編譯 CPython 時可以配置在記憶體中使用 16 位或 32 位存儲各個碼位。16 位是“窄建構”(narrow build),32 位是“寬建構”(wide build)。如果想知道用的是哪個,要檢視

      sys.maxunicode

      的值:65535 表示“窄建構”,不能透明地處理U+FFFF 以上的碼位。“寬建構”沒有這個限制,但是消耗的記憶體更多:每個字元占 4 個位元組,就算是中文象形文字的碼位大多數也隻占 2 個位元組。這兩種建構沒有高下之分,應該根據自己的需求選擇。
    5. 靈活的字元串表述類似于 Python 3 對 int 類型的處理方式:如果一個整數在一個機器字中放得下,那就存儲在一個機器字中;否則解釋器切換成變長表述,類似于 Python 2 中的 long 類型。

5. 一等函數

  1. 在 Python 中,函數是一等對象。一等對象的定義:
    1. 在運作時建立
    2. 能指派給變量或資料結構中的元素
    3. 能作為參數傳給函數
    4. 能作為函數的傳回結果
  2. 人們經常将“把函數視作一等對象”簡稱為“一等函數”。這樣說并不完美,似乎表明這是函數中的特殊群體。在 Python 中,所有函數都是一等對象。

5.1 把函數視作對象

  1. __doc__

    是函數對象衆多屬性中的一個,顯示函數的備注
  2. help(f)

    :輸出的文本來自函數對象的

    __doc__

    屬性
  3. 函數對象的 一等 本性:
    1. 把 factorial 函數指派給變量 fact,然後通過變量名調用
    2. 把它作為參數傳給map 函數,傳回一個可疊代對象,裡面的元素是把第一個參數

      (一個函數)應用到第二個參數(一個可疊代對象)中各個元素上得到的結果

  4. 高階函數:higher-order function,接受函數為參數,或者把函數作為結果傳回的函數

    這樣的函數例如:map,sorted,reduce,filter,apply(已移除)

    map、filter 和 reduce 這三個高階函數還能見到,不過多數使用場景下都有更好的替代品。

    1. 清單推導 或 生成器表達式 具有map和filter兩個函數的功能,且更易于閱讀
      list(map(fact, range(6)))
      [fact(n) for n in range(6)]
                 
      list(map(factorial, filter(lambda n: n % 2, range(6))))
      [fact(n) for n in range(7) if n % 2 != 0]
                 
    2. reduce 是内置函數,最常用于求和,現在最好使用内置的 sum 函數,在可讀性和性能方面,這是一項重大改善

      sum 和 reduce 的通用思想是把某個操作連續應用到序列的元素上,累計之前的結果,把一系列值歸約成一個值。

      from functools import reduce
      from operator import add
      
      reduce(add, range(100))
      sum(range(100))
                 
    3. all 和 any 也是内置的歸約函數
      • all(iterable)

        :如果 iterable 的每個元素都是真值,傳回 True;

        all([])

        傳回True。
      • any(iterable)

        :隻要 iterable 中有元素是真值,就傳回 True;

        any([])

        傳回False。
  5. 匿名函數:lambda 關鍵字在 Python 表達式内建立匿名函數。

    lambda 句法隻是文法糖:與 def 語句一樣,lambda 表達式會建立函數對象。這是 Python 中幾種可調用對象的一種。

  6. 調用類的時候會運作類的

    __new__

    方法建立一個執行個體,然後運作

    __init__

    方法,初始化執行個體,最後把執行個體傳回給調用方。
    • 因為Python沒有new運算符,是以調用類相當于調用函數。
    • 如果類定義了

      __call__

      方法,那麼它的執行個體可以作為函數調用。
  7. 生成器函數:使用

    yield

    關鍵字的函數或方法。調用生成器函數傳回的是生成器對象。(生成器函數還可以作為協程)
  8. Python 中有各種各樣可調用的類型,是以判斷對象能否調用,最安全的方法是使用内置的

    callable()

    函數
    [callable(obj) for obj in (abs, str, 13)]
    
    # [True, True, False]
               
  9. 任何 Python 對象都可以表現得像函數。為此,隻需實作執行個體方法

    __call__

  10. 函數内省:除了

    __doc__

    ,函數對象還有其他屬性:

    dir(factorial)

    ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
               
    1. 與使用者定義的正常類一樣,函數使用

      __dict__

      屬性存儲賦予它的使用者屬性。這相當于一種基本形式的注解。
    2. 列出 正常對象沒有 而 函數對象有 的屬性:
      class C: pass
      obj = C()
      def func(): pass
      
      str(sorted(set(dir(func)) - set(dir(obj))))
                 
      ['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
                 
    3. 使用者定義函數的屬性:
      名稱 類型 說明

      __annotations__

      dict 參數和傳回值的注解

      __call__

      method-wrapper 實作()運算符;即可調用對象協定

      __closure__

      tuple 函數閉包,即自由變量的綁定(通常是None)

      __code__

      code 編譯成位元組碼的函數中繼資料和函數定義體

      __defaults__

      tuple 形式參數的預設值

      __get__

      method-wrapper 實作隻讀描述符協定

      __globals__

      dict 函數所在子產品中的全局變量

      __kwdefaults__

      dict 僅限關鍵字形式參數的預設值

      __name__

      str 函數名稱

      __qualname__

      str 函數的限定名稱,如

      Random.choice

5.2 函數的參數和注解

  1. 僅限關鍵字參數:keyword-only argument,調用函數時使用

    *

    **

    展開可疊代對象,映射到單個參數。
  2. 在傳參的時候,将字典加上

    **

    作為參數傳遞,實作的是字典中所有元素作為單個參數傳入,同名的鍵回綁定到對應的具名參數上,餘下的則被

    **attrs

    捕獲。
  3. 僅限關鍵字參數:它一定不會捕獲未命名的定位參數。

    此種參數隻能由關鍵字提供,絕對不會被位置參數自動填充。

    • 定義函數時若想指定僅限關鍵字參數,要把它們放到前面有

      *

      的參數後面。
    • 如果不想支援數量不定的定位參數,但是想支援僅限關鍵字參數,在簽名中放

      一個

      *

    • 允許正常參數出現在可變參數之後:此時這個正常參數就是一個僅限關鍵字參數。強制性的,它隻能通過關鍵字傳參
    • 僅限關鍵字參數不需要有預設值, 由于Python需要将所有的參數都綁定一個值,而且将值綁定到關鍵字參數的唯一方法是通過這個關鍵字,是以這種參數是 需要關鍵字的參數 。是以這些參數必須通過調用方提供,且必須通過關鍵字提供值。
    • 文法上的更改是允許省略可變參數的參數名。這意味着對于一個有僅限關鍵字參數的函數來說,它不會再接受一個可變參數。
  4. 擷取關于參數的資訊
    1. 使用HTTP微架構Bobo中有個使用函數内省的好例子。

      啟動方式:

      bobo -f hello.py

      import bobo
      
      @bobo.query('/')
      def hello(person):
          return "Hello %s" % person
                 
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
    2. 函數對象有個

      __defaults__

      屬性,它的值是一個元組,裡面儲存着定位參數和關鍵字參數的預設值。僅限關鍵字參數的預設值在

      __kwdefaults__

      屬性中。然而,參數的名稱在

      __code__

      屬性中,它的值是一個 code 對象引用,自身也有很多屬性。
  5. 通過

    inspect.signature

    提取函數的簽名
    1. inspect.signature

      函數傳回一個

      inspect.Signature

      對象,它有一個

      parameters

      屬性,這是一個有序映射,把參數名和

      inspect.Parameter

      對象對應起來。各個

      Parameter

      屬性也有自己的屬性,例如

      name

      default

      kind

      。特殊的

      inspect._empty

      值表示沒有預設值,考慮到

      None

      是有效的預設值(也經常這麼做),而且這麼做是合理的。
    2. kind

      的屬性的值,為

      _ParameterKind

      類中的5個值之一:
      1. POSITIONAL_OR_KEYWORD

        :可以通過定位參數和關鍵字參數傳入的形參(多數 Python 函數的參數屬于此類)。
      2. VAR_POSITIONAL

        :定位參數元組。
      3. VAR_KEYWORD

        :關鍵字參數元組。
      4. KEYWORD_ONLY

        :僅限關鍵字參數(Python 3 新增)。
      5. POSITIONAL_ONLY

        :僅限定位參數;目前,Python 聲明函數的句法不支援,但是有些使用 C 語言實作且不接受關鍵字參數的函數(如 divmod)支援。
  6. 函數注解
    1. 函數聲明中的各個參數可以在 : 之後增加注解表達式。如果參數有預設值,注解放在參數名和 = 号之間。
    2. 如果想注解傳回值,在 ) 和函數聲明末尾的 : 之間添加 -> 和一個表達式。那個表達式可以是任何類型。
    3. 注解中最常用的類型是類(如 str 或 int)和字元串(如 ‘int > 0’)
    4. 注解不會做任何處理,隻是存儲在函數的

      __annotations__

      屬性中
    5. Python 對注解所做的唯一的事情是,把它們存儲在函數的

      __annotations__

      屬性裡。僅此而已,Python 不做檢查、不做強制、不做驗證,什麼操作都不做。換句話說,注解對 Python 解釋器沒有任何意義。
    6. 函數注解的最大影響或許不是讓 Bobo 等架構自動設定,而是為 IDE 和

      lint 程式等工具中的靜态類型檢查功能提供額外的類型資訊。

5.3 支援函數式程式設計的包

  1. operator子產品
    1. 使用reduce函數和一個匿名函數計算階乘
      def fact(n):
          return reduce(lambda a,b: a*b, range(1, n+1))
                 
    2. operator 子產品為多個算術運算符提供了對應的函數,進而避免編寫

      lambda a, b: a*b

      這種平凡的匿名函數
      def fact2(n):
          return reduce(mul, range(1, n+1))
                 
    3. 使用 itemgetter 排序一個元組清單
      • itemgetter 使用

        []

        運算符,是以它不僅支援序列,還支援映射和任何實作

        __getitem__

        方法的類。
      • itemgetter(1)

        的作用與

        lambda fields:fields[1]

        一樣
      metro_data = [
      ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
      ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
      ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
      ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
      ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
      ]
      
      from operator import itemgetter
      for city in sorted(metro_data, key=itemgetter(2)):
          print(city)
                 
    4. 如果把多個參數傳給 itemgetter,它建構的函數會傳回提取的值構成的元組:
      cc_name = itemgetter(1, 0)
      
      for city in metro_data:
          print(cc_name(city))
                 
    5. attrgetter 與 itemgetter 作用類似,它建立的函數根據名稱提取對象的屬性
      from collections import namedtuple
      
      LatLong = namedtuple('LatLong', 'lat long')
      Metropolis = namedtuple('Metropolis', 'name cc pop coord')
      metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for name, cc, pop, (lat, long) in metro_data]
      metro_areas
      metro_areas[0].coord.lat
      
      from operator import attrgetter
      name_lat = attrgetter('name', 'coord.lat')
      for city in sorted(metro_areas, key=attrgetter('coord.lat')):
          print(name_lat(city))
                 
    6. attrgetter 與 itemgetter 個人總結:
      1. itemgetter:偏向于數組的切片操作
      2. attrgetter:偏向于類的屬性擷取操作
    7. methodcaller 的作用與 attrgetter 和 itemgetter 類似,它會自行建立函數,methodcaller 建立的函數會在對象上調用參數指定的方法:
      from operator import methodcaller
      s = 'The tiem has come'
      
      upcase = methodcaller('upper')
      upcase(s)
      hiphenate = methodcaller('replace', ' ', '-')
      hiphenate(s)
                 
    8. methodcaller還可以當機某些參數,也就是部分應用(partial application),這與

      functoosl.partial

      函數的作用類似。
  2. 使用

    functools.partial

    當機參數
    • functools.partial 這個高階函數用于部分應用一個函數。部分應用是指,基于一個函數建立一個新的可調用對象,把原函數的某些參數固定。使用這個函數可以把接受一個或多個參數的函數改編成需要回調的API,這樣參數更少。
      from operator import mul
      from functools import partial
      triple = partial(mul, 3)
      triple(7)
      
      list(map(triple, range(1, 10)))
                 
    • 固定nfc函數,标準化字元串編碼處理
      import unicodedata, functools
      nfc = functools.partial(unicodedata.normalize, 'NFC')
      
      s1 = 'café'
      s2 = 'cafe\u0301'
      
      s1, s2
      s1 == s2
      nfc(s1) == nfc(s2)
                 
    • functools.partialmethod

      函數的作用與

      partial

      一樣,不過是用于處理方法的。
      • 對于類方法,partial就沒辦法了,是以新引用了

        partialmethod

      • 參考連結
      • Python functools 子產品
    • functools 子產品中的 lru_cache 函數令人印象深刻,它會做備忘(memoization),這是一種自動優化措施,它會存儲耗時的函數調用結果,避免重新計算。

6. 使用一等函數實作涉及模式

  1. 實作“政策” 模式:
    • 經典“政策”模式
    • 使用函數實作“政策”模式
  2. 政策對象通常是很好的享元:
    • 享元:flyweight,享元是可共享的對象,可以同時再多個上下文中使用
    • 可以避免運作時消耗
    • 函數比使用者定義的類的執行個體輕量,而且無需使用“享元”模式,因為各個政策函數在 Python 編譯子產品時隻會建立一次。普通的函數也是“可共享的對象,可以同時在多個上下文中使用”。
  3. 在 Python 中,子產品也是一等對象,而且标準庫提供了幾個處理子產品的函數
    1. globals()

      :傳回一個字典,表示目前的全局符号表。這個符号表始終針對目前子產品(對函數或方法來說,是指定義它們的子產品,而不是調用它們的模

      塊)。

  4. 指令模式
    • 可以通過把函數作為參數傳遞而簡化。
    • “指令”模式的目的是解耦調用操作的對象(調用者)和提供實作的對象(接收者)。
    • 這個模式的做法是,在二者之間放一個 Command 對象,讓它實作隻有一個方法(execute)的接口,調用接收者中的方法執行所需的操作。這樣,調用者無需了解接收者的接口,而且不同的接收者可以适應不同的 Command 子類。調用者有一個具體的指令,通過調用 execute 方法執行。
    class MacroCommand:
        """一個執行一組指令的指令"""
        def __init__(self, commands):
            self.commands = list(commands)
            
        def __call__(self):
            for command in self.commands:
                command()
               
  5. 深入淺出python閉包
  6. 複習
    1. 一等對象:指的是滿足下述條件的程式實體
      1. 在運作時建立
      2. 能指派給變量或資料結構中的元素
      3. 能作為參數傳給函數
      4. 能作為函數的傳回結果
    2. 整數、字元串和字典都是一等對象。在面向對象程式設計中,函數也是對象,并滿足以上條件,是以函數也是一等對象,稱為"一等函數"
    3. 普通函數 & 高階函數:接受函數為參數的函數為高階函數,其餘為普通函數
    4. 《設計模式:可複用面向對象軟體的基礎》書中的兩個設計原則:
      1. 對接口程式設計,而不是對實作程式設計
      2. 優先使用對象組合,而不是類繼承

7. 函數裝飾器和閉包

  1. 函數裝飾器用于在源碼中“标記”函數,以某種方式增強函數的行為。這是一項強大的功能,但是若想掌握,必須了解閉包。
  2. 除了在裝飾器中有用處之外,閉包還是回調式異步程式設計和函數式程式設計風格的基礎。

7.1 裝飾器基礎

  1. 裝飾器是可調用的對象,其參數是另一個函數(被裝飾的函數)。裝飾器可能會處理被裝飾的函數,然後把它傳回,或者将其替換成另一個函數或可調用對象。

    下述兩個代碼等價:

    1. 裝飾器
      @decorate
      def target():
          print('running target()')
                 
    2. 另一種寫法
      def target():
          print('running target()')
          
      target = decorate(target)
                 
  2. 兩種寫法的最終結果一樣:上述兩個代碼片段執行完畢後得到的 target 不一定是原來那個 target 函數,而是 decorate(target) 傳回的函數。
  3. 裝飾器通常把函數替換成另一個函數
    def deco(func):
        def inner():
            print('running inner()')
        return inner
        
    @deco
    def target():
        print('running target()')
        
    target()
    # running inner()
    target
    # <function __main__.deco.<locals>.inner()>
               
  4. 嚴格來說,裝飾器隻是文法糖。
  5. 裝飾器的特性:
    1. 一大特性是,能把被裝飾的函數替換成其他函數。
    2. 第二個特性是,裝飾器在加載子產品時立即執行。
  6. Python何時執行裝飾器:在被裝飾的函數定義之後立即運作
    registry = []
    
    def register(func):
        print('running register(%s)' % func)
        registry.append(func)
        return func
    
    @register
    def f1():
        print('running f1()')
        
    @register
    def f2():
        print('running f2()')
        
    def f3():
        print('running f3()')
        
    def main():
        print('running main()')
        print('registry ->', registry)
        f1()
        f2()
        f3()
        
    if __name__ == '__main__':
        main()
               
    running register(<function f1 at 0x00000246ABFAC708>)
    running register(<function f2 at 0x00000246AC0D4CA8>)
    running main()
    registry -> [<function f1 at 0x00000246ABFAC708>, <function f2 at 0x00000246AC0D4CA8>]
    running f1()
    running f2()
    running f3()
               
  7. 如果導入上述代碼:

    import registration

    ,函數的裝飾器再導入子產品時立即執行,而被裝飾的函數隻再明确調用時運作。
  8. 裝飾器在真實代碼中的常用方式:
    1. 裝飾器通常在一個子產品中定義,然後應用到其他子產品中的函數上
    2. 大多數裝飾器會在内部定義函數,然後将其傳回
  9. register 裝飾器原封不動地傳回被裝飾的函數,但是這種技術并非沒有用處。
    1. 很多 Python Web 架構使用這樣的裝飾器把函數添加到某種中央注冊處,例如把 URL 模式映射到生成 HTTP 響應的函數上的注冊處。
    2. 這種注冊裝飾器可能會也可能不會修改被裝飾的函數。
  10. 使用裝飾器改進“政策”模式
    • 多數裝飾器會修改被裝飾的函數
    • 通常,它們會定義一個内部函數,然後将其傳回,替換被裝飾的函數
    • 使用内部函數的代碼幾乎都要靠閉包才能正确運作

7.2 閉包

  1. 變量作用域規則
    1. Python 不要求聲明變量,但是假定在函數定義體中指派的變量是局部變量
    2. 如果在函數中指派時想讓解釋器把 b 當成全局變量,要使用

      global

      聲明
    3. 通過

      from dis import dis

      檢視程式的位元組碼
  2. 假如有個名為 avg 的函數,它的作用是計算不斷增加的系列值的均值;例如,整個曆史中某個商品的平均收盤價。每天都會增加新價格,是以平均值要考慮至目前為止所有的價格
    • avg使用方法:
      >>> avg(10)
      10.0
      >>> avg(11)
      10.5
      >>> avg(12)
      11.0
                 
    • 實作方式:
      1. 計算移動平均的類
        class Averager():
            def __init__(self):
                self.series = []
                
            def __call__(self, new_value):
                self.series.append(new_value)
                total = sum(self.series)
                return total/len(self.series)
                   
      2. 計算移動平均值的高階函數
        def make_averager():
            series = []
            
            def averager(new_value):
                series.append(new_value)
                total = sum(series)
                return total/len(series)
            
            return averager
                   
      3. **注意:**這兩個示例有共通之處:調用

        Averager()

        make_averager()

        得到一個可調用對象 avg,它會更新曆史值,然後計算目前均值
        1. averager

          函數中,

          series

          時自由變量(free variable),這是一個技術術語,指:未在本地作用域中綁定的變量
          《流暢的python》學習筆記及書評《流暢的python》學習筆記
        2. averager 的閉包延伸到那個函數的作用域之外,包含自由變量 series 的綁定
        3. 審查傳回的 averager 對象,我們發現 Python 在

          __code__

          屬性(表示編譯後的函數定義體)中儲存局部變量和自由變量的名稱
          avg.__code__.co_varnames
          # ('new_value', 'total')
          
          avg.__code__.co_freevars
          # ('series',)
                     
        4. series 的綁定在傳回的 avg 函數的

          __closure__

          屬性中。

          avg.__closure__

          中的各個元素對應于

          avg.__code__.co_freevars

          中的一個名稱。這些元素是 cell 對象,有個

          cell_contents

          屬性,儲存着真正的值
          avg.__closure__
          # (<cell at 0x00000246AC517108: list object at 0x00000246AC124A88>,)
          
          avg.__closure__[0].cell_contents
          # [10, 11, 12]
                     
  3. 閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,這樣調用函數時,雖然定義作用域不可用了,但是仍能使用那些綁定。
  4. 注意,隻有嵌套在其他函數中的函數才可能需要處理不在全局作用域中的外部變量。
  5. nonlocal聲明
    1. 上面的操作時引入了list(可變對象) series,用來儲存每一次的值,然後這個series 是 所謂的 自由變量
    2. 但是對數字、字元串、元組等不可變類型來說,隻能讀取,不能更新,就不是所謂的 自由變量了,是以不會儲存在閉包中。
    3. nonlocal

      聲明可以把變量标記為自由變量,即使在函數中為變量賦予新值了,也會變成自由變量。
    4. 如果為

      nonlocal

      聲明的變量賦予新值,閉包中儲存的綁定會更新。
    5. 優化後的閉包
      def make_averager():
          count = 0
          total = 0
          
          def averager(new_value):
              nonlocal count, total
              count += 1
              total += new_value
              return total/count
          
          return averager
                 
    6. nonlocal是python3的特性,在python2中需要把内部函數需要修改的變量存儲為可變對象(如字典或簡單的執行個體)的元素或屬性,并且把那個對象綁定給一個自由變量。

7.3 裝飾器

  1. 實作一個簡單的裝飾器:定義了一個裝飾器,它會在每次調用被裝飾的函數時計時,然後把經過的時間、傳入的參數和調用的結果列印出來。
    import time
    
    def clock(func):
        def clocked(*args):
            # 記錄初始時間t0
            t0 = time.perf_counter()
            # 調用原來的 factorial 函數,儲存結果
            result = func(*args)
            # 計算經過的時間
            elapsed = time.perf_counter() - t0
            
            # 格式化收集的資料,然後列印出來
            name = func.__name__
            arg_str = ', '.join(repr(arg) for arg in args)
            print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
            # 傳回第 2 步儲存的結果
            return result
        return clocked
    
    @clock
    def snooze(seconds):
        time.sleep(seconds)
        
    @clock
    def factorial(n):
        return 1 if n < 2 else n * factorial(n - 1)
    
    if __name__ == '__main__':
        print('*' * 40, 'Calling snooze(.123)')
        snooze(.123)
        print('*' * 40, 'Calling factorial(6)')
        print('6! = ', factorial(6))
               
    **************************************** Calling snooze(.123)
    [0.12354480s] snooze(0.123) -> None
    **************************************** Calling factorial(6)
    [0.00000040s] factorial(1) -> 1
    [0.00003500s] factorial(2) -> 2
    [0.00004590s] factorial(3) -> 6
    [0.00005500s] factorial(4) -> 24
    [0.00006390s] factorial(5) -> 120
    [0.00007470s] factorial(6) -> 720
    6! =  720
               
  2. 這是裝飾器的典型行為:把被裝飾的函數替換成新函數,二者接受相同的參數,而且(通常)傳回被裝飾的函數本該傳回的值,同時還會做些額外操作。

    裝飾器:動态地給一個對象添加一些額外的職責

    ——《設計模式:可複用面向對象軟體的基礎》

  3. 上述實作的clock裝飾器有幾個缺點:
    1. 不支援關鍵字參數
    2. 遮蓋了被裝飾函數的

      __name__

      __doc__

      屬性
  4. 使用

    functools.wraps

    裝飾器把相關屬性從func複制到clocked中,同時還可以正确處理關鍵字參數
    import time
    from functools import wraps
    
    def clock(func):
        @wraps(func)
        def clocked(*args, **kwargs):
            t0 = time.perf_counter()
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            
            arg_lst = []
            if args:
                arg_lst.append(', '.join(repr(arg) for arg in args))
            if kwargs:
                pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
                arg_lst.append(', '.join(pairs))
            arg_str = ', '.join(arg_lst)
            print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
            return result
        return clocked
               
  5. 标準庫中的裝飾器
    1. python 内置了三個用于裝飾方法的函數:property、classmethod、staticmethod
    2. functools:wraps、lru_cache、singledispatch
  6. 使用

    functools.lru_cache

    做備忘,memoization,這是一項優化技術,它把耗時的函數的結果儲存起來,避免傳入相同的參數時重複計算。
    • LRU:Least Recently Used,表明緩存不會無限制增長,一段時間不用的緩存條目會被扔掉。
    • 注意,必須像正常函數哪一行調用

      lru_cache

      @functools.lru_cache()

      ,這是因為

      lru_cache

      可以接受配置參數。
    • 除了優化遞歸算法之外,

      lru_cache

      在從Web中擷取資訊的應用中也能發揮巨大作用。
  7. functools.lru_cache(maxsize=128, typed=False)

    • maxsize 參數指定存儲多少個調用的結果。緩存滿了之後,舊的結果會被扔掉,騰出空間。為了得到最佳性能,maxsize 應該設為 2 的幂。
    • typed 參數如果設為 True,把不同參數類型得到的結果分開儲存,即把通常認為相等的浮點數和整數參數(如 1 和 1.0)區分開。
    • lru_cache 使用字典存儲結果,而且鍵根據調用時傳入的定位參數和關鍵字參數建立,是以被 lru_cache 裝飾的函數,它的所有參數都必須是可散列的。
  8. 單分派泛函數
    1. 因為 Python 不支援重載方法或函數,是以我們不能使用不同的簽名定義htmlize 的變體,也無法使用不同的方式處理不同的資料類型。
    2. 在Python 中,一種常見的做法是把 htmlize 變成一個分派函數,使用一串 if/elif/elif,調用專門的函數,如htmlize_str、htmlize_int,等等。這樣不便于子產品的使用者擴充,還顯得笨拙:時間一長,分派函數 htmlize 會變得很大,而且它與各個專門函數之間的耦合也很緊密。
    3. functools.singledispatch

      裝飾器可以把整體方案拆分成多個子產品,甚至可以為你無法修改的類提供專門的函數。
    4. 使用

      @singledispatch

      裝飾的普通函數會變成泛函數(generic function):根據第一個參數的類型,以不同方式執行相同操作的一組函數。
      這才稱得上是單分派。如果根據多個參數選擇專門的函數,那就是多分派了。
    5. singledispatch 建立一個自定義的htmlize.register 裝飾器,把多個函數綁在一起組成一個泛函數
      from functools import singledispatch
      from collections import abc
      import numbers
      import html
      
      @singledispatch
      def htmlize(obj):
          content = html.escape(repr(obj))
          return '<pre>{}</pre>'.format(content)
      
      @htmlize.register(str)
      def _(text):
          content = html.escape(text).replace('\n', '<br>\n')
          return '<p>{0}</p>'.format(content)
      
      @htmlize.register(numbers.Integral)
      def _(n):
          return '<pre>{0} (0x{0:x})</pre>'
      
      @htmlize.register(tuple)
      @htmlize.register(abc.MutableSequence)
      def _(seq):
          inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
          return '<ul>\n<li>' + inner + '</li>\n</ul>'
                 
    6. numbers.Integral

      int

      的虛拟超類
    7. 可以疊放多個

      register

      裝飾器,讓同一個函數支援不同類型
    8. 隻要可能,注冊的專門函數應該處理抽象基類(如 numbers.Integral和 abc.MutableSequence),不要處理具體實作(如 int 和 list)。這樣,代碼支援的相容類型更廣泛。
    9. 使用抽象基類檢查類型,可以讓代碼支援這些抽象基類現有和未來的具體子類或虛拟子類
    10. @singledispatch

      不是為了把 Java 的那種方法重載帶入Python
    11. 在一個類中為同一個方法定義多個重載變體,比在一個函數中使用一長串 if/elif/elif/elif 塊要更好。
    12. 但是這兩種方案都有缺陷,因為它們讓代碼單元(類或函數)承擔的職責太多。
    13. @singledispath

      的優點是支援子產品化擴充:各個子產品可以為它支援的各個類型注冊一個專門函數。
    14. 裝飾器是函數,是以可以組合起來使用(即,可以在已經被裝飾的函數上應用裝飾器)
  9. 疊放裝飾器

    下述代碼等價

    @d1
    @d2
    def f():
    	print('f')
    	
    f = d1(d2(f))
               
  10. 參數化裝飾器
    registry = set()
    def register(active=True):
        def decorate(func):
            print('running register(active=%s )->decorate(%s)' % (active, func))
            if active:
                registry.add(func)
            else:
                registry.discard(func)
            
            return func
        return decorate
    
    @register(active=False)
    def f1():
        print('running f1()')
        
    @register()
    def f2():
        print('running f2()')
        
    def f3():
        print('running f3()')
               
    1. 這裡decorate時裝飾器,必須傳回一個函數
    2. register是裝飾器工廠函數,是以傳回的是一個裝飾器decorate
    3. 即使不傳入參數,register也必須作為函數調用:

      @register()

    4. 如果不使用 @ 句法,那就要像正常函數那樣使用 register;若想把 f 添加到 registry 中,則裝飾 f 函數的句法是

      register()(f)

      ;不想添加(或把它删除)的話,句法是

      register(active=False)(f)

  11. 參數化clock裝飾器
    import time
    
    DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
    
    def clock(fmt=DEFAULT_FMT):
        def decorate(func):
            def clocked(*_args):
                t0 = time.time()
                _result = func(*_args)
                elapsed = time.time() - t0
                name = func.__name__
                args = ", ".join(repr(arg) for arg in _args)
                result = repr(_result)
                print(fmt.format(**locals()))
                return _result
            return clocked
        return decorate
    
    if __name__ == '__main__':
        @clock()
        def snooze(seconds):
            time.sleep(seconds)
        
        for i in range(3):
            snooze(.123)
               
    [0.12328148s] snooze(0.123) -> None
    [0.12369442s] snooze(0.123) -> None
    [0.12362432s] snooze(0.123) -> None
               
    1. clock:參數化裝飾器工廠函數
    2. decorate:真正的裝飾器
    3. clocked:包裝被裝飾的函數
    4. _result

      :被裝飾函數傳回的真正結果
    5. _args

      :clocked的參數,args用于顯示的字元串
    6. result:

      _result

      的字元串表現形式,用于顯示
  12. 裝飾器的參數
    @clock('{name}: {elapsed}s')
    def snooze(seconds):
        time.sleep(seconds)
    
    for i in range(3):
        snooze(.123)
               
    snooze: 0.12300252914428711s
    snooze: 0.12314867973327637s
    snooze: 0.1238546371459961s
               
    @clock('{name}({args}) dt={elapsed:0.3f}s')
    def snooze(seconds):
        time.sleep(seconds)
    
    for i in range(3):
        snooze(.123)
               
    snooze(0.123) dt=0.124s
    snooze(0.123) dt=0.123s
    snooze(0.123) dt=0.123s
               

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

8.1 變量不是盒子

  1. 如果把變量想象為盒子,那麼無法解釋 Python 中的指派;應該把變量視作便利貼
  2. 對引用式變量來說,說把變量配置設定給對象更合理
  3. ==

    運算符比較兩個對象的值(對象中儲存的資料),而

    is

    比較對象的辨別
  4. 元組的相對不可變性:元組的不可變性其實是指

    tuple

    資料結構的實體内容(即儲存的引用)不可變,與引用的對象無關。

    元組裡面有一個list,這個list就可以append,但是id還是不變。

    這也是有些元組不可散列的原因。

  5. str、bytes 和 array.array 等單一類型序列是扁平的,它們儲存的不是引用,而是在連續的記憶體中儲存資料本身(字元、位元組和數字)。
  6. 淺複制(copy.copy)和深複制(copy.deepcopy)的差別:深複制中副本不共享内部對象的引用

8.2 函數的參數作為引用

  1. 不要使用可變類型作為參數的預設值
    • 不可以預設值為類似于

      []

      ,因為如果不傳參的話,這個

      []

      就是類的内部變量,多個執行個體會共用這個變量
    • 這也是為什麼通常使用 None 作為接收可變值的參數的預設值的原因。
  2. 防禦可變參數
    • 類中的操作可能會修改傳入類的可變參數
    • 修正的方法:初始化時,把參數值的副本指派給成員變量

8.3 del、垃圾回收和弱引用

注意,這一章節的一些代碼要在cmd中才能實作,jupyterlab中的實驗結果與書中給的結果不一緻。
  1. del 語句删除名稱,而不是對象
  2. del 指令可能會導緻對象被當作垃圾回收,但是僅當删除的變量儲存的是對象的最後一個引用,或者無法得到對象時。
  3. 重新綁定也可能會導緻對象的引用數量歸零,導緻對象被銷毀。
  4. 弱引用:正是因為有引用,對象才會在記憶體中存在。當對象的引用數量歸零後,垃圾回收程式會把對象銷毀。但是,有時需要引用對象,而不讓對象存在的時間超過所需時間。這經常用在緩存中。
    1. 弱引用不會增加對象的引用數量。引用的目标對象稱為所指對象(referent)。是以我們說,弱引用不會妨礙所指對象被當作垃圾回收。
    2. 弱引用在緩存應用中很有用,因為我們不想僅因為被緩存引用着而始終儲存緩存對象。
  5. weakref.ref 類其實是低層接口,供進階用途使用,多數程式最好使用 weakref 集合和 finalize。也就是說,應該使用

    WeakKeyDictionary

    WeakValueDictionary

    WeakSet

    finalize

    (在内部使用弱引用),不要自己動手建立并處理 weakref.ref 執行個體。
  6. WeakValueDictionary
    • WeakValueDictionary 類實作的是一種可變映射,裡面的值是對象的弱引用。
    • 被引用的對象在程式中的其他地方被當作垃圾回收後,對應的鍵會自動從 WeakValueDictionary 中删除。
    • 是以,WeakValueDictionary 經常用于緩存。
  7. 與 WeakValueDictionary 對應的是 WeakKeyDictionary,後者的鍵是弱引用
  8. WeakSet 類:儲存元素弱引用的集合類。元素沒有強引用時,集合會把它删除
    • 如果一個類需要知道所有執行個體,一種好的方案是建立一個WeakSet 類型的類屬性,儲存執行個體的引用。
    • 如果使用正常的 set,執行個體永遠不會被垃圾回收,因為類中有執行個體的強引用,而類存在的時間與 Python 程序一樣長,除非顯式删除類。
  9. 弱引用的局限
    • 不是每個 Python 對象都可以作為弱引用的目标(或稱所指對象)。基本的 list 和 dict 執行個體不能作為所指對象,但是它們的子類可以
    • set 執行個體可以作為所指對象
    • 使用者定義的類型也沒問題
    • 但是,int 和 tuple 執行個體不能作為弱引用的目标,甚至它們的子類也不行
    • 這些局限基本上是 CPython 的實作細節,在其他 Python 解釋器中情況可能不一樣。這些局限是内部優化導緻的結果
  10. Python對不可變類型施加的把戲
    1. 對元組 t 來說,

      t[:]

      不建立副本,而是傳回同一個對象的引用。此外,

      tuple(t)

      獲得的也是同一個元組的引用。(str、bytes 和 frozenset 執行個體也有這種行為)
      • frozenset 執行個體不是序列,是以不能使用

        fs[:]

        (fs 是一個 frozenset 執行個體),但是,

        fs.copy()

        具有相同的效果:傳回同一個對象的引用,而不是建立一個副本
    2. 字元串字面量可能會建立共享的對象
      s1 = 'ABC'
      s2 = 'ABC'
      s2 is s1
      
      # True
                 
      • 共享字元串字面量是一種優化措施,稱為駐留(interning)。CPython 還會在小的整數上使用這個優化措施,防止重複建立“熱門”數字
      • CPython 不會駐留所有字元串和整數
  11. 雜談
    1. java的

      ==

      運算符比較的是對象(不是基本類型)的引用,而不是對象的值。否則的話要用到

      .equals

      方法,如果調用方法的變量為

      null

      ,會得到一個 空指針異常
    2. 在python中,

      ==

      比較對象的值,

      is

      比較引用
    3. 最重要的是,python支援重載運算發,

      ==

      能正确處理标準庫中的所有對象,包括

      None

      ,與java的null不同

9. 符合Python風格的對象

9.1 對象的表示形式

  1. 擷取對象的字元串表示形式的标準方式
    1. repr()

      :開發者了解的方式傳回對象的字元串表示形式
    2. str()

      :使用者了解的方式傳回對象的字元串表示形式
  2. 在 Python 3 中,

    __repr__

    __str__

    __format__

    都必須傳回 Unicode 字元串(str 類型)。隻有

    __bytes__

    方法應該傳回位元組序列(bytes 類型)
  3. 定義

    __iter__

    方法,把類的執行個體程式設計可疊代的對象,這樣才能拆包。

    x, y = my_vector

    這一行也可以寫成

    yield self.x; yield self.y

  4. eval()

    函數用來執行一個字元串表達式,并傳回表達式的值。
  5. classmethod:第一個參數是類本身,最常見的用途是定義備選構造方法
  6. staticmethod:第一個參數不是特殊的值,其實靜态方法就是普通的函數,隻是碰巧在類的定義體中,而不是在子產品層定義
  7. 作者對staticmethod的态度是:”不是特别有用“,因為如果想定義不需要與類進行互動的函數,隻需要在子產品中定義就好了。

9.2 格式化顯示

  1. 内置的

    format()

    函數和

    str.format()

    方法把各個類型的格式化方式委托給相應的

    .__format__(format_spec)

    方法
    • format(my_obj, format_spec)

      裡的第二個參數
    • str.format()

      方法的格式字元串,

      {}

      裡代換字段中冒号後面的部分
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
  2. {0.mass:5.3e}

    這樣的格式字元串其實包含兩部分
    • 冒号左邊的

      .mass

      在代換字段句法中是字段名
    • 冒号後面的

      5.3e

      是格式說明符
    • 格式說明符使用的表示法叫 格式規範微語言 ,Format Specification Mini-Language
  3. 格式規範微語言為一些内置類型提供了專用的表示代碼
    1. b 和 x 分别表示二進制和十六進制的 int 類型
    2. f 表示小數形式的 float 類型
    3. % 表示百分數形式
  4. 格式規範微語言是可擴充的,因為各個類可以自行決定如何解釋format_spec 參數
  5. 如果類沒有定義

    __format__

    方法,從 object 繼承的方法會傳回

    str(my_object)

  6. 在格式規範微語言中,整數使用的代碼有

    bcdoxXn

    ,浮點數使用的代碼有

    eEfFgGn%

    ,字元串使用的代碼有

    s

  7. 使用兩個前導下劃線(尾部沒有下劃線,或者有一個下劃線),把屬性标記為私有的
  8. @property

    裝飾器把讀值方法标記為屬性
  9. 要想建立可散列的類型,不一定要實作特性,也不一定要保護執行個體屬性。隻需正确地實作

    __hash__

    __eq__

    方法即可
  10. 如果定義的類型有标量數值,可能還要實作

    __int__

    __float__

    方法(分别被

    int()

    float()

    構造函數調用),以便在某些情況下用于強制轉換類型。此外,還有用于支援内置的

    complex()

    構造函數的

    __complex__

    方法
    from array import array
    import math
    
    class Vector2d:
        typecode = 'd'
        
        def __init__(self, x, y):
            self.__x = float(x)
            self.__y = float(y)
        
        @property
        def x(self):
            return self.__x
        
        @property
        def y(self):
            return self.__y
            
        def __iter__(self):
            return (i for i in (self.x, self.y))
            
        def __repr__(self):
            class_name = type(self).__name__
            return '{}({!r}, {!r})'.format(class_name, *self)
        
        def __str__(self):
            return str(tuple(self))
        
        def __bytes__(self):
            return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))
        
        def __eq__(self, other):
            return tuple(self) == tuple(other)
        
        def __abs__(self):
            return math.hypot(self.x, self.y)
        
        def __bool__(self):
            return bool(abs(self))
        
        @classmethod
        def frombytes(cls, octets):
            typecode = chr(octets[0])
            memv = memoryview(octets[1:]).cast(typecode)
            return cls(*memv)
        
        def angle(self):
            return math.atan2(self.y, self.x)
        
        def __hash__(self):
            return hash(self.x) ^ hash(self.y)
        
        def __format__(self, fmt_spec=''):
            if fmt_spec.endswith('p'):
                fmt_spec = fmt_spec[: -1]
                coords = (abs(self), self.angle())
                outer_fmt = '<{}, {}>'
            else:
                coords = self
                outer_fmt = '({}, {})'
                components = (format(c, fmt_spec) for c in coords)
                return outer_fmt.format(*components)
               

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

  1. 私有屬性需要在前面加兩根下劃線,Python 會把屬性名存入執行個體的

    __dict__

    屬性中,而且會在前面加上一個下劃線和類名
    • 父類:_Dog__mood
    • 子類:_Beagle__mood
    • 這個語言特性叫 名稱改寫 (name mangling)
  2. Python的私有屬性是可以被修改的,通過上述名稱修改
  3. 受保護屬性:
    • Python 解釋器不會對使用單個下劃線的屬性名做特殊處理,不過這是很多 Python 程式員嚴格遵守的約定,他們不會在類外部通路這種屬性。
    • 不過在子產品中,頂層名稱使用一個前導下劃線的話,的确會有影響:對

      from mymod import *

      來說,mymod 中字首為下劃線的名稱不會被導入。然而,依舊可以使用

      from mymod import _privatefunc

      将其導入。
  4. 使用

    __slots__

    類屬性節省空間
    • 為了使用底層的散清單提升通路速度,字典會消耗大量記憶體。如果要處理數百萬個屬性不多的執行個體,通過

      __slots__

      類屬性,能節省大量記憶體,方法是讓解釋器在元組中存儲執行個體屬性,而不用字典。
    • 定義

      __slots__

      的方式是,建立一個類屬性,使用

      __slots__

      這個名字,并把它的值設為一個字元串構成的可疊代對象,其中各個元素表示各個執行個體屬性。
    • 在類中定義

      __slots__

      屬性的目的是告訴解釋器:“這個類中的所有執行個體屬性都在這兒了!”這樣,Python 會在各個執行個體中使用類似元組的結構存儲執行個體變量,進而避免使用消耗記憶體的

      __dict__

      屬性。如果有數百萬個執行個體同時活動,這樣做能節省大量記憶體。
    • 如果類中定義了

      __slots__

      屬性,而且想把執行個體作為弱引用的目标,那麼要把

      '__weakref__'

      添加到

      __slots__

      中。
  5. 如果使用得當,

    __slots__

    能顯著節省記憶體
    • 每個子類都要定義

      __slots__

      屬性,因為解釋器會忽略繼承的

      __slots__

      屬性
    • 執行個體隻能擁有

      __slots__

      中列出的屬性,除非把

      '__dict__'

      加入

      __slots__

      中(這樣做就失去了節省記憶體的功效)
    • 如果不把

      '__weakref__'

      加入

      __slots__

      ,執行個體就不能作為弱引用的目标
  6. 覆寫類屬性
    • 類屬性可用于為執行個體屬性提供預設值
    • 如果為不存在的執行個體屬性指派,會建立執行個體屬性
    • 假如我們為

      typecode

      執行個體屬性指派,那麼同名 類屬性 不受影響
    • 然而,自此之後,執行個體讀取的

      self.typecode

      是執行個體屬性

      typecode

      ,也就是把同名類屬性遮蓋了
    • Python風格的修改方法:
      • 類屬性是公開的,是以會被子類繼承
      • 于是經常會建立一個子類,隻用于定制類的資料屬性
  7. 總結:
    1. 所有用于擷取字元串和位元組序清單示形式的方法:

      __repr__

      __str__

      __format__

      __bytes__

    2. 把對象轉換成數字的幾個方法:

      __abs__

      __bool__

      __hash__

    3. 用于測試位元組序列轉換和支援散列(連同

      __hash__

      方法)的

      __eq__

      運算符。

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

  1. reprlib.repr

    :展示變量的程式員能明白的語句
  2. 協定和鴨子類型
    1. 在 Python 中建立功能完善的序列類型無需使用繼承,隻需實作符合序列協定的方法
    2. 在面向對象程式設計中,協定是非正式的接口,隻在文檔中定義,在代碼中不定義
    3. 例如,Python 的序列協定隻需要

      __len__

      __getitem__

      兩個方法
    4. 協定是非正式的,沒有強制力,是以如果你知道類的具體使用場景,通常隻需要實作一個協定的部分。
  3. slice.indices

    :indices 方法開放了内置序列實作的棘手邏輯,用于優雅地處理缺失索引和負數索引,以及長度超過目标序列的切片。這個方法會“整頓”元組,把 start、stop 和 stride 都變成非負數,而且都落在指定長度序列的邊界内。
  4. __getattr__()

    :對

    my_obj.x

    表達式,Python 會檢查

    my_obj

    執行個體有沒有名為

    x

    的屬性;如果沒有,到類(

    my_obj.__class__

    )中查找;如果還沒有,順着繼承樹繼續查找。如果依舊找不到,調用

    my_obj

    所屬類中定義的

    __getattr__

    方法,傳入

    self

    和屬性名稱的字元串形式(如 ‘x’)
  5. 不建議隻為了避免建立執行個體屬性而使用

    __slots__

    屬性。

    __slots__

    屬性隻應該用于節省記憶體,而且僅當記憶體嚴重不足時才應該這麼做。
  6. 歸約函數(reduce、sum、any、all)把序列或有限的可疊代對象變成一個聚合結果
  7. functools.reduce()

    的關鍵思想是,把一系列值歸約成單個值。

    reduce()

    函數的第一個參數是接受兩個參數的函數,第二個參數是一個可疊代的對象。
    import functools
    functools.reduce(lambda a, b: a * b, range(1, 6))
    # 120
               
  8. reduce(function, iterable, initializer)

    :使用的時候最好提供第三個參數,這樣能避免異常。如果序列為空,

    initializer

    是傳回的結果;否則,在歸約中使用它作為第一個參數,是以應該使用恒等值。比如,對 +、| 和 ^ 來說,

    initializer

    應該是 0;而對 * 和 & 來說,應該是 1。
  9. zip

    :使用 zip 函數能輕松地并行疊代兩個或更多可疊代對象,它傳回的元組可以拆包成變量,分别對應各個并行輸入中的一個元素。
    • zip 有個奇怪的特性:當一個可疊代對象耗盡後,它不發出警告就停止
    • itertools.zip_longest

      函數的行為有所不同:使用可選的

      fillvalue

      (預設值為 None)填充缺失的值,是以可以繼續産出,直到最長的可疊代對象耗盡
    from array import array
    import reprlib
    import math
    import numbers
    import functools
    import operator
    import itertools
    
    class Vector:
        typecode = 'd'
        
        def __init__(self, components):
            self._components = array(self.typecode, components)
            
        def __iter__(self):
            return iter(self._components)
        
        def __repr__(self):
            components = reprlib.repr(self._components)
            components = components[components.find('['):-1]
            return 'Vector({})'.format(components)
        
        def __str__(self):
            return str(tuple(self))
        
        def __bytes__(self):
            return (bytes([ord(self.typecode)]) + bytes(self._components))
        
        def __eq__(self, other):
            return tuple(self) == tuple(other)
        
        def __abs__(self):
            return math.sqrt(sum(x * x for x in self))
        
        def __bool__(self):
            return bool(abs(self))
        
        def __len__(self):
            return len(self._components)
        
    #     def __getitem__(self, index):
    #         return self._components[index]
    
        def __getitem__(self, index):
            cls = type(self)
            # 如果傳入的參數是切片,就傳回改切片生成的Vector類
            if isinstance(index, slice):
                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:
                pos = cls.shortcut_names.find(name)
                if 0 <= pos < len(self._components):
                    return self._components[pos]
            
            msg = '{.__name__!r} object has no attribute {!r}'
            raise AttributeError(msg.format(cls, name))
            
        def __setattr__(self, name, value):
            cls = type(self)
            if len(name) == 1:
                if name in cls.shortcut_names:
                    error = 'readonly attribute {attr_name!r}'
                elif name.islower():
                    error = "can't set attributes 'a' to 'z' in {cls_name!r}"
                else:
                    error = ''
                
                if error:
                    msg = error.format(cls_name=cls.__name__, attr_name=name)
                    raise AttributeError(msg)
            super().__setattr__(name, value)
            
        def __eq__(self, other):
    #         return tuple(self) == tuple(other)
            # 為了提高比較的效率,使用zip函數
    #         if len(self) != len(other):
    #             return False      
    #         for a, b in zip(self, other):
    #             if a != b:
    #                 return False
    #         return True
            
            # 過用于計算聚合值的整個 for 循環可以替換成一行 all 函數調用:
            # 如果所有分量對的比較結果都是 True,那麼結果就是 True。
            return len(self) == len(other) and all(a == b for a, b in zip(self, other))
        
        def __hash__(self):
            # 建立一個生成器表達式,惰性計算各個分量的散列值
            # hashes = (hash(x) for x in self._components)
            # 這裡換成map方法
            hashes = map(hash, self._components)
            
            # 把hashes提供給reduce函數,第三個參數0是初始值
            return functools.reduce(operator.xor, hashes, 0)
        
        def angle(self, n):
            r = math.sqrt(sum(x * x for x in self[n:]))
            a = math.atan2(r, self[n-1])
            if (n == len(self) - 1) and (self[-1] < 0):
                return math.pi * 2 - a
            else:
                return a
            
        def angles(self):
            return (self.angle(n) for n in range(1, len(self)))
        
        def __format__(self, fmt_spec=''):
            if fmt_spec.endswith('h'):
                # 超球面坐标
                fmt_spec = fmt_spec[:-1]
                coords = itertools.chain([abs(self)], self.angles())
                outer_fmt = '<{}>'
            else:
                coords = self
                outer_fmt = '({})'
            components = (format(c, fmt_spec) for c in coords)
            return outer_fmt.format(', '.join(components))
                
        @classmethod
        def frombytes(cls, octets):
            typecode = chr(octets[0])
            memv = memoryview(octets[1:]).cast(typecode)
            return cls(memv)
               

11. 接口:從協定到抽象基類

鴨子類型:“當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”

程式設計中,鴨子類型(英語:duck typing)是 動态類型 的一種風格。在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實作特定的接口,而是由"目前方法" (計算機科學)和屬性的集合決定。

在鴨子類型中,關注點在于對象的行為,能做什麼;而不是關注對象所屬的類型。

  1. Python文化中的接口和協定
    1. 受保護的屬性和私有屬性不在接口中
    2. 接口 的補充定義:對象公開方法的子集,讓對象在系統中扮演特定的角色
    3. 協定與繼承沒有關系。一個類可能會實作多個接口,進而讓執行個體扮演多個角色。
    4. 協定是接口,但不是正式的,是以協定不能像正式接口那樣施加限制
    5. 一個類可能隻實作部分接口,這是允許的
    6. 對 Python 程式員來說,“X 類對象”、“X 協定”和“X 接口”都是一個意思
    7. 序列協定是 Python 最基礎的協定之一。即便對象隻實作了那個協定最基本的一部分,解釋器也會負責任地處理。
  2. Python喜歡序列
    1. 定義為抽象基類的Sequence正式接口
    2. 定義

      __getitem__

      方法,隻實作序列協定的一部分,這樣足夠通路元素、疊代和使用 in 運算符了
      class Foo:
          def __getitem__(self, pos):
              return range(0, 30, 10)[pos]
                 
      • 雖然沒有

        __iter__

        方法,但是 Foo 執行個體是可疊代的對象,因為發現有

        __getitem__

        方法時,Python 會調用它,傳入從 0 開始的整數索引,嘗試疊代對象
      • 鑒于序列協定的重要性,如果沒有

        __iter__

        __contains__

        方法,Python 會調用

        __getitem__

        方法,設法讓疊代和 in 運算符可用。
    3. shuffle 函數要調換集合中元素的位置,隻實作了

      __getitem__

      即是隻實作了不可變的序列協定,可變的序列還必須提供

      __setitem__

      方法。
    4. 猴子更新檔:在運作時修改類或子產品,而不改動源碼
    5. 協定是動态的:

      random.shuffle

      函數不關心參數的類型,隻要那個對象實作了部分可變序列協定即可。即便對象一開始沒有所需的方法也沒關系,後來再提供也行。
    6. “鴨子類型”:對象的類型無關緊要,隻要實作了特定的協定即可。
  3. Alex Martelli的水禽
    1. 用isinstance檢查對象的類型,而不是

      type(foo) is bar

    2. 白鵝類型,goose typing,指隻要

      cls

      是抽象基類,即

      cls

      的元類是

      abc.ABCMeta

      ,就可以使用

      isinstance(obj, cls)

    3. Python 的抽象基類還有一個重要的實用優勢:可以使用

      register

      類方法在終端使用者的代碼中把某個類“聲明”為一個抽象基類的“虛拟”子類
    4. 無需注冊,

      abc.Sized

      也能把Struggle識别為自己的子類,隻要實作了特殊方法

      __len__

      即可。要使用正确的句法和語義實作,前者要求沒有參數,後者要求傳回一個非負整數,指明對象的長度。如果不使用規範的文法和語義實作特殊方法,如

      __len__

      ,會導緻非常嚴重的問題
      class Struggle:
          def __len__(self):
              return 23
      
      from collections import abc
      isinstance(Struggle(), abc.Sized)
                 
    5. 在 Python 3.4 中沒有能把字元串和元組或其他不可變序列區分開的抽象基類,是以必須測試 str:

      isinstance(x, str)

    6. EAFP和LBYL
      1. EAFP:Easier to Ask for Forgiveness than Permission,請求寬恕比許可更容易
        • 操作前不檢查,出了問題由異常處理來處理
        • 代碼表現:try…except…
        try:
            x = test_dict["key"]
        except KeyError:
            # key 不存在
                   
      2. LBYL:Look Before You Leap,三思而後行
        • 操作前先檢查,再執行
        • 代碼表現:if…else…
        if "key" in test_dict:
            x = test_dict["key"]
        else:
            # key 不存在
                   
      3. EAFP 的異常處理往往也會影響一點性能,因為在發生異常的時候,程式會進行保留現場、回溯traceback等操作,但在異常發生頻率比較低的情況下,性能相差的并不是很大。
      4. 而 LBYL 則會消耗更高的固定成本,因為無論成敗與否,總是執行額外的檢查。
      5. 相比之下,如果不引發異常,EAFP 更優一些,
      6. Python 的動态類型(duck typing)決定了 EAFP,而 Java等強類型(strong typing)決定了 LBYL
    7. 定義抽象基類的子類
      1. 導入時,Python不會檢查抽象方法的實作,在運作時執行個體化類的時候才會真正檢查。是以,如果沒有正确實作某個抽象方法,Python會抛出TypeError異常。
      2. MutableSequence 抽象基類和 collections.abc 中它的超類的UML類圖(箭頭由子類指向祖先;以斜體顯示的名稱是抽象類和抽象方法)
        《流暢的python》學習筆記及書評《流暢的python》學習筆記
    8. 标準庫中的抽象基類
      1. collections.abc

        子產品中各個抽象基類的 UML 類圖
        《流暢的python》學習筆記及書評《流暢的python》學習筆記
      2. Iterable、Container 和 Sized
        • 各個集合應該繼承這三個抽象基類,或者至少實作相容的協定。
        • Iterable 通過

          __iter__

          方法支援疊代
        • Container 通過

          __contains__

          方法支援 in 運算符
        • Sized 通過

          __len__

          方法支援

          len()

          函數
      3. Sequence、Mapping 和 Set
        • 這三個是主要的不可變集合類型,而且各自都有可變的子類
      4. MappingView
        • 映射方法 .items()、.keys() 和 .values() 傳回的對象分别是 ItemsView、KeysView 和 ValuesView 的執行個體
      5. Callable 和 Hashable
        • 這兩個抽象基類與集合沒有太大的關系,隻不過因為

          collections.abc

          是标準庫中定義抽象基類的第一個子產品,而它們又太重要了,是以才把它們放到

          collections.abc

          子產品中
        • 這兩個抽象基類的主要作用是為内置函數

          isinstance

          提供支援,以一種安全的方式判斷對象能不能調用或散列
        • 若想檢查是否能調用,可以使用内置的 callable() 函數;但是沒有類似的 hashable() 函數,是以測試對象是否可散列,最好使用

          isinstance(my_obj, Hashable)

      6. Iterator
        • Iterable 的子類
    9. 抽象基類的金字塔
      1. numbers包定義的是 數字塔
        • Number
        • Complex
        • Real
        • Rational
        • Integral
      2. 檢查一個數是不是整數:

        isinstance(x, numbers.Integral)

        ,這樣代碼就可以接受int、bool
      3. 檢查一個數是不是浮點數:

        isinstance(x, number.Real)

        ,這樣代碼就可以接受bool、int、float、fractions.Fraction,還有外部庫如Numpy
      4. deciaml.Decimal沒有注冊為numbers.Real的虛拟子類,是因為,如果你的程式需要Decimal的精度,要防止與其他低精度數字類型混合,尤其是浮點數
    10. 定義并使用一個抽象基類
      1. 抽象方法示例:
        import abc
        
        class Tombola(abc.ABC):
            @abc.abstractmethod
            def load(self, iterable):
                """從可疊代對象中添加元素"""
            
            @abc.abstractmethod
            def pick(self):
                """随機删除元素,然後将其傳回
                如果執行個體為空,這個方法應該抛出 LookupError
                """
            
            def loaded(self):
                """如果至少有一個元素,傳回True,否則傳回False"""
                return bool(self.inspect())
            
            def inspect(self):
                """傳回一個有序元組,由目前元素構成"""
                items = []
                while True:
                    try:
                        items.append(self.pick())
                    except LookupError:
                        break
                self.load(items)
                return tuple(sorted(items))
                   
      2. 抽象方法可以有實作代碼。即便實作了,子類也必須覆寫抽象方法,但是在子類中可以使用 super() 函數調用抽象方法,為它添加功能,而不是從頭開始實作
      3. IndexEror和KeyError是LookupError的子類
        1. IndexError:嘗試從序列中擷取索引超過最後位置的元素時抛出
        2. KeyError:使用不存在的鍵從映射中擷取元素時,抛出 KeyError 異常
    11. 抽象基類文法詳解
      1. 可以通過裝飾器堆疊的方式,聲明抽象類方法、抽象靜态方法、抽象屬性等。
        class MyABC(abc.ABC):
            @classmethod
            @abc.abstractmethod
            def an_abstract_classmethod(cls, ...):
                pass
                   
      2. 與其他方法描述符一起使用時,

        abstractmethod()

        應該放在 最裡層
      3. 唯一推薦使用的抽象基類方法裝飾器是@abstractmethod,其他裝飾器已經廢棄了
    12. 定義Tombola抽象基類的子類
      1. 就算iterable參數始終傳入清單,

        list(iterable)

        會建立參數的副本,這依然是好的做法
      2. 白鵝類型 的重要動态特性:使用register方法聲明虛拟子類
      3. Python中的鴨子類型和白鵝類型
        鴨子類型與繼承毫無關系。
        • 鴨子類型的定義是:

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

        • 言簡意赅的了解是:

          “對象的類型無關緊要,隻要實作了特定的協定即可。忽略對象真正的類型,轉而關注對象有沒有實作所需的方法、簽名和語義。”

        • 最直接的結果就是:
          • 一個使用者定義的類型,不需要真正的繼承自抽象基類,但是卻可以當作其子類一樣來使用。
          • 比如使用者實作了序列協定,就可以當作内置序列類型來用,對其使用

            len()

            等函數,調用

            __len__()

            等用于内置類型的方法。
          • 比如使用者實作了

            __getitem__

            方法,Python就可以直接去疊代這個類型。
          • Python内置庫和第三方的庫,雖然是針對Python的類型設計的,但是都可以直接用于使用者自定義的類型上。
        而白鵝類型與此恰好相反。
        • 白鵝類型的定義是:

          使用抽象基類明确聲明接口,子類顯示地繼承抽象基類,抽象基類會檢查子類是否符合接口定義。

        • 這樣做的劣勢是:

          子類為了經過抽象基類的接口檢查,必須實作一些接口,但是這些接口你可能用不到。

        • 這樣做的優勢是:

          一些直接繼承自抽象基類的接口是可以拿來即用的。

    13. Tombola的虛拟子類
      @Tombola.register
      class TomboList(list):
          def pick(self):
              if self:
                  position = randrange(len(self))
                  return self.pop(position)
              else:
                  raise LookupError('pop from empty TomboList')
              
          load = list.extend
          
          def loaded(self):
              return bool(self)
          
          def inspect(self):
              return tuple(sorted(self))
          
      # Tombola.register(TomTomboList)
                 
      1. 白鵝類型的一個基本特性:即便不繼承,也有辦法把一個類注冊為抽象基類的虛拟子類。
      2. 注冊虛拟子類的方式是在抽象基類上調用 register 方法
      3. 這麼做之後,注冊的類會變成抽象基類的虛拟子類,而且 issubclass 和isinstance 等函數都能識别
      4. 但是注冊的類不會從抽象基類中繼承任何方法或屬性
      5. 虛拟子類不會繼承注冊的抽象基類,而且任何時候都不會檢查它是否符合抽象基類的接口,即便在執行個體化時也不會檢查。為了避免運作時錯誤,虛拟子類要實作所需的全部方法。
      6. load = list.extend

        ,load跟list的extend一樣。
    14. 類的繼承關系在一個特殊的類屬性中指定——

      __mro__

      ,即方法解析順序(Method Resolution Order)
      1. 這個屬性的作用很簡單,按順序列出類及其超類,Python 會按照這個順序搜尋方法
      2. 它隻列出了“真實的”超類,利用

        @類名.register

        內建的不在其中,是以也沒有從中繼承任何方法
    15. Tombola子類的測試方法
      1. __subclasses__()

        :這個方法傳回類的直接子類清單,不含虛拟子類
      2. _abc_registry

        :隻有抽象基類有這個資料屬性,其值是一個 WeakSet 對象,即抽象類注冊的虛拟子類的弱引用
    16. 問題:報錯沒有找到

      _abc_registry

      • 這是因為,python3.7版本沒有,需要用3.4才行,參考連結
      • 并且在3.7的版本中是不能擷取到這個屬性的
    17. Python使用register的方式
      • 雖然現在可以把 register 當作裝飾器使用了,但更常見的做法還是把它當作函數使用,用于注冊其他地方定義的類
    18. 鵝的行為有可能像鴨子
      • 即便不注冊,抽象基類也能把一個類識别為虛拟子類
      • 比如一個類實作了

        __len__()

        方法:
        class Struggle:
            def __len__(self):
                return 23
                   
      • 然後可以看到這個類是Sized抽象基類的虛拟子類
        from collections import abc
        
        isinstance(Struggle(), abc.Sized)
        # True
        issubclass(Struggle, abc.Sized)
        # True
                   
      • 這是因為

        abc.Sized

        實作了一個特殊的類方法:

        __sunclasshook__

    19. __subclasshook__

      在白鵝類型中添加了一些鴨子類型的蹤迹。
      • 我們可以使用抽象基類定義正式接口,可以始終使用 isinstance 檢查
      • 也可以完全使用不相關的類,隻要實作特定的方法即可(或者做些事情讓

        __subclasshook__

        信服)
      • 當然,隻有提供

        __subclasshook__

        方法的抽象基類才能這麼做。
    20. 在你我自己編寫的抽象基類中實作

      __subclasshook__

      方法,可靠性很低
      • 程式員最好讓 Spam 繼承 Tombola,至少也要注冊(Tombola.register(Spam))
      • 自己實作的

        __subclasshook__

        方法還可以檢查方法簽名和其他特性,但我覺得不值得這麼做
    21. 強類型和弱類型
      1. 如果一門語言很少隐式轉換類型,說明它是強類型語言;如果經常這麼做,說明它是弱類型語言
      2. Java、C++ 和 Python 是強類型語言
      3. PHP、JavaScript 和 Perl 是弱類型語言
    22. 靜态類型和動态類型
      1. 在編譯時檢查類型的語言是靜态類型語言,在運作時檢查類型的語言是動态類型語言
      2. 靜态類型需要聲明類型(有些現代語言使用類型推導避免部分類型聲明)
      3. Fortran 和 Lisp 是最早的兩門語言,現在仍在使用,它們分别是靜态類型語言和動态類型語言
    23. 小結
      1. 強類型能及早發現缺陷
      2. 靜态類型使得一些工具(編譯器和 IDE)便于分析代碼、找出錯誤和提供其他服務(優化、重構,等等)
      3. 動态類型便于代碼重用,代碼行數更少,而且能讓接口自然成為協定而不提早實行
      4. Python 是動态強類型語言
    24. 猴子更新檔
      1. 猴子更新檔的名聲不太好。如果濫用,會導緻系統難以了解和維護。更新檔通常與目标緊密耦合,是以很脆弱。另一個問題是,打了猴子更新檔的兩個庫可能互相牽絆,因為第二個庫可能撤銷了第一個庫的更新檔。
      2. 猴子更新檔也有它的作用,例如可以在運作時讓類實作協定。擴充卡設計模式通過實作全新的類解決這種問題。
      3. Python 不允許為内置類型打猴子更新檔,這一局限能減少外部庫打的更新檔有沖突的機率
    25. 不把隐喻當作設計範式,而代之以“習慣用法的界面”

12. 繼承的優缺點

  1. 子類化内置類型很麻煩
    1. 内置類型不會調用使用者定義的類覆寫的特殊方法
    2. 内置類型的方法不會調用子類覆寫的方法
    3. 直接子類化内置類型(如 dict、list 或 str)容易出錯,因為内置類型的方法通常會忽略使用者覆寫的方法
    4. 不要子類化内置類型,使用者自己定義的類應該繼承 collections 子產品中的類,例如UserDict、UserList 和 UserString,這些類做了特殊設計,是以易于擴充
    5. 如果子類化使用Python 編寫的類,如 UserDict 或 MutableMapping,就不會受此影響
  2. 多重繼承和方法解析順序
    1. “菱形問題”:不相關的祖先類實作同名方法引起的沖突
    2. Python 會按照特定的順序周遊繼承圖,這個順序叫方法解析順序(Method Resolution Order, MRO)
    3. 直接在類上調用執行個體方法時,必須顯式傳入 self 參數,因為這樣通路的是未綁定方法(unbound method)
    4. 使用 super() 最安全,也不易過時。調用架構或不受自己控制的類層次結構中的方法時,尤其适合使用 super()
    5. 使用 super() 調用方法時,會遵守方法解析順序
    6. 方法解析順序不僅考慮繼承圖,還考慮子類聲明中列出超類的順序,**先聲明的先調用你敢信?**沒事多看看

      __mro__

      屬性吧,按這個順序調用。
  3. 多重繼承的真實應用(以Tkinter為例)
    • Toplevel 是所有圖形類中唯一沒有繼承 Widget 的,因為它是頂層視窗,行為不像小元件,例如不能依附到視窗或窗體上
    • Toplevel 繼承自 Wm,後者提供直接通路宿主視窗管理器的函數,例如設定視窗标題和配置視窗邊框
    • Widget 直接繼承自 BaseWidget,還繼承了 Pack、Place 和Grid。後三個類是幾何管理器,負責在視窗或窗體中排布小元件。各個類封裝了不同的布局政策和小元件位置 API
  4. 處理多重繼承
    1. 下面是避免把類圖攪亂的一些建議
      1. 把接口繼承和實作繼承區分開
        • 繼承接口,建立子類型,實作“是什麼”關系
        • 繼承實作,通過重用避免代碼重複
      2. 使用抽象基類顯式表示接口
      3. 通過混入重用代碼
        • 如果一個類的作用是為多個不相關的子類提供方法實作,進而實作重用,但不展現“是什麼”關系,應該把那個類明确地定義為混入類(mixin class)
        • 從概念上講,混入不定義新類型,隻是打包方法,便于重用
        • 混入類絕對不能執行個體化,而且具體類不能隻繼承混

          入類

        • 混入類應該提供某方面的特定行為,隻實作少量關系非常緊密的方法
      4. 在名稱中明确指明混入

        因為在 Python 中沒有把類聲明為混入的正規方式,是以強烈推薦在名稱中加入 …Mixin 字尾

      5. 抽象基類可以作為混入,反過來則不成立
      6. 不要子類化多個具體類
        • 具體類可以沒有,或最多隻有一個具體超類
        • 具體類的超類中除了這一個具體超類之外,其餘的都是抽象基類或混入
      7. 為使用者提供聚合類
        • 如果抽象基類或混入的組合對客戶代碼非常有用,那就提供一個類,使用易于了解的方式把它們結合起來。Grady Booch 把這種類稱為聚合類(aggregate class)
        • 例如

          tkinter.Widget

          類中繼承了多個抽象基類/混入:

          class Widget(BaseWidget, Pack, Place, Grid):pass

        • Widget 類的定義體是空的,但是這個類提供了有用的服務:把四個超類結合在一起,這樣需要建立新小元件的使用者無需記住全部混入,也不用擔心聲明 class 語句時有沒有遵守特定的順序
      8. “優先使用對象組合,而不是類繼承”

        組合和委托可以代替混入,把行為提供給不同的類,但是不能取代接口繼承去定義類型層次結構

        • 依賴、關聯、聚合、組合
        • 混入,python多繼承和混入類,混入類就是多重繼承的一種實作技巧,為了防止類的指數增長,為了使具體類具有多個獨立、解耦的功能(個人想法)
    2. KISS 原則:KISS 原則是使用者體驗的高層境界,簡單地了解這句話,就是要把一個産品做得連白癡都會用,因而也被稱為“懶人原則”。
      • Keep it Simple and Stupid
    3. 内置類型:dict、list 和 str 類型
      1. 是 Python 的底層基礎,速度必須快,與這些内置類型有關的任何性能問題幾乎都會對其他所有代碼産生重大影響
      2. CPython 走了捷徑,故意讓内置類型的方法行為不當,即不調用被子類覆寫的方法
      3. 解決這一困境的可能方式之一是,為這些類型分别提供兩種實作:一種供内部使用,為解釋器做了優化;另一種供外部使用,便于擴充(UserDict、UserList 和 UserString)
    4. 多重分派:
      1. 分派就是指根據變量的類型選擇相應的方法,單分派指的是指根據第一個參數類型去選擇方法。
      2. 函數func()的結果隻跟第一個參數的類型有關,跟後面的參數沒有關系,這就是單分派。
      3. 使用函數的所有參數,而非隻用第一個,來決定調用哪個方法被稱為多重分派。

13. 正确重載運算符

  1. 運算符重載基礎
    1. Python 施加了一些限制,做好了靈活性、可用性和安全性方面的平衡:
      1. 不能重載内置類型的運算符
      2. 不能建立運算符,隻能重載現有的
      3. 某些運算符不能重載——is、and、or 和 not(不過位運算符 &、| 和 ~ 可以)
  2. 一進制運算符
    1. 常見的運算符和對應的特殊方法:
      1. -

        __neg__

        ,取負算術運算符
      2. +

        __pos__

        ,取正算術運算符
      3. ~

        __invert__

        ,按位取反
        • 定義為   x = = − ( x + 1 ) ~x == -(x+1)  x==−(x+1)
        • 如果x是2,那麼~x == -3
      4. abs()

        __abs__

        ,取絕對值
    2. x 和 +x 何時不相等
      1. jupyter和ipython中無法重制例子,但是cmd可以
        《流暢的python》學習筆記及書評《流暢的python》學習筆記
      2. Counter的例子
        1. Counter 相加時,負值和零值計數會從結果中剔除
        2. 而一進制運算符 + 等同于加上一個空 Counter,是以它産生一個新的 Counter 且僅保留大于零的計數器
        3. 什麼意思呢,就是說,如果使用Counter對序列進行計數之後,然後給部分key負值為0或者負值,然後在前面使用+算術運算符,就會導緻負值和0對應的

          key:value

          鍵值對剔除。
  3. 重載向量加法運算符+
    1. __radd__

      __rsub__

      中的r:reflected(反射),reverse(反向),兩種皆可,推薦使用反向
    2. r這種特殊的方法,是一種後備機制,如果左操作數沒有實作對應的方法,或者實作了,但是傳回的是

      NotImplemented

      表明它不知道如何處理右操作數,那麼Python會調用r的方法。
  4. 重載标量乘法運算符*
    1. Vector([1, 2, 3]) * x

      :計算标量積(scalar product),結果是一個新Vector執行個體,各個分量都會乘以x,這也叫元素級乘法(elementwise multiplication)
    2. 在Numpy庫中,點積使用

      numpy.dot()

      計算
    3. Python3.5起,引入

      @

      用作中綴點選運算符
    4. __mul__

      __rmul__

      方法
    5. decimal.Decimal 沒有把自己注冊為 numbers.Real 的虛拟子類。是以,Vector 類不會處理decimal.Decimal 數字。
  5. 中綴運算符方法的名稱
    運算符 正向方法 反向方法 就地方法 說明
    +

    __add__

    __radd__

    __iadd__

    加法或拼接
    -

    __sub__

    __rsub__

    __isub__

    減法
    *

    __mul__

    __rmul__

    __imul__

    乘法或重複複制
    /

    __truediv__

    __rtruediv__

    __itruediv__

    除法
    //

    __floordiv__

    __rfloordiv__

    __ifloordiv__

    整除
    %

    __mod__

    __rmod__

    __imod__

    取模
    divmod()

    __divmod__

    __rdivmod__

    __idivmod__

    傳回由整除的商和模數組成的元組
    **, pow()

    __pow__

    __rpow__

    __ipow__

    取幂
    @

    __matmul__

    __rmatmul__

    __imatmul__

    矩陣乘法
    &

    __add__

    __radd__

    __iadd__

    位與
    |

    __or__

    __ror__

    __ior__

    位或
    ^

    __xor__

    __rxor__

    __ixor__

    位異或
    <<

    __lshift__

    __rlshift__

    __ilshift__

    按位左移
    >>

    __rshift__

    __rrshift__

    __irshift__

    按位右移
  6. 衆多比較運算符
    1. ==、!=、>、<、>=、<=
    2. 正向和反向調用使用的是同一系列方法
    3. 而正向的

      __gt__

      方法調用的是反向的

      __lt__

      方法,并把參數對調
    4. 對 == 和 != 來說,如果反向調用失敗,Python 會比較對象的 ID,而不抛出 TypeError
    5. 衆多比較運算符:正向方法傳回

      NotImplemented

      的話,調用反向方法
  7. 衆多比較運算符
    分組 中綴運算符 正向方法調用 反向方法調用 後備機制
    相等性 a == b

    a.__eq__(b)

    b.__eq__(a)

    傳回

    id(a) == id(b)

    a != b

    a.__ne__(b)

    b.__ne__(a)

    傳回

    not(a == b)

    排序 a > b

    a.__gt__(b)

    b.__lt__(a)

    抛出TypeError
    a < b

    a.__lt__(b)

    b.__gt__(a)

    抛出TypeError
    a >= b

    a.__ge__(b)

    b.__le__(a)

    抛出TypeError
    a < b

    a.__le__(b)

    b.__ge__(a)

    抛出TypeError
  8. 增量指派運算符
    1. 如果一個類沒有實作上表列出的就地運算符,增量指派運算符隻是文法糖:a += b 的作用與 a = a + b 完全一樣
    2. 如果實作了就地運算符方法,例如

      __iadd__

      ,計算 a += b 的結果時會調用就地運算符方法。這種運算符的名稱表明,它們會就地修改左操作數,而不會建立新對象作為結果
    3. 不可變類型,一定不能實作就地特殊方法
  9. 如果操作數的類型不同,我們要檢測出不能處理的操作數。本章使用兩種方式處理這個問題:
    1. 一種是鴨子類型,直接嘗試執行運算,如果有問題,捕獲 TypeError 異常
      • 鴨子類型更靈活,但是顯式檢查更能預知結果
    2. 另一種是顯式使用 isinstance 測試,

      __mul__

      方法就是這麼做的
      • 如果選擇使用 isinstance,要小心,不能測試具體類,而要測試

        numbers.Real

        抽象基類,例如

        isinstance(scalar, numbers.Real)

      • 這在靈活性和安全性之間做了很好的折中
  10. 在可接受的類型方面,+ 應該比 += 嚴格
    • 對序列類型來說,+ 通常要求兩個操作數屬于同一類型
    • 而 += 的右操作數往往可以是任何可疊代對象

14. 可疊代的對象、疊代器和生成器

  • 疊代器模式(Iterator pattern):疊代是資料處理的基石。掃描記憶體中放不下的資料集時,我們要找到一種惰性擷取資料項的方式,即按需一次擷取一個資料項。
  • 疊代器:用于從集合中取出元素
  • 生成器:用于“憑空”生成元素
  • 在 Python社群中,大多數時候都把疊代器和生成器視作同一概念
  1. 單詞序列
    1. 第一版Sentence
      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 __getitem__(self, index):
              return self.words[index]
          
          def __len__(self):
              return len(self.words)
          
          def __repr__(self):
              return 'Sentence(%s)' % reprlib.repr(self.text)
                 
    2. 序列可以疊代的原因:iter函數,解釋器需要疊代對象 x 時,會自動調用 iter(x)。
    3. 内置的 iter 函數有以下作用:
      1. 檢查對象是否實作了

        __iter__

        方法,如果實作了就調用它,擷取一個疊代器
      2. 如果沒有實作

        __iter__

        方法,但是實作了

        __getitem__

        方法,Python 會建立一個疊代器,嘗試按順序(從索引 0 開始)擷取元素
      3. 如果嘗試失敗,Python 抛出 TypeError 異常,通常會提示“C object is not iterable”(C 對象不可疊代),其中 C 是目标對象所屬的類
    4. 這是鴨子類型(duck typing)的極端形式:不僅要實作特殊的

      __iter__

      方法,還要實作

      __getitem__

      方法,而且

      __getitem__

      方法的參數是從 0 開始的整數(int),這樣才認為對象是可疊代的
    5. 白鵝類型(goose typing)理論中,可疊代對象的定義簡單一些,不過沒那麼靈活:如果實作了

      __iter__

      方法,那麼就認為對象是可疊代的。此時,不需要建立子類,也不用注冊,因為 abc.Iterable 類實作了

      __subclasshook__

      方法
    6. 檢查對象 x 能否疊代,最準确的方法是:
      1. 調用 iter(x) 函數,如果不可疊代,再處理 TypeError 異常。
      2. 這比使用 isinstance(x, abc.Iterable) 更準确
      3. 因為 iter(x) 函數會考慮到遺留的

        __getitem__

        方法,而 abc.Iterable 類則

        不考慮

  2. 可疊代的對象與疊代器的對比
    1. 可疊代的對象和疊代器之間的關系:Python 從可疊代的對象中擷取疊代器
    2. StopIteration 異常表明疊代器到頭了
    3. 标準的疊代器接口有兩個方法:
      1. __next__

        :傳回下一個可用的元素,如果沒有元素了,抛出 StopIteration異常
      2. __iter__

        :傳回 self,以便在應該使用可疊代對象的地方使用疊代器,例如在 for 循環中
    4. Iterator(Iterable)

      源碼中,根據

      __subclasshook__(cls, C)

      函數識别C是不是Iterator的子類。這種方法是通過在

      C.__mro__

      中找是否同時存在

      __next__

      __iter__

      方法
    5. 檢查對象 x 是否為疊代器最好的方式是調用

      isinstance(x, abc.Iterator)

    6. 代器是這樣的對象:
      1. 實作了無參數的

        __next__

        方法,傳回序列中的下一個元素;
      2. 如果沒有元素了,那麼抛出 StopIteration 異常
      3. Python 中的疊代器還實作了

        __iter__

        方法,是以疊代器也可以疊代
  3. 典型的疊代器
    1. 建構可疊代的對象和疊代器時經常會出現錯誤,原因是混淆了二者
      1. 可疊代的對象有個

        __iter__

        方法,每次都執行個體化一個新的疊代器
      2. 而疊代器要實作

        __next__

        方法,傳回單個元素,此外還要實作

        __iter__

        方法,傳回疊代器本身
    2. 疊代器可以疊代,但是可疊代的對象不是疊代器
    3. 疊代器模式可用來:
      1. 通路一個聚合對象的内容而無需暴露它的内部表示
      2. 支援對聚合對象的多種周遊
      3. 為周遊不同的聚合結構提供一個統一的接口(即支援多态疊代)
    4. 為了“支援多種周遊”,必須能從同一個可疊代的執行個體中擷取多個獨立的疊代器,而且各個疊代器要能維護自身的内部狀态,是以這一模式正确的實作方式是,每次調用 iter(my_iterable) 都建立一個獨立的疊代器。這就是為什麼這個示例需要定義 SentenceIterator 類
    5. 可疊代的對象一定不能是自身的疊代器。也就是說,可疊代的對象必須實作

      __iter__

      方法,但不能實作

      __next__

      方法
    6. 另一方面,疊代器應該一直可以疊代。疊代器的

      __iter__

      方法應該傳回自身
    7. 第二版Sentence
      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
              return word
          
          def __iter__(self):
              return self
                 
  4. 生成器函數
    1. 達到 實作相同功能,但符合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:
                  yield word
              return 
                 
    2. 這個 return 語句不是必要的;這個函數可以直接“落空”,自動傳回。不管有沒有 return 語句,生成器函數都不會抛出 StopIteration 異常,而是在生成完全部值之後會直接退出
    3. 生成器函數的工作原理
      1. 隻要Python函數的定義體中有yield關鍵字,該函數就是生成器函數
      2. 調用生成器函數時,會傳回一個生成器對象
      3. 也就是說,生成器函數是生成器工廠
    4. 描述:
      1. 函數傳回值;
      2. 調用生成器函數傳回生成器;
      3. 生成器産出或生成值
      4. 生成器不會以正常的方式“傳回”值:生成器函數定義體中的 return 語句會觸發生成器對象抛出 StopIteration 異常
  5. 惰性實作
    1. 惰性求值(lazy evaluation)和 及早求值(eager evaluation)是程式設計語言理論方面的技術術語
    2. re.finditer 函數是 re.findall 函數的惰性版本,傳回的不是清單,而是一個生成器,能節省大量記憶體
    3. Sentence類第4版:
      import re
      import reprlib
      
      RE_WORD = re.compile('\w+')
      
      class Sentence:
          def __init__(self, text):
              self.text = 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()
                 
  6. 生成器表達式
    1. 生成器表達式可以了解為清單推導的惰性版本:不會迫切地建構清單,而是傳回一個生成器,按需惰性生成元素
    2. 如果清單推導是制造清單的工廠,那麼生成器表達式就是制造生成器的工廠
    3. 生成器表達式會産出生成器
      import re
      import reprlib
      
      RE_WORD = re.compile('\w+')
      
      class Sentence:
          def __init__(self, text):
              self.text = 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))
                 
    4. 生成器表達式是文法糖:完全可以替換成生成器函數,不過有時使用生成器表達式更便利
  7. 何時使用生成器表達式
    1. 如果函數或構造方法隻有一個參數,傳入生成器表達式時不用寫一對調用函數的括号,再寫一對括号圍住生成器表達式,隻寫一對括号就行了
    2. 如果生成器表達式後面還有其他參數,那麼必須使用括号圍住,否則會抛出 SyntaxError 異常
  8. 等差數列生成器
    1. 類生成器
      class ArithmeticProgression:
          def __init__(self, begin, step, end=None):
              self.begin = begin
              self.step = step
              self.end = end
              
          def __iter__(self):
              result = type(self.begin + self.step)(self.begin)
              forever = self.end is None
              index = 0
              while forever or result < self.end:
                  yield result
                  index += 1
                  result = self.begin + self.step * index
                 
    2. 函數生成器
      def aritpro_gen(begin, step, end=None):
          result = type(begin + step)(begin)
          forever = end is None
          index = 0
          while forever or result < end:
              yield result
              index += 1
              result = begin + step * index
                 
    3. 使用itertools子產品生成等差數列
      import itertools
      
      gen = itertools.count(1, .5)
      next(gen)
                 
    4. itertools.takewhile

      • 它會生成一個使用另一個生成器的生成器
      • 在指定的條件計算結果為 False 時停止
      • 可以把這兩個函數結合在一起使用
    5. 生成器工廠,傳回的是一個生成器
      import itertools
      
      def aritpro_gen(begin, step, end=None):
          first = type(begin + step)(begin)
          ap_gen = itertools.count(first, step)
          if end is not None:
              ap_gen = itertools.takewhile(lambda n: n < end, ap_gen)
          return ap_gen
                 
  9. 标準庫中的生成器函數,參考連結
    1. 下面大多數函數都接受一個斷言參數(predicate),這個參數是個布爾函數,有一個參數,會應用到輸入中的每個元素上,用于判斷元素是否包含在輸出中。

      用于過濾的生成器函數

      子產品 函數 說明
      itertools compress(it, selector_it) 并行處理兩個可疊代的對象;如果 selector_it 中的元素是真值,産出 it 中對應的元素
      itertools dropwhile(predicate, it) 處理 it,跳過 predicate 的計算結果為真值的元素,然後産出剩下的各個元素(不再進一步檢查)
      (内置) filter(predicate, it) 把 it 中的各個元素傳給 predicate,如果predicate(item) 傳回真值,那麼産出對應的元素;如果 predicate 是 None,那麼隻産出真值元素
      itertools filterfalse(predicate, it) 與 filter 函數的作用類似,不過 predicate 的邏輯是相反的:predicate 傳回假值時産出對應的元素
      itertools islice(it, stop) 或islice(it, start, stop, step=1) 産出 it 的切片,作用類似于 s[:stop] 或s[start:stop:step],不過 it 可以是任何可疊代的對象,而且這個函數實作的是惰性操作
      itertools takewhile(predicate, it) predicate 傳回真值時産出對應的元素,然後立即停止,不再繼續檢查
    2. 上述函數測試
      1. compress(it, selector_it)

        按照真值表篩選元素
        import itertools
        
        temp = [1, 0, -1, 2, 4, 9]
        temp_select = [x > 2 for x in temp]
        it = itertools.compress(temp, temp_select)
        
        list(it)
        # [4, 9]
                   
      2. dropwhile(predicate, it)

        按照真值函數丢棄掉清單和疊代器前面的元素
        import itertools
        
        x = itertools.dropwhile(lambda e: e < 5, range(10))
        
        list(x)
        # [5, 6, 7, 8, 9]
                   
      3. filter(predicate, it)

        過濾函數為True的元素
        x = filter(lambda e: e < 5, range(10))
        
        list(x)
        # [0, 1, 2, 3, 4]
                   
      4. filterfalse(predicate, it)

        保留對應真值為False的元素
        import itertools
        
        x = itertools.filterfalse(lambda e: e < 5, (1, 5, 3, 6, 9, 4))
        list(x)
        # [5, 6, 9]
                   
      5. islice(it, stop) 或 islice(it, start, stop, step=1)

        對疊代器進行切片
        import itertools
        
        x = itertools.islice(range(10), 0, 9, 2)
        list(x)
        # [0, 2, 4, 6, 8]
                   
      6. takewhile(predicate, it)

        與dropwhile相反,保留元素直至真值函數值為假(有順序,一旦為假,後面的就不管了)
        import itertools
        
        x = itertools.takewhile(lambda x: x.lower() in 'aeiou', 'Aardvark')
        list(x)
        # ['A', 'a']
                   
    3. 下一組是用于映射(map)的生成器函數:在輸入的單個可疊代對象(map 和starmap 函數處理多個可疊代的對象)中的各個元素上做計算,然後傳回結果
      • 生成器函數會從輸入的可疊代對象中的各個元素中産出一個元素
      • 如果輸入來自多個可疊代的對象,第一個可疊代的對象到頭後就停止輸出
      • 用于映射的生成器函數
        子產品 函數 說明
        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 starmap(func, it) 把 it 中的各個元素傳給 func,産出結果;輸入的 可疊代對象應該産出可疊代的元素 iit,然後以 func(*iit) 這種形式調用 func
    4. 上述函數測試
      1. accumulate(it, [func])

        簡單來說就是累加
        import itertools
        
        sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
        print(list(itertools.accumulate(sample)))
        # [5, 9, 11, 19, 26, 32, 35, 35, 44, 45]
        print(list(itertools.accumulate(sample, min)))
        # [5, 4, 2, 2, 2, 2, 2, 0, 0, 0]
        print(list(itertools.accumulate(sample, max)))
        # [5, 5, 5, 8, 8, 8, 8, 8, 9, 9]
        
        import operator
        print(list(itertools.accumulate(sample, operator.mul)))
        # [5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0]
        print(list(itertools.accumulate(range(1, 11), operator.mul)))
        # [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
                   
      2. enumerate(iterable, start=0)

        生成編号
        print(list(enumerate('albatroz', 1)))
        # [(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')]
                   
      3. map(func, it1, [it2, ..., itN])

        import itertools
        
        print(list(map(operator.mul, range(11), range(1, 12, 1))))
        # [0, 2, 6, 12, 20, 30, 42, 56, 72, 90, 110]
                   
        import itertools
        
        print(list(map(lambda a,b : (a, b), range(11), [2, 4, 8])))
        # [(0, 2), (1, 4), (2, 8)]
                   
      4. starmap(func, it)

        類似map,

        itertools

        子產品的

        starmap()

        函數實際上是

        map()

        函數的

        *a

        版本,參考連結
        import itertools
        
        print(list(itertools.starmap(operator.mul, enumerate('albatroz', 1))))
        # ['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']
        
        sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
        print(list(itertools.starmap(lambda a, b: b / a, enumerate(itertools.accumulate(sample), 1))))
        # [5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333, 5.0, 4.375, 4.888888888888889, 4.5]
                   
    5. 這一組是用于合并的生成器函數,這些函數都從輸入的多個可疊代對象中産出元素。chain 和 chain.from_iterable 按順序(一個接一個)處理輸入的可疊代對象,而 product、zip 和 zip_longest 并行處理輸入的各個可疊代對象

      合并多個可疊代對象的生成器函數

      子產品 函數 說明
      itertools chain(it1, …, itN) 先産出 it1 中的所有元素,然後産出 it2 中的所有元素,以此類推,無縫連接配接在一起
      itertools chain.from_iterable(it) 産出 it 生成的各個可疊代對象中的元素,一個接一個,無縫連接配接在一起;it 應該産出可疊代的元素,例如可疊代的對象清單
      itertools product(it1, …, itN, repeat=1) 計算笛卡兒積:從輸入的各個可疊代對象中擷取元素,合并成由 N 個元素組成的元組,與嵌套的 for 循環效果一樣;repeat 指明重複處理
      (内置) zip(it1, …, itN) 并行從輸入的各個可疊代對象中擷取元素,産出由 N 個元素組成的元組,隻要有一個可疊代的對象到頭了,就默默地停止
      itertools zip_longest(it1, …, itN, fillvalue=None) 并行從輸入的各個可疊代對象中擷取元素,産出由 N 個元素組成的元組,等到最長的可疊代對象到頭後才停止,空缺的值使用 fillvalue 填充
    6. 上述函數測試
      1. chain(it1, ..., itN)

        list(itertools.chain('ABC', range(2)))
        # ['A', 'B', 'C', 0, 1]
                   
      2. chain.from_iterable(it)

        chain.from_iterable 函數從可疊代的對象中擷取每個元素,然後按順序把元素連接配接起來,前提是各個元素本身也是可疊代的對象
        list(itertools.chain(enumerate('ABC')))
        # [(0, 'A'), (1, 'B'), (2, 'C')]
                   
        list(itertools.chain.from_iterable(enumerate('ABC')))
        # [0, 'A', 1, 'B', 2, 'C']
                   
      3. zip(it1, ..., itN)

        zip_longest(it1, ..., itN, fillvalue=None)

        list(zip('ABC', range(5)))
        # [('A', 0), ('B', 1), ('C', 2)]
                   
        list(itertools.zip_longest('ABC', range(5)))
        # [('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
                   
        list(itertools.zip_longest('ABC', range(6), fillvalue='?'))
        # [('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
                   
      4. product(it1, ..., itN, repeat=1)

        計算笛卡兒積的惰性方式
        list(itertools.product('ABC', range(2)))
        # [('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]
        
        print(list(itertools.product('AK', suits)))
        # [('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'), ('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')]
        
        list(itertools.product('ABC'))
        # [('A',), ('B',), ('C',)]
        
        print(list(itertools.product('ABC', repeat=2)))
        # [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
        
        print(list(itertools.product('AB', repeat=3)))
        # [('A', 'A', 'A'), ('A', 'A', 'B'), ('A', 'B', 'A'), ('A', 'B', 'B'), ('B', 'A', 'A'), ('B', 'A', 'B'), ('B', 'B', 'A'), ('B', 'B', 'B')]
        
        print(list(itertools.product('AB', range(2), repeat=2)))
        # [('A', 0, 'A', 0), ('A', 0, 'A', 1), ('A', 0, 'B', 0), ('A', 0, 'B', 1), ('A', 1, 'A', 0), ('A', 1, 'A', 1), ('A', 1, 'B', 0), ('A', 1, 'B', 1), ('B', 0, 'A', 0), ('B', 0, 'A', 1), ('B', 0, 'B', 0), ('B', 0, 'B', 1), ('B', 1, 'A', 0), ('B', 1, 'A', 1), ('B', 1, 'B', 0), ('B', 1, 'B', 1)]
                   
    7. 有些生成器函數會從一個元素中産出多個值,擴充輸入的可疊代對象

      把輸入的各個元素擴充成多個輸出元素的生成器函數

      子產品 函數 說明
      itertools combinations(it, out_len) 把 it 産出的 out_len 個元素組合在一起,然後産出
      itertools combinations_with_replacement(it, out_len) 把 it 産出的 out_len 個元素組合在一起,然後産出,包含相同元素的組合
      itertools count(start=0, step=1) 從 start 開始不斷産出數字,按 step 指定的步幅增加
      itertools cycle(it) 從 it 中産出各個元素,存儲各個元素的副本,然後按順序重複不斷地産出各個元素
      itertools permutations(it, out_len=None) 把 out_len 個 it 産出的元素排列在一起,然後産出這些排列;out_len的預設值等于 len(list(it))
      itertools repeat(item, [times]) 重複不斷地産出指定的元素,除非提供 times,指定次數
    8. 上述函數測試
      1. count(start=0, step=1)

        ct = itertools.count()
        next(ct), next(ct), next(ct)
        # (0, 1, 2)
        
        list(itertools.islice(itertools.count(1, .3), 3))
        # [1, 1.3, 1.6]
                   
      2. cycle(it)

        cy = itertools.cycle('ABC')
        list(itertools.islice(cy, 7))
        # ['A', 'B', 'C', 'A', 'B', 'C', 'A']
                   
      3. repeat(item, [times])

        repeat 函數的常見用途:為 map 函數提供固定參數,這裡提供的是

        乘數 5

        rp = itertools.repeat(7)
        next(rp), next(rp), next(rp)
        # (7, 7, 7)
        
        list(itertools.repeat(8, 4))
        # [8, 8, 8, 8]
        
        list(map(operator.mul, range(11), itertools.repeat(5)))
        # [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
                   
      4. combinations(it, out_len)

        • combinations、combinations_with_replacement 和 permutations 生成器函數,連同 product 函數,稱為組合學生成器(combinatoric generator)
        • itertools.product 函數和其餘的組合學函數有緊密的聯系
        # 兩兩組合,不包括自己,順序無關,相當于排列組合中的組合
        list(itertools.combinations('ABC', 2))
        # [('A', 'B'), ('A', 'C'), ('B', 'C')]
        
        # 兩兩組合,包括自己,順序無關
        list(itertools.combinations_with_replacement('ABC', 2))
        # [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
        
        # 兩兩組合,不包括自己,順序有關
        list(itertools.permutations('ABC', 2))
        # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
        
        # 兩兩組合,笛卡爾積,包括自己,順序有關
        print(list(itertools.product('ABC', repeat=2)))
        # [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
                   
    9. 最後一組生成器函數用于産出輸入的可疊代對象中的全部元素,不過會以某種方式重新排列

      用于重新排列元素的生成器函數

      子產品 函數 說明
      itertools groupby(it, key=None) 産出由兩個元素組成的元素,形式為 (key, group),其中 key 是分組标準,group 是生成器,用于産出分組裡的元素
      (内置) reversed(seq) 從後向前,倒序産出 seq 中的元素;seq 必須是序列,或者是實作了

      __reversed__

      特殊方法的對象
      itertools tee(it, n=2) 産出一個由 n 個生成器組成的元組,每個生成器用于單獨産出輸入的可疊代對象中的元素
    10. 上述函數測試
      1. groupby(it, key=None)

        itertools.groupby

        假定輸入的可疊代對象要使用分組标準排序;即使不排序,至少也要使用指定的标準分組各個元素
        import itertools
        
        print(list(itertools.groupby('LLLLAAGGG')))
        # [('L', <itertools._grouper object at 0x0000018A1AF32948>), ('A', <itertools._grouper object at 0x0000018A1AF32248>), ('G', <itertools._grouper object at 0x0000018A1AF32708>)]
        
        for char, group in itertools.groupby('LLLLAAAGG'):
            print(char, '->', list(group))
        # L -> ['L', 'L', 'L', 'L']
        # A -> ['A', 'A', 'A']
        # G -> ['G', 'G']
        
        animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', 'bat', 'dolphin', 'shark', 'lion']
        animals.sort(key=len)
        animals
        # ['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin']
        
        for length, group in itertools.groupby(animals, len):
            print(length, '->', list(group))
        """
        3 -> ['rat', 'bat']
        4 -> ['duck', 'bear', 'lion']
        5 -> ['eagle', 'shark']
        7 -> ['giraffe', 'dolphin']
        """
        
        for length, group in itertools.groupby(reversed(animals), len):
            print(length, '->', list(group))
        """
        7 -> ['dolphin', 'giraffe']
        5 -> ['shark', 'eagle']
        4 -> ['lion', 'bear', 'duck']
        3 -> ['bat', 'rat']
        """
                   
      2. tee(it, n=2)

        輸入的一個可疊代對象中産出多個生成器,每個生成器都可以産出輸入的各個元素
        list(itertools.tee('ABC'))
        # [<itertools._tee at 0x18a1b6ac808>, <itertools._tee at 0x18a1b6ac688>]
        
        g1, g2 = itertools.tee('ABC')
        list(g2), list(g2)
        # (['A', 'B', 'C'], ['A', 'B', 'C'])
        
        list(zip(*itertools.tee('ABC')))
        # [('A', 'A'), ('B', 'B'), ('C', 'C')]
                   
  10. Python3.3中新出現的文法:yield from
    1. 下面兩個代碼等價
      def chain(*iterrables):
          print(iterrables)
          for it in iterrables:
              for i in it:
                  yield i
                 
      def chain(*iterables):
          for i in iterables:
              yield from i
                 
    2. 可以看出,yield from i 完全代替了内層的 for 循環
    3. 除了代替循環之外,yield from 還會建立通道,把内層生成器直接與外層生成器的用戶端聯系起來。把生成器當成協程使用時,這個通道特别重要,不僅能為用戶端代碼生成值,還能使用用戶端代碼提供的值
  11. 可疊代的歸約函數
    1. 下述函數都接受一個可疊代的對象,然後傳回單個結果。這些函數叫“歸約”函數、“合攏”函數或“累加”函數
    2. 這裡列出的每個内置函數都可以使用 functools.reduce 函數實作,内置是因為使用它們便于解決常見的問題
    3. 對 all 和 any 函數來說,有一項重要的優化措施是 reduce 函數做不到的:這兩個函數會短路(即一旦确定了結果就立即停止使用疊代器)
    4. 讀取疊代器,傳回單個值的内置函數
    5. sorted 和這些歸約函數隻能處理最終會停止的可疊代對象。否則,這些函數會一直收集元素,永遠無法傳回結果
      子產品 函數 說明
      (内置) all(it) it 中的所有元素都為真值時傳回 True,否則傳回 False;all([]) 傳回 True
      (内置) any(it) 隻要 it 中有元素為真值就傳回 True,否則傳回 False;any([]) 傳回 False
      (内置)

      max(it, [key=,][default=])

      傳回 it 中值最大的元素;key 是排序函數,與 sorted 函數中的一樣;如果可疊代的對象為空,傳回 default
      (内置)

      min(it, [key=,][default=])

      傳回 it 中值最小的元素;key 是排序函數,與 sorted 函數中的一樣;如果可疊代的對象為空,傳回 default
      functools reduce(func,it,[initial]) 把前兩個元素傳給 func,然後把計算結果和第三個元素傳給 func,以此類推,傳回最後的結果;如果提供了initial,把它當作第一個元素傳入
      (内置) sum(it,start=0) it 中所有元素的總和,如果提供可選的 start,會把它加上(計算浮點數的加法時,可以使用 math.fsum 函數提高精度)
  12. 深入分析iter函數
    1. iter 函數還有一個鮮為人知的用法
      • 傳入兩個參數,使用正常的函數或任何可調用的對象建立疊代器
      • 這樣使用時,第一個參數必須是可調用的對象,用于不斷調用(沒有參數),産出各個值
      • 第二個值是哨符,這是個标記值,當可調用的對象傳回這個值時,觸發疊代器抛出 StopIteration 異常,而不産出哨符
    2. 如何使用 iter 函數擲骰子,直到擲出 1 點為止
      from random import randint
      
      def d6():
          return randint(1, 6)
      
      d6_iter = iter(d6, 1)
      for roll in d6_iter:
          print(roll)
                 
    3. 逐行讀取檔案,直到遇到空行或者到達檔案末尾為止:
      with open('mydate.txt') as fp:
          for line in iter(fp.readline, '\n'):
              process_line(line)
                 
  13. 案例分析:在資料庫轉換工具中使用生成器
  14. 把生成器當成協程
    1. .__next__()

      方法一樣,

      .send()

      方法緻使生成器前進到下一個yield 語句
    2. .send()

      方法還允許使用生成器的客戶把資料發給自己,即不管傳給

      .send()

      方法什麼參數,那個參數都會成為生成器函數定義體中對應的 yield 表達式的值
    3. 也就是說,

      .send()

      方法允許在客戶代碼和生成器之間雙向交換資料
    4. .__next__()

      方法隻允許客戶從生成器中擷取資料
    5. 像這樣使用的話,生成器就變身為協程
    6. 雖然在協程中會使用 yield 産出值,但這與疊代無關
  15. 雜談
    1. grok 的意思不僅是學會了新知識,還要充分吸收知識,做到“人劍合一”
    2. 設計模式在各種程式設計語言中使用的方式并不相同
    3. yield 關鍵字隻能把最近的外層函數變成生成器函數
    4. 雖然生成器函數看起來像函數,可是我們不能通過簡單的函數調用把職責委托給另一個生成器函數
    5. Python 新引入的 yield from 句法允許生成器或協程把工作委托給第三方完成,這樣就無需嵌套 for 循環作為變通了
      def f(): 
          def do_yield(n):
              yield n
          x=0
          while True:
              x += 1
              yield from do_yield(x)
                 
    6. 在協程中,yield 碰巧(通常)出現在指派語句的右手邊,因為 yield 用于接收客戶傳給 .send() 方法的參數
    7. 盡管有一些相同之處,但是生成器和協程基本上是兩個不同的概念
  16. 生成器與疊代器的語義對比
    1. 第一方面是接口
      • Python 的疊代器協定定義了兩個方法:

        __next__

        __iter__

      • 生成器對象實作了這兩個方法,是以從這方面來看,所有生成器都是疊代器
      • 由此可以得知,内置的 enumerate() 函數建立的對象是疊代器
    2. 第二方面是實作方式
      • 生成器這種 Python 語言結構可以使用兩種方式編寫:含有 yield 關鍵字的函數,或者生成器表達式
      • 調用生成器函數或者執行生成器表達式得到的生成器對象屬于語言内部的 GeneratorType 類型
      • 從這方面來看,所有生成器都是疊代器,因為 GeneratorType 類型的執行個體實作了疊代器接口
      • 我們可以編寫不是生成器的疊代器,方法是實作經典的疊代器模式
      • 從這方面來看,enumerate 對象不是生成器
      • types.GeneratorType 類型的定義:生成器-疊代器對象的類型,調用生成器函數時生成
    3. 第三方面是概念
      • 在典型的疊代器設計模式中,疊代器用于周遊集合,從中産出元素
        • 不管典型的疊代器中有多少邏輯,都是從現有的資料源中讀取值
        • 疊代器不能修改從資料源中讀取的值,隻能原封不動地産出值
      • 生成器可能無需周遊集合就能生成值
        • 即便依附了集合,生成器不僅能産出集合中的元素,還可能會産出派生自元素的其他值

15. 上下文管理器和else塊

  • with 語句會設定一個臨時的上下文,交給上下文管理器對象控制,并且負責清理上下文
  • 這麼做能避免錯誤并減少樣闆代碼,是以 API 更安全,而且更易于使用
  • 了自動關閉檔案之外,with 塊還有很多用途
  1. 先做這個,再做那個:if語句之外的else塊
    1. else 子句不僅能在if 語句中使用,還能在 for、while 和 try 語句中使用
    2. else-for:僅當 for 循環運作完畢時(即 for 循環沒有被 break 語句中止)才運作 else 塊
    3. else-while:僅當 while 循環因為條件為假值而退出時(即 while 循環沒有被 break 語句中止)才運作 else 塊
    4. try-else:僅當 try 塊中沒有異常抛出時才運作 else 塊,else 子句抛出的異常不會由前面的 except 子句處理
    5. 在所有情況下,如果異常或者 return、break 或 continue 語句導緻控制權跳到了複合語句的主塊之外,else 子句也會被跳過
    6. 這裡在嘗試了解的時候,可以把else了解為then,即,嘗試運作這個,然後做那個,意思就是說,在運作for/while/try沒有發生問題時,就運作else
  2. 上下文管理器和with塊
    1. with 語句的目的是簡化 try/finally 模式
    2. 上下文管理器協定包含

      __enter__

      __exit__

      兩個方法
      • with 語句開始運作時,會在上下文管理器對象上調用

        __enter__

        方法
      • with 語句運作結束後,會在上下文管理器對象上調用

        __exit__

        方法
    3. 執行 with 後面的表達式得到的結果是上下文管理器對象
    4. 不過,把值綁定到目标變量上(as 子句)是在上下文管理器對象上調用

      __enter__

      方法的結果
    5. 解釋器調用

      __enter__

      方法時,除了隐式的 self 之外,不會傳入任何參數
    6. 傳給

      __exit__

      方法的三個參數列舉如下:
      1. exc_type

        :異常類(例如 ZeroDivisionError)
      2. exc_value

        :異常執行個體。有時會有參數傳給異常構造方法,例如錯誤消息,這些參數可以使用 exc_value.args 擷取
      3. traceback

        :traceback 對象
    7. 例子
      class LookingGlass:
          def __enter__(self):
              import sys
              self.original_write = sys.stdout.write
              sys.stdout.write = self.reverse_write
              return 'JABBERWOCKY'
          
          def reverse_write(self, text):
              self.original_write(text[::-1])
              
          def __exit__(self, exc_type, exc_value, traceback):
              import sys
              sys.stdout.write = self.original_write
              if exc_type is ZeroDivisionError:
                  print(exc_type, exc_value, traceback)
                  print('Please DO NOT divide by zero')
                  return True
                 
  3. contextlib子產品中的實用工具
    1. closing:如果對象提供了 close() 方法,但沒有實作

      __enter__/__exit__

      協定,那麼可以使用這個函數建構上下文管理器
    2. suppress:建構臨時忽略指定異常的上下文管理器
    3. @contextmanager(用得最多):這個裝飾器把簡單的生成器函數變成上下文管理器,這樣就不用建立類去實作管理器協定了

      **注意:**它與疊代無關,卻要使用 yield 語句

    4. ContextDecorator:這是個基類,用于定義基于類的上下文管理器。這種上下文管理器也能用于裝飾函數,在受管理的上下文中運作整個函數
    5. ExitStack:這個上下文管理器能進入多個上下文管理器
      • with 塊結束時,ExitStack 按照後進先出的順序調用棧中各個上下文管理器的

        __exit__

        方法
      • 如果事先不知道 with 塊要進入多少個上下文管理器,可以使用這個類
  4. 使用@contextmanager
    1. @contextmanager 裝飾器能減少建立上下文管理器的樣闆代碼量
    2. 隻需實作有一個 yield 語句的生成器,生成想讓

      __enter__

      方法傳回的值
    3. 在使用 @contextmanager 裝飾的生成器中,yield 語句的作用是把函數的定義體分成兩部分:
      • yield 語句前面的所有代碼在 with 塊開始時(即解釋器調用

        __enter__

        方法時)執行
      • yield 語句後面的代碼在 with 塊結束時(即調用

        __exit__

        方法時)執行
    4. 使用生成器實作的上下文管理器
      import contextlib
      
      @contextlib.contextmanager
      def looking_glass():
          import sys
          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
                 
    5. 其實,contextlib.contextmanager 裝飾器會把函數包裝成實作

      __enter__

      __exit__

      方法的類;類的名稱是 _GeneratorContextManager
    6. 這個類的

      __enter__

      方法有如下作用:
      1. 調用生成器函數,儲存生成器對象(這裡把它稱為 gen)
      2. 調用 next(gen),執行到 yield 關鍵字所在的位置
      3. 傳回 next(gen) 産出的值,以便把産出的值綁定到 with/as 語句中的目标變量上
    7. with 塊終止時,

      __exit__

      方法會做以下幾件事
      1. 檢查有沒有把異常傳給 exc_type;如果有,調用 gen.throw(exception),在生成器函數定義體中包含 yield 關鍵字的那一行抛出異常
      2. 否則,調用 next(gen),繼續執行生成器函數定義體中 yield 語句之後的代碼
    8. 處理異常的代碼:
      import contextlib
      
      @contextlib.contextmanager
      def looking_glass():
          import sys
          original_write = sys.stdout.write
          
          def reverse_write(text):
              original_write(text[::-1])
          
          sys.stdout.write = reverse_write
          msg = ''
          try:
              yield 'JABBERWOCKY'
          except Exception:
              msg = '出錯啦'
          finally:
              sys.stdout.write = original_write
              if msg:
                  print(msg)
                 
    9. 使用 @contextmanager 裝飾器時,要把 yield 語句放在try/finally 語句中(或者放在 with 語句中),這是無法避免的,因為我們永遠不知道上下文管理器的使用者會在 with 塊中做什麼
    10. 用于原地重寫檔案的上下文管理器
  5. 在 @contextmanager 裝飾器裝飾的生成器中,yield 與疊代沒有任何關系。在本節所舉的示例中,生成器函數的作用更像是協程:執行到某一點時暫停,讓客戶代碼運作,直到客戶讓協程繼續做事
  6. with 不僅能管理資源,還能用于去掉正常的設定和清理代碼,或者在另一個過程前後執行的操作
  7. @contextmanager 裝飾器優雅且實用,把三個不同的 Python 特性結合到了一起:函數裝飾器、生成器和 with 語句

16. 協程

  • 字典為動詞“to yield”給出了兩個釋義:産出和讓步
  • 對于 Python 生成器中的 yield 來說,這兩個含義都成立
  • yield item 這行代碼會産出一個值,提供給 next(…) 的調用方;此外,還會作出讓步,暫停執行生成器,讓調用方繼續工作,直到需要使用另一個值時再調用next()
  1. 生成器如何進化成協程
    1. 協程是指一個過程,這個過程與調用方協作,産出由調用方提供的值
    2. .send(...)

      :生成器的調用方可以使用 .send(…) 方法發送資料,發送的資料會成為生成器函數中 yield 表達式的值
    3. .throw(...)

      :讓調用方抛出異常,在生成器中處理
    4. .close()

      :終止生成器
  2. 用作協程的生成器的基本行為
    1. 協程使用生成器函數定義:定義體中有 yield 關鍵字
    2. 協程可以身處四個狀态中的一個
    3. 目前狀态可以使用

      inspect.getgeneratorstate(...)

      函數确定
      • GEN_CREATED:等待開始執行
      • GEN_RUNNING:解釋器正在執行
      • GEN_SUSPENDED:在 yield 表達式處暫停
      • GEN_CLOSED:執行結束
    4. 僅當協程處于暫停狀态時才能調用 send 方法
    5. 如果協程還沒激活(即,狀态是 ‘GEN_CREATED’)
      • 始終要調用

        next(my_coro)

        激活協程
      • 也可以調用

        my_coro.send(None)

        ,效果一樣
    6. 我對yield在協程中使用的了解
      def simple_coro2(a):
          print('-> Started: a =', a)
          b = yield a
          print('-> Received: b =', b)
          c = yield a + b
          print('-> Received: c =', c)
                 
      • 拿這個例子為例
      • b = yield a

        :也就是說,程式運作到這一行的時候,先執行

        yield a

        ,相當于

        return a

        ,前面的

        b = yield

        是等待指派
      • 然後使用

        my_cc.send(28)

        ,就是給b指派了,這一行才算運作結束
      • 是以這一行的運作,夾在了兩個

        next(my_cc)

      《流暢的python》學習筆記及書評《流暢的python》學習筆記
  3. 示例:使用協程計算移動平均值
    def averager():
        total = 0.0
        count = 0
        average = None
        while True:
            term = yield average
            total += term
            count += 1
            average = total/count
               
    • 使用協程之前必須預激
  4. 預激協程的裝飾器
    • 為了簡化協程的用法,有時會使用一個預激裝飾器
      from functools import wraps
      
      def coroutine(func):
          """裝飾器:向前執行到第一個yield表達式,預激func"""
          @wraps(func)
          def primer(*args, **kwargs):
              gen = func(*args, **kwargs)
              next(gen)
              return gen
          return primer
                 
    • 需要在協程的函數前面加這個裝飾器
      @coroutine
      def averager():
          total = 0.0
          count = 0
          average = None
          while True:
              term = yield average
              total += term
              count += 1
              average = total/count
                 
    • 這樣該協程的初始化的狀态就是 GEN_SUSPENDED
      from inspect import getgeneratorstate
      getgeneratorstate(coro_avg)
      
      # 'GEN_SUSPENDED'
                 
    • 很多架構都提供了處理協程的特殊裝飾器
      • 不是所有裝飾器都用于預激協程
      • 有些會提供其他服務,例如勾入事件循環
      • 比如說,異步網絡庫 Tornado 提供了 tornado.gen 裝飾器
    • 使用 yield from 句法調用協程時,會自動預激
    • Python 3.4 标準庫裡的 asyncio.coroutine 裝飾器不會預激協程,是以能相容 yield from 句法
  5. 終止協程和異常處理
    1. 協程中未處理的異常會向上冒泡,傳給 next 函數或 send 方法的調用方(即觸發協程的對象)
    2. 終止協程的一種方式:發送某個哨符值,讓協程退出
    3. 内置的 None 和 Ellipsis 等常量經常用作哨符值
    4. 客戶代碼可以在生成器對象上調用兩個方法,顯式地把異常發給協程
      • generator.throw(exc_type[, exc_value[, traceback]]):緻使生成器在暫停的 yield 表達式處抛出指定的異常
      • generator.close():緻使生成器在暫停的 yield 表達式處抛出 GeneratorExit 異常
    5. 學習在協程中處理異常的測試代碼
      class DemoException(Exception):
          """為這次示範定義的異常類型"""
          
      def demo_exc_handling():
          print('-> coroutine started')
          while True:
              try:
                  x = yield
              except DemoException:
                  print('*** DemoException handled. Continuing...')
              else:
                  print('-> coroutine received: {!r}'.format(x))
      
          raise RuntimeError('This line should never run.')
                 
      • 最後一行代碼不會執行,因為隻有未處理的異常才會中止那個無限循環,而一旦出現未處理的異常,協程會立即終止
      • 激活和關閉 demo_exc_handling,沒有異常
      • 把 DemoException 異常傳入 demo_exc_handling 不會導緻協程中止
      • 如果無法處理傳入的異常,協程會終止
    6. 使用 try/finally 塊在協程終止時執行操作
      class DemoException(Exception):
          """為這次示範定義的異常類型"""
          
      def demo_exc_handling():
          print('-> coroutine started')
          try:
              while True:
                  try:
                      x = yield
                  except DemoException:
                      print('*** DemoException handled. Continuing...')
                  else:
                      print('-> coroutine received: {!r}'.format(x))
          finally:
              print('-> coroutine ending...')
      
          raise RuntimeError('This line should never run.')
                 
    7. Python 3.3 引入 yield from 結構的主要原因
      • 與把異常傳入嵌套的協程有關
      • 讓協程更友善地傳回值
  6. 讓協程傳回值
    1. 定義一個求平均值的協程,讓它傳回一個結果
      from collections import namedtuple
      
      Result = namedtuple('Result', 'count average')
      
      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 Result(count, average)
                 
    2. 最後發送None的時候,協程結束,傳回結果。抛出異常StopIteration,并将return的值儲存到異常對象的value屬性中
    3. 如何擷取協程的傳回值
      try:
          coro_avg.send(None)
      except StopIteration as exc:
          result = exc.value
          
      result
      # Result(count=3, average=15.5)
                 
    4. yield from 結構會在内部自動捕獲 StopIteration 異常
  7. 使用yield from
    1. yield from 可用于簡化 for 循環中的 yield 表達式
      • 下述:
        def gen():
            for c in 'AB':
                yield c
            for i in range(1, 3):
                yield i
                   
      • 可改寫為:
        def gen():
            yield from 'AB'
            yield from range(1, 3)
                   
    2. 相關術語
      1. 委派生成器:包含

        yield from <iterable>

        表達式的生成器函數
      2. 子生成器:從 yield from 表達式中

        <iterable>

        部分擷取的生成器
      3. 調用方:調用委派生成器的用戶端代碼
    3. 委派生成器在 yield from 表達式處暫停時,調用方可以直接把資料發給子生成器,子生成器再把産出的值發給調用方
    4. 子生成器傳回之後,解釋器會抛出 StopIteration 異常,并把傳回值附加到異常對象上,此時委派生成器會恢複
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
    5. 如果子生成器不終止,委派生成器會在 yield from 表達式處永遠暫停
      from collections import namedtuple
      
      Result = namedtuple('Result', 'count average')
      
      # 子生成器 
      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 Result(count, average)
      
      # 委派生成器
      def grouper(results, key):
          while True:
              results[key] = yield from averager()
              
      # 用戶端代碼,即調用方
      def main(data):
          results = {}
          for key, values in data.items():
              group = grouper(results, key)
              next(group)
              for value in values:
                  group.send(value)
              # 重要
              group.send(None)
              report(results)
              
      # 輸出報告
      def report(results):
          for key, result in sorted(results.items()):
              group, unit = key.split(';')
              print('{:2}{:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))
              
      data = {
      'girls;kg':
      [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
      'girls;m':
      [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
      'boys;kg':
      [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
      'boys;m':
      [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
      }
      
      if __name__ == '__main__':
          main(data)
                 
    6. 委派生成器相當于管道,是以可以把任意數量個委派生成器連接配接在一起
    7. 一個委派生成器使用 yield from 調用一個子生成器,而那個子生成器本身也是委派生成器,使用 yield from 調用另一個子生成器,以此類推
    8. 最終,這個鍊條要以一個隻使用 yield 表達式的簡單生成器結束;不過,也能以任何可疊代的對象結束
    9. 任何 yield from 鍊條都必須由客戶驅動,在最外層委派生成器上調用 next(…) 函數或 .send(…) 方法
  8. yield from 的意義(6點 yield from 的行為)
    1. 子生成器産出的值都直接傳給委派生成器的調用方(即用戶端代碼)
    2. 使用 send() 方法發給委派生成器的值都直接傳給子生成器。如果發送的值是 None,那麼會調用子生成器的

      __next__()

      方法。如果發送的值不是 None,那麼會調用子生成器的 send() 方法。如果調用的方法抛出 StopIteration 異常,那麼委派生成器恢複運作。任何其他異常都會向上冒泡,傳給委派生成器
    3. 生成器退出時,生成器(或子生成器)中的 return expr 表達式會觸發 StopIteration(expr) 異常抛出
    4. yield from 表達式的值是子生成器終止時傳給 StopIteration 異常的第一個參數
    yield from 結構的另外兩個特性與異常和終止有關
    1. 傳入委派生成器的異常,除了 GeneratorExit 之外都傳給子生成器的 throw() 方法。如果調用 throw() 方法時抛出 StopIteration 異常,委派生成器恢複運作。StopIteration 之外的異常會向上冒泡,傳給委派生成器
    2. 如果把 GeneratorExit 異常傳入委派生成器,或者在委派生成器上調用 close() 方法,那麼在子生成器上調用 close() 方法,如果它有的話。

      如果調用 close() 方法導緻異常抛出,那麼異常會向上冒泡,傳給委派生成器;否則,委派生成器抛出 GeneratorExit 異常

    • 通過閱讀 yield from 的僞代碼,我們可以看到代碼裡已經 預激了子生成器,這說明,用于自動預激的裝飾器與 yield from 不相容
  9. 使用案例:使用協程做離散時間仿真
    1. 在計算機科學領域,仿真是協程的經典應用
    2. 通過仿真系統能說明如何使用協程代替線程實作并發的活動
    3. 離散事件仿真:Discrete Event Simulation,DES,是一種把系統模組化成一系列事件的仿真類型
    4. 為了實作連續仿真,在多個線程中處理實時并行的操作更自然。而協程恰好為實作離散事件仿真提供了合理的抽象
    5. 一個示例:說明如何在一個主循環中處理事件,以及如何通過發送資料驅動協程
      sim = Simulator(taxis)
      sim.run(end_time)
                 
      import queue
      
      class Simulator:
          def __init__(self, procs_map):
              self.events = queue.PriorityQueue()
              self.procs = dict(procs_map)
              
          def run(self, end_time): #1
              """排定并顯示事件,直到時間結束"""
              # 排定各輛計程車的第一個事件
              for _, proc in sorted(self.procs.items()): #2
                  first_event = next(proc) #3
                  self.events.put(first_event) #4
              
              # 這個仿真系統的主循環
              sim_time = 0 #5
              while sim_time < end_time: #6
                  if self.events.empty(): #7
                      print('*** end of events ***')
                      break
                  
                  current_event = self.events.get() #8
                  sim_time, proc_id, previous_action = current_event #9
                  print('taxi:', proc_id, proc_id * '  ', current_event) #10
                  active_proc = self.procs[proc_id] #11
                  # 傳入前一個動作,把結果加到sim_time上,獲得下一次活動的時刻
                  next_time = sim_time + compute_duration(previous_action) #12
                  try:
                      next_event = active_proc.send(next_time) #13
                  except StopIteration:
                      del self.procs[proc_id] #14
                  else:
                      self.events.put(next_event) #15
              
              else: #16
                  msg = '*** end of simulation time: {} events pending ***'
                  print(msg.format(self.events.qsize()))            
                 
  10. 本章小結:
    1. 生成器有三種不同代碼編寫風格:
      • 傳統的拉取式,疊代器
      • 推送式,計算平均值
      • 任務式,協程
    2. 假如沒有協程,我們要寫一個并發程式。可能有以下問題:
      • 使用最正常的同步程式設計要實作異步并發效果并不理想,或者難度極高
      • 由于GIL鎖的存在,多線程的運作需要頻繁的加鎖解鎖,切換線程,這極大地降低了并發性能
    3. 而協程的出現,剛好可以解決以上的問題。它的特點有:
      • 協程是在單線程裡實作任務的切換的
      • 利用同步的方式去實作異步
      • 不再需要鎖,提高了并發性能
    4. 深入了解yield from
    5. 事件驅動型架構(如 Tornado 和 asyncio)的運作方式:
      • 在單個線程中使用一個主循環驅動協程執行并發活動
      • 使用協程做面向事件程式設計時,協程會不斷把控制權讓步給主循環,激活并向前運作其他協程,進而執行各個并發活動
      • 這是一種協作式多任務:協程顯式自主地把控制權讓步給中央排程程式
      • 而多線程實作的是搶占式多任務。排程程式可以在任何時刻暫停線程(即使在執行一個語句的過程中),把控制權讓給其他線程。
    6. 寬泛的、不正式的對協程的定義:通過客戶調用 .send(…) 方法發送資料或使用 yield from 結構驅動的生成器函數
    7. asyncio 庫建構在協程之上,不過采用的協程定義更為嚴格
      • 在 asyncio 庫中,協程(通常)使用 @asyncio.coroutine 裝飾器裝飾
      • 而且始終使用 yield from 結構驅動
      • 而不通過直接在協程上調用 .send(…) 方法驅動
      • 當然,在 asyncio 庫的底層,協程使用 next(…) 函數和 .send(…) 方法驅動,不過在使用者代碼中隻使用 yield from 結構驅動協程運作
    8. SimPy 是使用标準的 Python 開發的基于程序的離散事件仿真架構,事件排程程式基于 Python 的生成器實作,是以還可用于異步網絡或實作多智能體系統(即可模拟,也可真正通信)
    9. Python 3.5 已經接受了 PEP 492,增加了兩個關鍵字:async 和 await
    10. Python Async/Await入門指南
      • 在3.5過後,我們可以使用async修飾将普通函數和生成器函數包裝成異步函數和異步生成器

17. 使用期物處理并發

  • 期物:future,期物指一種對象,表示異步執行的操作
  • 這個概念的作用很大,是 concurrent.futures 子產品和 asyncio 包的基礎
  1. 網絡下載下傳的三種風格
    1. 對并發下載下傳的腳本來說,每次下載下傳的順序都不同
    2. 拒絕服務:Denial-of-Service,DoS
    3. 在 I/O 密集型應用中,如果代碼寫得正确,那麼不管使用哪種并發政策(使用線程或 asyncio 包),吞吐量都比依序執行的代碼高很多
    4. 第一種,直接按順序下載下傳棋子
      import os
      import time
      import sys
      
      import requests
      
      POP20_CC = ('CN IN US ID BR P1K NG BD RU JP MX PH VN ET EG DE IR TR CD FR').split()
      
      BASE_URL = 'http://flupy.org/data/flags'
      
      DEST_DIR = './downloads/'
      
      def save_flag(img, filename):
          path = os.path.join(DEST_DIR, filename)
          with open(path, 'wb') as fp:
              fp.write(img)
              
      def get_flag(cc):
          url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
          resp = requests.get(url)
          return resp.content
      
      def show(text):
          print(text, end=' ')
          sys.stdout.flush()
          
      def download_many(cc_list):
          for cc in sorted(cc_list):
              image = get_flag(cc)
              show(cc)
              save_flag(image, cc.lower() + '.gif')
              
          return len(cc_list)
      
      def main(download_many):
          t0 = time.time()
          count = download_many(POP20_CC)
          elapsed = time.time() - t0
          msg = '\n{} flags downloaded in {:.2f}s'
          print(msg.format(count, elapsed))
          
      if __name__ == '__main__':
          main(download_many)
          
      # BD BR CD CN DE EG ET FR ID IN IR JP MX NG P1K PH RU TR US VN 
      # 20 flags downloaded in 21.82s
                 
    5. 第二種,使用

      concurrent.futures

      子產品下載下傳
      from concurrent import futures
      
      from flags import save_flag, get_flag, show, main
      
      MAX_WORKERS = 20
      
      def download_one(cc):
          image = get_flag(cc)
          show(cc)
          save_flag(image, cc.lower() + '.gif')
          return cc
      
      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))
              
          return len(list(res))
      
      if __name__ == '__main__':
          main(download_many)
      
      # BD CD BR TR IR FR JP IN MX VNET  PH NG EG CN ID DE P1K US RU 
      # 20 flags downloaded in 1.05s
                 
      • concurrent.futures 子產品的主要特色是 ThreadPoolExecutor 和 ProcessPoolExecutor 類
      • 這兩個類實作的接口能分别在不同的線程或程序中執行可調用的對象
      • 這兩個類在内部維護着一個工作線程或程序池,以及要執行的任務隊列
    6. 從 Python 3.4 起,标準庫中有兩個名為 Future 的類:concurrent.futures.Future 和 asyncio.Future
    7. 期物封裝待完成的操作,可以放入隊列,完成的狀态可以查詢,得到結果(或抛出異常)後可以擷取結果(或異常)
    8. 通常情況下自己不應該建立期物,而隻能由并發架構(concurrent.futures 或 asyncio)執行個體化
    9. 原因很簡單:期物表示終将發生的事情,而确定某件事會發生的唯一方式是執行的時間已經排定
    10. 是以,隻有排定把某件事交給 concurrent.futures.Executor 子類處理時,才會建立 concurrent.futures.Future 執行個體
    11. 用戶端代碼不應該改變期物的狀态,并發架構在期物表示的延遲計算結束後會改變期物的狀态,而我們無法控制計算何時結束
    12. .done()

      :這個方法不阻塞,傳回值是布爾值,指明期物連結的可調用對象是否已經執行
    13. .add_done_callback()

      :這個方法隻有一個參數,類型是可調用的對象,期物運作結束後會調用指定的可調用對象
    14. .result()

      :傳回可調用對象的結果,或者重新抛出執行可調用的對象時抛出的異常
      • 如果期物沒有運作結束,result 方法在兩個 Future 類中的行為相差很大
      • 對concurrency.futures.Future 執行個體來說,調用

        f.result()

        方法會阻塞調用方所在的線程,直到有結果可傳回。此時,result 方法可以接收可選的 timeout 參數,如果在指定的時間内期物沒有運作完畢,會抛出 TimeoutError 異常
      • asyncio.Future.result 方法不支援設定逾時時間,在那個庫中擷取期物的結果最好使用 yield from 結構
    15. 這兩個庫中有幾個函數會傳回期物,其他函數則使用期物,以使用者易于了解的方式實作自身
    16. Executor.map 方法屬于後者:傳回值是一個疊代器,疊代器的

      __next__

      方法調用各個期物的result 方法,是以我們得到的是各個期物的結果,而非期物本身
    17. concurrent.futures.as_completed

      :這個函數的參數是一個期物清單,傳回值是一個疊代器,在期物運作結束後産出期物
    18. 把download_many 函數中的 executor.map 方法換成 executor.submit 方法和 futures.as_completed 函數
      def download_many(cc_list):
          cc_list = cc_list[:5]
          with futures.ThreadPoolExecutor(max_workers=3) as executor:
              to_do = []
              for cc in sorted(cc_list):
                  future = executor.submit(download_one, c)
                  to_do.append(future)
                  msg = 'Scheduled for {}: {}'
                  print(msg.format(cc, future))
                  
              results = []
              for future in futures.as_completed(to_do):
                  res = future.result()
                  msg = '{} result: {!r}'
                  print(msg.format(future, res))
                  results.append(res)
                  
          return len(results)
                 
    19. GIL:Global Interpreter Lock,全局解釋器鎖
    20. GIL 幾乎對 I/O 密集型處理無害
  2. 阻塞型I/O和GIL
    1. CPython 解釋器本身就不是線程安全的,是以有全局解釋器鎖(GIL),一次隻允許使用一個線程執行 Python 位元組碼
    2. 是以,一個 Python 程序通常不能同時使用多個 CPU 核心
    3. 編寫 Python 代碼時無法控制 GIL;不過,執行耗時的任務時,可以使用一個内置的函數或一個使用 C 語言編寫的擴充釋放 GIL
    4. 标準庫中所有執行阻塞型 I/O 操作的函數,在等待作業系統傳回結果時都會釋放 GIL,這意味着在 Python 語言這個層次上可以使用多線程,而 I/O 密集型 Python 程式能從中受益
    5. 一個 Python 線程等待網絡響應時,阻塞型 I/O 函數會釋放 GIL,再運作一個線程
  3. 使用concurrent.futures子產品啟動程序
    1. 如果需要做 CPU 密集型處理,使用這個子產品的ProcessPoolExecutor類能繞開 GIL,利用所有可用的 CPU 核心
    2. 對簡單的用途來說,ThreadPoolExecutor和ProcessPoolExecutor這兩個實作Executor接口的類唯一值得注意的差別是,
      • ThreadPoolExecutor.__init__

        方法需要max_workers參數,制定線程池中線程的數量
      • 在 ProcessPoolExecutor 類中,那個參數是可選的,而且大多數情況下不使用——預設值是

        os.cpu_count()

        函數傳回的 CPU 數量
  4. 實驗Executor.map方法
    1. Executor.map 函數傳回結果的順序與調用開始的順序一緻
    2. 不過,通常更可取的方式是,不管送出的順序,隻要有結果就擷取。為此,要把 Executor.submit 方法和 futures.as_completed 函數結合起來使用
    3. executor.submit 和 futures.as_completed 這個組合比executor.map 更靈活,因為 submit 方法能處理不同的可調用對象和參數,而 executor.map 隻能處理參數不同的同一個可調用對象
    4. 傳給 futures.as_completed 函數的期物集合可以來自多個 Executor 執行個體
  5. 顯示下載下傳進度并處理錯誤
    1. flags2系列示例處理錯誤的方式
    2. 使用

      futures.as_completed

      函數
    3. 線程和多程序的替代方案
      1. concurrent.futures 是使用線程的最新方式
      2. 如果

        futures.ThreadPoolExecutor

        類對某個作業來說不夠靈活,可能要

        使用 threading 子產品中的元件(如 Thread、Lock、Semaphore 等)自行制定方案

      3. 對 CPU 密集型工作來說,要啟動多個程序,規避 GIL
      4. 建立多個程序最簡單的方式是,使用 futures.ProcessPoolExecutor 類
      5. 如果使用場景較複雜,需要更進階的工具。使用 multiprocessing 子產品,API 與 threading 子產品相仿,不過作業交給多個程序處理
      6. multiprocessing 子產品還能解決協作程序遇到的最大挑戰:在程序之間傳遞資料
  6. 小結
    1. 為什麼盡管有 GIL,Python 線程仍然适合 I/O 密集型應用:準庫中每個使用 C 語言編寫的 I/O 函數都會釋放 GIL,是以,當某個線程在等待 I/O 時, Python 排程程式會切換到另一個線程
    2. 借助

      concurrent.futures.ProcessPoolExecutor

      類使用多程序,以此繞

      開 GIL,使用多個 CPU 核心運作

    3. 對于 CPU 密集型和資料密集型并行處理,現在有個新工具可用——分布式計算引擎 Apache Spark,Spark 在大資料領域發展勢頭強勁,提供了友好的 Python API,支援把 Python 對象當作資料
    4. lelo

      包:定義了一個@parallel 裝飾器,可以應用到任何函數上,把函數變成非阻塞:調用被裝飾的函數時,函數在一個新程序中執行
    5. python-parallelize

      包提供了一個 parallelize 生成器,能把 for 循環配置設定給多個 CPU 執行
    6. 這兩個包在内部都使用了 multiprocessing 子產品
    7. GIL 簡化了 CPython 解釋器和 C 語言擴充的實作,得益于 GIL,Python 有很多 C 語言擴充
    8. Python 線程特别适合在 I/O 密集型系統中使用
    9. 在 JavaScript 中,隻能通過回調式異步程式設計實作并發

18. 使用asyncio包處理并發

  1. 線程與協程對比
    1. spinner_thread.py
      import threading
      import itertools
      import time
      import sys
      
      class Signal:
          go = True
          
      
      def spin(msg, signal):
          write, flush = sys.stdout.write, sys.stdout.flush
          for char in itertools.cycle('|/-\\'):
              status = char + ' ' + msg
              write(status)
              flush()
              write('\x08' * len(status))
              time.sleep(.1)
              if not signal.go:
                  break
          write(' ' * len(status) + '\x08' * len(status))
          
      def slow_function():
          # 假裝等待I/O一段時間
          time.sleep(3)
          return 42
      
      def supervisor():
          signal = Signal()
          spinner = threading.Thread(target=spin, args=('thinking!', signal))
          print('spinner object:', spinner)
          spinner.start()
          result = slow_function()
          signal.go = False
          spinner.join()
          return result
      
      def main():
          result = supervisor()
          print('Answer:', result)
          
      if __name__ == '__main__':
          main()
                 
    2. Python 沒有提供終止線程的 API,這是有意為之的。若想關閉線程,必須給線程發送消息,這裡是signal.go屬性
    3. spinner_asyncio.py:通過協程以動畫形式顯示文本式旋轉指針;使用 @asyncio.coroutine 裝飾器替代線程,實作相同的行為
      import asyncio
      import itertools
      import sys
      
      
      @asyncio.coroutine #1
      def spin(msg): #2
          write, flush = sys.stdout.write, sys.stdout, flush
          for char in itertools.cycle('|/-\\'):
              status = char + ' ' + msg
              write(status)
              flush()
              write('\x08' * len(status))
              try:
                  yield from asyncio.sleep(.1) #3
              except asyncio.CancelledError: #4
                  break
          write(' ' * len(status) + '\x08' * len(status))
          
      
      @asyncio.coroutine
      def slow_function(): #5
          # 假裝等待I/O一段時間
          yield from asyncio.sleep(3) #6
          return 42
      
      @asyncio.coroutine
      def supervisor(): #7
          spinner = asyncio.async(spin('thinking!')) #8
          print('spinner object:', spinner) #9
          result = yield from slow_function() #10
          spinner.cancel() #11
          return result
      
      def main():
          loop = asyncio.get_event_loop() #12
          result = loop.run_until_complete(supervisor()) #13
          loop.close()
          print('Answer:', result)
          
      
      if __name__ == '__main__':
          main()
                 
      1. 使用

        yield from asyncio.sleep(.1)

        代替

        time.sleep(.1)

        ,這樣的休眠不會阻塞事件循環
      2. asyncio.async(...)

        函數排定 spin 協程的運作時間,使用一個Task 對象包裝 spin 協程,并立即傳回
    4. 除非想阻塞主線程,進而當機事件循環或整個應用,否則不要在 asyncio 協程中使用

      time.sleep(...)

      。如果協程需要在一段時間内什麼也不做,應該使用

      yield from asyncio.sleep(DELAY)

    5. 兩種 supervisor 實作之間的主要差別概述如下:
      1. asyncio.Task 對象差不多與 threading.Thread 對象等效
      2. Task 對象用于驅動協程,Thread 對象用于調用可調用的對象
      3. Task 對象不由自己動手執行個體化,而是通過把協程傳給

        asyncio.async(...)

        函數或 loop.create_task(…) 方法擷取
      4. 擷取的 Task 對象已經排定了運作時間(例如,由

        asyncio.async

        函數排定);Thread 執行個體則必須調用 start 方法,明确告知讓它運作
      5. 線上程版 supervisor 函數中,slow_function 函數是普通的函數,直接由線程調用。在異步版 supervisor 函數中,slow_function 函數是協程,由 yield from 驅動
      6. 沒有 API 能從外部終止線程,因為線程随時可能被中斷,導緻系統處于無效狀态。如果想終止任務,可以使用 Task.cancel() 執行個體方法,在協程内部抛出 CancelledError 異常。協程可以在暫停的yield 處捕獲這個異常,處理終止請求
      7. supervisor 協程必須在 main 函數中由 loop.run_until_complete 方法執行
    6. asyncio.Future

      :故意不阻塞
      1. asyncio.Future 類與 concurrent.futures.Future 類的接口基本一緻,不過實作方式不同,不可以互換
      2. asyncio.Future 類的 .result() 方法沒有參數,是以不能指定逾時時間。此外,如果調用 .result() 方法時期物還沒運作完畢,那麼.result() 方法不會阻塞去等待結果,而是抛出asyncio.InvalidStateError 異常
      3. 使用 yield from 處理期物與使用 add_done_callback 方法處理協程的作用一樣:延遲的操作結束後,事件循環不會觸發回調對象,

        而是設定期物的傳回值;而 yield from 表達式則在暫停的協程中生成

        傳回值,恢複執行協程

      4. 因為 asyncio.Future 類的目的是與 yield from 一起使用,是以通常不需要使用以下方法
        1. 無需調用 my_future.add_done_callback(…),因為可以直接把想在期物運作結束後執行的操作放在協程中 yield from my_future 表達式的後面。這是協程的一大優勢:協程是可以暫停和恢複的函數
        2. 無需調用 my_future.result(),因為 yield from 從期物中産出的值就是結果(例如,result = yield from my_future)
    7. 從期物、任務和協程中産出
      1. 對協程來說,擷取 Task 對象有兩種主要方式:
        1. asyncio.async(coro_or_future, *, loop=None)

        2. BaseEventLoop.create_task(coro)

      2. 測試腳本中試驗期物和協程:
        import asyncio
        def run_sync(coro_or_future):
            loop = asyncio.get_event_loop()
            return loop.run_until_complete(coro_or_future)
        
        a = run_sync(some_coroutine())
                   
  2. 使用asyncio和aiohttp包下載下傳
    1. flags_asyncio.py:使用 asyncio 和 aiohttp 包實作的異步下載下傳腳本
      import asyncio
      import aiohttp # 1
      from flags import BASE_URL, save_flag, show, main #2
      
      @asyncio.coroutine #3
      def get_flag(cc):
          url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
          resp = yield from aiohttp.request('GET', url) #4
          image = yield from resp.read() #5
          return image
      
      @asyncio.coroutine
      def download_one(cc): #6
          image = yield from get_flag(cc) #7
          show(cc)
          save_flag(image, cc.lower() + '.gif')
          return cc
      
      def download_many(cc_list):
          loop = asyncio.get_event_loop() #8
          to_do = [download_one(cc) for cc in sorted(cc_list)] #9
          wait_coro = asyncio.wait(to_do) #10
          res, _ = loop.run_until_complete(wait_coro) #11
          loop.close() #12
          	
          return len(res)
      
      
      if __name__ == '__main__':
          main(download_many)
                 
      1. 雖然函數的名稱是 asyncio.wait,但它不是阻塞型函數。wait 是一個協程,等傳給它的所有協程運作完畢後結束
      2. wait 函數有兩個關鍵字參數,如果設定了可能會傳回未結束的期物;這兩個參數是timeout 和 return_when
    2. 【我的了解】 協程,就是給函數加上了

      @asyncio.coroutine

      裝飾器的函數,然後裡面需要等待的地方加上了

      yield from

      語句,如果去掉這些語句,就是正常調用的阻塞型流程。
    3. yield from foo

      句法能防止阻塞,是因為當協程(即包含yield from代碼的委派生成器)暫停後,控制權回到事件循環手中,再去驅動其他協程;foo期物或協程運作完畢後,把結果傳回給暫停的協程,将其恢複。
    4. 關于 yield from 的用法的兩點陳述:
      1. 使用 yield from 連結的多個協程最終必須由不是協程的調用方驅動,調用方顯式或隐式(例如,在 for 循環中)在最外層委派生成器上調用 next(…) 函數或 .send(…) 方法
      2. 鍊條中最内層的子生成器必須是簡單的生成器(隻使用 yield)或 可疊代的對象
    5. 在 asyncio 包的 API 中使用 yield from 時,上述兩點都成立,不過要注意下述細節:
      1. 我們編寫的協程鍊條始終通過把最外層委派生成器傳給 asyncio 包 API 中的某個函數(如 loop.run_until_complete(…))驅動。也就是說,使用 asyncio 包時,我們編寫的代碼不通過調用 next(…) 函數或 .send(…) 方法驅動協程——這一點由asyncio 包實作的事件循環去做
      2. 我們編寫的協程鍊條最終通過 yield from 把職責委托給 asyncio 包中的某個協程函數或協程方法(例如:yield from asyncio.sleep(…)),或者其他庫中實作高層協定的協程(例如:resp = yield from aiohttp.request(‘GET’, url))

        也就是說,最内層的子生成器是庫中真正執行 I/O 操作的函數,而

        不是我們自己編寫的函數

    6. 概括起來就是:使用 asyncio 包時,我們編寫的異步代碼中包含由 asyncio 本身驅動的協程(即委派生成器),而生成器最終把職責委托給 asyncio 包或第三方庫(如 aiohttp)中的協程。這種處理方式相當于架起了管道,讓 asyncio 事件循環(通過我們編寫的協程)驅動執行低層異步 I/O 操作的庫函數
  3. 避免阻塞型調用
    1. 有兩種方法能避免阻塞型調用中止整個應用程式的程序:
      1. 在單獨的線程中運作各個阻塞型操作
      2. 把每個阻塞型操作轉換成非阻塞的異步調用使用
    2. 為了降低記憶體的消耗,通常使用回調來實作異步調用
    3. 使用回調時,我們不等待響應,而是注冊一個函數,在發生某件事時調用。這樣,所有調用都是非阻塞的
    4. 隻有異步應用程式底層的事件循環能依靠基礎設定的中斷、線程、輪詢和背景程序等,確定多個并發請求能取得進展并最終完成,這樣才能使用回調
    5. 把生成器當作協程使用是異步程式設計的另一種方式。對事件循環來說,調用回調與在暫停的協程上調用 .send() 方法效果差不多。各個暫停的協程是要消耗記憶體,但是比線程消耗的記憶體數量級小。而且,協程能避免可怕的“回調地獄”
    6. 異步和協程
  4. 改進asyncio下載下傳版本
    1. 使用 asyncio.as_completed 函數
    2. raise X from Y:連結原來的異常
    3. Semaphore對象維護着一個内部計數器,若在對象調用 .acquire() 協程方法,計數器則遞減;若在對象上調用 .release() 協程方法,計數器則遞增。
    4. 如果計數器大于0,那麼調用 .acquire() 方法不會阻塞;可是,如果計數器為0,那麼 .acquire() 方法會阻塞調用這個方法的協程,直到其他協程在同一個Semaphore對象上調用 .release() 方法,讓計數器遞增。
    5. flags2_asyncio.py
      import asyncio
      import collections
      
      import aiohttp
      from aiohttp import web
      import tqdm
      
      from flags2_common import main, HTTPStatus, Result, save_flag
      
      # 預設設為較小的值,防止遠端網站出錯
      # 例如503 - Service Temporarily Unavaliable
      DEFAULT_CONCUR_REQ = 5
      MAX_CONCUR_REQ = 1000
      
      class FetchError(Exception): #1
          def __init__(self, country_code):
              self.country_code = country_code
              
      @asyncio.coroutine
      def get_flag(base_url, cc): #2
          url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower())
          resp = yield from aiohttp.request('GET', url)
          if resp.status == 200:
              image = yield from resp.read()
              return image
          elif resp.status == 404:
              raise web.HTTPNotFound()
          else:
              raise aiohttp.HttpProcessingError(code=resp.status, message=resp.reason, headers=resp.headers)
      
      @asyncio.coroutine
      def download_one(cc, base_url, semaphore, verbose): #3
          # semaphore: 信号标
          try:
              with (yield from semaphore): #4
                  image = yield from get_flag(base_url, cc) #5
          except web.HTTPNotFound: #6
              status = HTTPStatus.not_found
              msg = 'not found'
          except Exception as exc:
              raise FetchError(cc) from exc #7
          else:
              save_flag(image, cc.lower() + '.gif') #8
              status = HTTPStatus.ok
              msg = 'OK'
              
          if verbose and msg:
              print(cc, msg)
          
          return Result(status, cc)
      
      @asyncio.coroutine
      def downloader_coro(cc_list, base_url, verbose, concur_req): #1
          counter = collections.Counter()
          semaphore = asyncio.Semaphore(concur_req) #2
          to_do = [download_one(cc, base_url, semaphore, verbose) for cc in sorted(cc_list)] #3
          
          to_do_iter = asyncio.as_completed(to_do) #4
          if not verbose:
              to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) #5
          for future in to_do_iter: #6
              try:
                  res = yield from future #7
              except FetchError as exc: #8
                  country_code = exc.country_code #9
                  try:
                      error_msg = exc.__cause__.args[0] #10
                  except IndexError:
                      error_msg = exc.__cause__.__class__.__name__ #11
                  if verbose and error_msg:
                      msg = '*** Error for {}: {}'
                      print(msg.format(country_code, error_msg))
                  status = HTTPStatus.error
              else:
                  status = res.status
                  
              counter[status] += 1 #12
              
          return counter #13
      
      def download_many(cc_list, base_url, verbose, concur_req):
          loop = asyncio.get_event_loop()
          coro = downloader_coro(cc_list, base_url, verbose, concur_req)
          counts = loop.run_until_complete(coro) #14
          loop.close() #15
          return counts
      
      if __name__ == '__main__':
          main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
                 
    6. 擷取 asyncio.Future 對象的結果,最簡單的方法是使用 yield from,而不是調用 future.result() 方法
    7. 因為失敗時不能以期物為鍵從字典中擷取國家代碼,是以實作了自定義的 FetchError 異常,包裝網絡異常,并關聯相應的國家代碼,是以在詳細模式中報告錯誤時能顯示國家代碼。
  5. 使用Executor對象,防止阻塞事件循環
    1. asyncio 的事件循環在背後維護着一個 ThreadPoolExecutor 對象,我們可以調用 run_in_executor 方法,把可調用的對象發給它執行。
    2. 在 download_one函數中,save_flag函數會阻塞客戶代碼與 asyncio 事件循環公用的唯一線程,是以儲存檔案時,整個應用程式都會當機。
    3. 解決方案:
      1. 之前的 download_one 函數中
        @asyncio.coroutine
        def download_one(cc, base_url, semaphore, verbose):
            try:
                ...
            else:
                save_flag(image, cc.lower() + '.gif')
                status = HTTPStatus.ok
                msg = 'OK'
                
            if verbose and msg:
                ...
                   
      2. 修改之後的代碼
        @asyncio.coroutine
        def download_one(cc, base_url, semaphore, verbose):
            try:
                ...
            else:
                loop = asyncio.get_event_loop()
                loop.run_in_executor(None, save_flag, image, cc.lower() + '.gif')
                status = HTTPStatus.ok
                msg = 'OK'
                
            if verbose and msg:
                ...
                   
      3. run_in_executor 方法的第一個參數是 Executor 執行個體,如果設為None,使用事件循環的預設 ThreadPoolExecutor 執行個體。
  6. 從回調到期物和協程
    1. 使用協程和yield from結構做異步程式設計,無需使用回調
      @asyncio.coroutine
      def three_stages(request1):
          reponse1 = yield from api_call1(request1)
          request2 = step1(response1)
          response2 = yield from api_call2(request2)
          request3 = step2(response2)
          response3 = yield from api_call3(request3)
          step3(response3)
          
      # 必須顯示排程執行
      loop.create_task(three_stages(request1))
                 
    2. 每次下載下傳發起多次請求
      @asyncio.coroutine
      def http_get(url):
          res = yield from aiohttp.request('GET', url)
          if res.status == 200:
              ctype = res.headers.get('Content-type', '').lower()
              if 'json' in ctype or url.endswith('json'):
                  data = yield from res.json() #1
              else:
                  data = yield from res.read() #2
              return data
          
          elif res.status == 404:
              raise web.HTTPNotFound()
          else:
              raise aiohttp.errors.HttpProcessingError(code=res.status, message=res.reason, headers=res.headers)
              
      @asyncio.coroutine
      def get_country(base_url, cc):
          url = '{}/{cc}/metadata.json'.format(base_url, cc=cc.lower())
          metadata = yield from http_get(url) #3
          return metadata['country']
      
      @asyncio.coroutine
      def get_flag(base_url, cc):
          url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower())
          return (yield from http_get(url)) #4
      
      @asyncio.coroutine
      def download_one(cc, base_url, semaphore, verbose):
          try:
              with (yield from semaphore): #5
                  image = yield from get_flag(base_url, cc)
              with (yield from semaphore):
                  country = yield from get_country(base_url, cc)
          except web.HTTPNotFound:
              status = HTTPStatus.not_found
              msg = 'not found'
          except Exception as exc:
              raise FetchError(cc) from exc
          else:
              country = country.replace(' ', '_')
              filename = '{}-{}.gif'.format(country, cc)
              loop = asyncio.get_event_loop()
              loop.run_in_executor(None, save_flag, image, filename)
              status = HTTPStatus.ok
              msg = 'ok'
          
          if verbose and msg:
              print(cc, msg)
              
          return Result(status, cc)
                 
  7. 使用asyncio包編寫伺服器
    1. 使用asyncio包編寫TCP伺服器
      import sys
      import asyncio
      
      from charfinder import UnicodeNameIndex #1
      
      CRLF = b'\r\n'
      PROMPT = b'?>'
      
      index = UnicodeNameIndex() #2
      
      @asyncio.coroutine
      def handle_queries(reader, writer): #3
          while True: #4
              writer.write(PROMPT) # 不能使用yield from #5
              yield from writer.drain() # 必須使用 yield from #6
              data = yield from reader.readline() #7
              try:
                  query = data.decode().strip()
              except UnicodeDecodeError: #8
                  query = '\x00'
              client = writer.get_extra_info('peername') #9
              print('Received from {}: {!r}'.format(client, query)) #10
              if query:
                  if ord(query[:1]) < 32: #11
                      break
                  lines = list(index.find_description_strs(query)) #12
                  if lines:
                      writer.writelines(line.encode() + CRLF for line in lines) #13
                  writer.write(index.status(query, len(lines)).encode() + CRLF) #14
                  
                  yield from writer.drain() #15
                  print('Sent {} results'.format(len(lines))) #16
              
              print('Close the client socket') #17
              writer.close() #18
              
      def main(address='127.0.0.1', port=2323): #1
          port = int(port)
          loop = asyncio.get_event_loop()
          server_coro = asyncio.start_server(handle_queries, address, port, loop=loop) #2
          server = loop.run_until_complete(server_coro) #3
          host = server.sockets[0].getsockname() #4
          print('Serving on {}. Hit CTRL-C to stop.'.format(host)) #5
          try:
              loop.run_forever() #6
          except KeyboardInterrupt: # 按CTRL-C鍵
              pass
          
          print('Server shutting down.')
          server.close() #7
          loop.run_until_complete(server.wait_closed()) #8
          loop.close() #9
      
      if __name__ == '__main__':
          main(*sys.argv[1:]) #10
                 
    2. 使用aiohttp包編寫Web伺服器
      1. asyncio.start_server 函數和 loop.create_server 方法都是協程,傳回的結果都是 asyncio.Server 對象
      2. 隻有驅動協程,協程才能做事。而驅動 asyncio.coroutine 裝飾的協程有兩種方法:
        • 要麼使用 yield from
        • 要麼傳給 asyncio 包中某個參數為協程或期物的函數,例如 run_until_complete
      3. 更好地支援并發的智能用戶端
      4. http_charfinder.py
        from aiohttp import web
        import asyncio, sys
        
        
        def home(request): #1
            query = request.GET.get('query', '').strip() # 2
            print('Query: {!r}'.format(query)) # 3
            if query: # 4
                descriptions = list(index.find_descriptions(query))
                res = '\n'.join(ROW_TPL.format(**vars(descr)) for descr in descriptions)
                msg = index.status(query, len(descriptions))
            else:
                descriptions = []
                res = ''
                msg = 'Enter words describing characters.'
                
            html = template.format(query=query, result=res, message=msg) # 5
            
            print('Sending {} results'.format(len(descriptions))) # 6
            return web.Response(content_type=CONTENT_TYPE, text=html) # 7
        
        @asyncio.coroutine
        def init(loop, address, port): # 1
            app = web.Application(loop=loop) #2
            app.router.add_route('GET', '/', home) #3
            handler = app.make_handler() # 4
            server = yield from loop.create_server(handler, address, port) # 5
            return server.sockets[0].getsockname() # 6
        
        
        def main(address='127.0.0.1', port=8889):
            port = int(port)
            loop = asyncio.get_event_loop()
            host = loop.run_until_complete(init(loop, address, port)) # 7
            print('Serving on {}. Hit CTRL-C to stop.'.format(host))
            try:
                loop.run_forever() # 8
            except KeyboardInterrupt: # 按CTRL-C鍵
                pass
            print('Server shutting down.')
            loop.close() # 9
            
        
        if __name__ == '__main__':
            main(*sys.argv[1:])
                   
  8. 小結:
    1. 盡管有些函數必然會阻塞,但是為了讓程式持續運作,有兩種解決方案可用:
      • 使用多個線程
      • 異步調用——以回調或協程的形式實作
    2. 異步庫依賴于低層線程(直至核心級線程),但是這些庫的使用者無需建立線程,也無需知道用到了基礎設施中的低層線程
    3. 使用 loop.run_in_executor 方法把阻塞的作業(例如儲存檔案)委托給線程池做
    4. 使用協程解決回調的主要問題:執行分成多步的異步任務時丢失上下文,以及缺少處理錯誤所需的上下文
    5. 智能的HTTP用戶端,例如單頁Web應用或智能手機應用,需要快速、輕量級的響應和推送更新。鑒于這樣的需求,伺服器端最好使用異步架構,不要使用傳統的Web架構(如Django)。傳統架構的目的是渲染完整的HTML網頁,而且不支援異步通路資料庫。
    6. Python 和 Node. js 都有一個問題,而 Go 和 Erlang 從一開始就解決了這個問題:我們編寫的代碼無法輕松地利用所有可用的 CPU 核心。

19. 動态屬性和特性

  1. 引子
    1. property:特性,在不改變類接口的前提下,使用存取方法(即讀值方法和設值方法)修改資料屬性
    2. attribute:屬性,在Python中,資料的屬性和處理資料的方法統稱屬性。
    3. 使用點号通路屬性時(如 obj.attr),Python 解釋器會調用特殊的方法(如

      __getattr__

      __setattr__

      )計算屬性
    4. 使用者自己定義的類可以通過

      __getattr__

      方法實作“虛拟屬性”,當通路不存在的屬性時(如 obj.no_such_attribute),即時計算屬性的值
  2. 使用動态屬性轉換資料
    1. 使用動态屬性通路JSON類資料
      from collections import abc
      
      class FrozenJson:
          """一個隻讀接口,使用屬性表示法通路JSON類對象"""
          
          def __init__(self, mapping):
              self.__data = dict(mapping) #1
              
          def __getattr__(self, name): #2
              if hasattr(self.__data, name):
                  return getattr(self.__data, name) #3
              else:
                  return FrozenJson.build(self.__data[name]) #4
          
          @classmethod
          def build(cls, obj): #5
              if isinstance(obj, abc.Mapping): #6
                  return cls(obj)
              elif isinstance(obj, abc.MutableSequence): #7
                  return [cls.build(item) for item in obj]
              else:
                  return obj
                 
    2. 從随機源中生成或仿效動态屬性名的腳本都必須處理一個問題:原始資料中的鍵可能不适合作為屬性名
    3. 處理無效屬性名
      from collections import abc
      import keyword
      
      class FrozenJson:
          """一個隻讀接口,使用屬性表示法通路JSON類對象"""
          
          def __init__(self, mapping):
              self.__data = {}
              for key, value in mapping.items():
                  if keyword.iskeyword(key):
                      key += '_'
                  self.__data[key] = value
              
          def __getattr__(self, name): #2
              if hasattr(self.__data, name):
                  return getattr(self.__data, name) #3
              else:
                  return FrozenJson.build(self.__data[name]) #4
          
          @classmethod
          def build(cls, obj): #5
              if isinstance(obj, abc.Mapping): #6
                  return cls(obj)
              elif isinstance(obj, abc.MutableSequence): #7
                  return [cls.build(item) for item in obj]
              else:
                  return obj
                 
    4. 使用

      __new__

      方法以靈活的方式建立對象

      使用

      __new__

      方法取代

      build

      方法,建構可能是也可能不是 FrozenJSON 執行個體的新對象
      from collections import abc
      
      class FrozenJSON:
          """一個隻讀接口,使用屬性表示法通路JSON類對象"""
          
          def __new__(cls, arg): #1
              if isinstance(arg, abc.Mapping):
                  return super().__new__(cls) #2
              elif isinstance(arg, abc.MutableSequence): #3
                  return [cls(item) for item in arg]
              else:
                  return arg
              
          def __init__(self, mapping):
              self.__data = {}
              for key, value in mapping.items():
                  if keyword.iskeyword(key):
                      key += '_'
                  self.__data[key] = value
                  
          def __getattr__(self, name):
              if hasattr(self.__data, name):
                  return getattr(self.__data, name)
              else:
                  return FrozenJson(self.__data[name]) #4
                 
    5. 使用shelve子產品調整OSCON資料源的結構
      1. shelve.open 高階函數傳回一個 shelve.Shelf 執行個體,這是簡單的鍵值對象資料庫,背後由 dbm 子產品支援,具有下述特點:
        • shelve.Shelf 是 abc.MutableMapping 的子類,是以提供了處理映射類型的重要方法
        • 此外,shelve.Shelf 類還提供了幾個管理 I/O 的方法,如 sync 和 close;它也是一個上下文管理器
        • 隻要把新值賦予鍵,就會儲存鍵和值
        • 鍵必須是字元串
        • 值必須是 pickle 子產品能處理的對象
      2. schedule1.py:通路儲存在 shelve.Shelf 對象裡的 OSCON 日程資料
        import warnings
        
        DB_NAME = 'data/schedule1_db'
        CONFERENCE = 'conference.115'
        
        
        class Record:
            def __init__(self, **kwargs):
                self.__dict__.update(kwargs) #2
                
            
        def load_db(db):
            raw_data = load() #3
            warnings.warn('loading ' + DB_NAME)
            for collection, rec_list in raw_data['Schedule'].items(): #4
                record_type = collection[:-1] #5
                for record in rec_list:
                    key = '{}.{}'.format(record_type, record['serial']) #6
                    record['serial'] = key #7
                    db[key] = Record(**record) #8
                   
        1. Record.__init__

          方法展示了一個流行的 Python 技巧。我們知道,對象的

          __dict__

          屬性中存儲着對象的屬性——前提是類中沒有聲明

          __slots__

          屬性
        2. 是以,更新執行個體的

          __dict__

          屬性,把值設為一個映射,能快速地在那個執行個體中建立一堆屬性
    6. 使用特性擷取連結的記錄:schedule2.py
      import warnings
      import inspect #1
      
      import osconfeed
      
      DB_NAME = 'data/schedule2_db' #2
      CONFERENCE = 'conference.115'
      
      
      class Record:
          def __init__(self, **kwargs):
              self.__dict__.update(kwargs)
              
          def __eq__(self, other): #3
              if isinstance(other, Record):
                  return self.__dict__ == other.__dict__
              else:
                  return NotImplemented
      
      class MissingDatabaseError(RuntimeError):
          """需要資料哭但沒有制定資料庫時抛出""" #1
          
      
      class DbRecord(Record): #2
          __db = None #3
          
          @staticmethod #4
          def set_db(db):
              DbRecord.__db = db #5
          
          @staticmethod #6
          def get_db():
              return DbRecordc.__db
          
          @classmethod #7
          def fetch(cls, ident):
              db = cls.get_db()
              try:
                  return db[ident] #8
              except TypeError:
                  if db is None: #9
                      msg = "database not set; call '{}.set_db(my_db)'"
                      raise MissingDatabaseError(msg.format(cls.__name__))
                  else: #10
                      raise
          
          def __repr__(self):
              if hasattr(self, 'serial'): #11
                  cls_name = self.__class__.__name__
                  return '<{} serial={!r}>'.format(cls_name, self.serial)
              else:
                  return super().__repr__() #12
              
      
      class Event(DbRecord): #1
          @property
          def venue(self):
              key = 'venue.{}'.format(self.venue_serial)
              return self.__class__.fetch(key) #2
          
          @property
          def speakers(self):
              if not hasattr(self, '_speaker_objs'): #3
                  spkr_serials = self.__dict__['speakers'] #4
                  fetch = self.__class__.fetch #5
                  self._speaker_objs = [fetch('speaker.{}'.format(key)) for key in spkr_serials] #6
                  
              return self._speaker_objs #7
          
          def __repr__(self):
              if hasattr(self, 'name'): #8
                  cls_name = self.__class__.__name__
                  return '<{} {!r}>'.format(cls_name, self.name)
              else:
                  return super().__repr__() #9
              
      def load_db(db):
          raw_data = osconfeed.load()
          warnings.warn('loading ' + DB_NAME)
          for collection, rec_list in raw_data['Schedule'].items():
              record_type = collection[:-1] #1
              cls_name = record_type.capitalize() #2
              cls = globals().get(cls_name, DbRecord) #3
              if inspect.isclass(cls) and issubclass(cls, DbRecord): #4
                  factory = cls #5
              else:
                  factory = DbRecord #6
              for record in rec_list: #7
                  key = '{}.{}'.format(record_type, record['serial'])
                  record['serial'] = key
                  db[key] = factory(**record) #8
                 
  3. 使用特性驗證屬性
    1. LineItem類第1版:表示訂單中商品的類
      class LineItem:
          def __init__(self, description, weight, price):
              self.description = description
              self.weight = weight
              self.price = price
              
          def subtotal(self):
              return self.weight * self.price
                 
    2. LineItem類第2版:能驗證值的特性
      class LineItem:
          def __init__(self, description, weight, price):
              self.description = description
              self.weight = weight
              self.price = price
              
          def subtotal(self):
              return self.weight * self.price
          
          @property
          def weight(self):
              return self.__weight
          
          @weight.setter
          def weight(self, value):
              if value > 0:
                  self.__weight = value
              else:
                  raise ValueError('value must be > 0')
                 
  4. 特性全解析
    1. 特性會覆寫執行個體屬性
      • 如果執行個體和所屬的類有同名資料屬性,那麼執行個體屬性會覆寫(或稱遮蓋)類屬性——至少通過那個執行個體讀取屬性時是這樣
      • 屬性:類變量(類屬性)、成員變量(執行個體屬性)(我的了解)
      • 特性:用@property修飾的,特性是類屬性(我的了解)
      • 同名變量會導緻,成員變量覆寫類變量,特性覆寫屬性
      • 删除特性後,類屬性和執行個體屬性,都會恢複
      • bj.attr 這樣的表達式不會從 obj 開始尋找 attr,而是從

        obj.__class__

        開始,而且,僅當類中沒有名為 attr 特性時,Python 才會在 obj 執行個體中尋找
      • 特性 其實是 覆寫型描述符
    2. 特性的文檔
      • 控制台中的 help() 函數或 IDE 等工具需要顯示特性的文檔時,會從特性的

        __doc__

        屬性中提取資訊
      • weight = property(get_weight, set_weight, doc='weight in kilograms')

        class Foo:
            @property
            def bar(self):
                '''The bar attribute'''
                return self.__dict__['bar']
            
            @bar.setter
            def bar(self, value):
                self.__dict__['bar'] = value
                   
  5. 定義一個特性工廠函數
    1. bulkfood_v2prop.py
      def quantity(storage_name): #1
          def qty_getter(instance): #2
              return instance.__dict__[storage_name] #3
          
          def qty_setter(instance, value): #4
              if value > 0:
                  instance.__dict__[storage_name] = value #5
              else:
                  raise ValueError('value must be > 0')
                  
          return property(qty_getter, qty_setter) #6
          
          
      class LineItem:
          weight = quantity('weight')
          price = quantity('price')
          
          def __init__(self, description, weight, price):
              self.description = description
              self.weight = weight
              self.price = price
              
          def subtotal(self):
              return self.weight * self.price
                 
    2. 對 self.weight 或 nutmeg.weight 的每個引用都由特性函數處理
    3. 隻有直接存取

      __dict__

      屬性才能跳過特性的處理邏輯
  6. 處理屬性删除操作
    • 使用 Python 程式設計時不常删除屬性,通過特性删除屬性更少見
    • 對象的屬性可以使用 del 語句删除
      class BlackKnight:
          def __init__(self):
              self.members = ['an arm', 'another arm', 'a leg', 'another leg']
              self.phrases = ["'Tis but a scratch.'", "It's just a flesh wound.", "I'm invinvible!", "All right, we'll call it a draw."]
              
          @property
          def member(self):
              print('next member is:')
              return self.members[0]
          
          @member.deleter
          def member(self):
              text = 'BLACK KNIGHT (loses {})\n-- {}'
              print(text.format(self.members.pop(0), self.phrases.pop(0)))
                 
  7. 處理屬性的重要屬性和函數
    1. 影響屬性處理方式的特殊屬性
      • __class__

        :對象所屬類的引用(即

        obj.__class__

        type(obj)

        的作用相同)。Python 的某些特殊方法,例如

        __getattr__

        ,隻在對象的類中尋找,而不在執行個體中尋找。
      • __dict__

        :一個映射,存儲對象或類的可寫屬性。有

        __dict__

        屬性的對象,任何時候都能随意設定新屬性。如果類有

        __slots__

        屬性,它的執行個體可能沒有

        __dict__

        屬性。
      • __slots__

        :類可以定義這個這屬性,限制執行個體能有哪些屬性。

        __slots__

        屬性的值是一個字元串組成的元組,指明允許有的屬性。如果

        __slots__

        中沒有

        '__dict__'

        ,那麼該類的執行個體沒有

        __dict__

        屬性,執行個體隻允許有指定名稱的屬性。

        __slots__

        屬性的值雖然可以是一個清單,但是最好始終使用元組,因為處理完類的定義體之後再修改

        __slots__

        清單沒有任何作用,是以使用可變的序列容易讓人誤解
    2. 處理屬性的内置函數
      • dir([object])

        :列出對象的大多數屬性
        • dir 函數能審查有或沒有

          __dict__

          屬性的對象
        • dir 函數不會列出

          __dict__

          屬性本身,但會列出其中的鍵
        • dir 函數也不會列出類的幾個特殊屬性,例如

          __mro__

          __bases__

          __name__

      • getattr(object, name[, default])

        :從 object 對象中擷取 name 字元串對應的屬性
        • 擷取的屬性可能來自對象所屬的類或超類
        • 如果沒有指定的屬性,getattr 函數抛出 AttributeError 異常,或者傳回 default 參數的值
      • hasattr(object, name)

        :如果 object 對象中存在指定的屬性,或者能以某種方式(例如繼承)通過 object 對象擷取指定的屬性,傳回 True
      • setattr(object, name, value)

        :把 object 對象指定屬性的值設為 value,前提是 object 對象能接受那個值
        • 這個函數可能會建立一個新屬性,或者覆寫現有的屬性
      • vars([object])

        :傳回 object 對象的

        __dict__

        屬性
        • 如果執行個體所屬的類定義了

          __slots__

          屬性,執行個體沒有

          __dict__

          屬性,那麼 vars 函數不能處理那個執行個體
        • 如果沒有指定參數,那麼 vars() 函數的作用與 locals() 函數一樣:傳回表示本地作用域的字典
    3. 處理屬性的特殊方法
      • 使用點号或内置的 getattr、hasattr 和 setattr 函數存取屬性都會觸發下述清單中相應的特殊方法
      • 但是,直接通過執行個體的

        __dict__

        屬性讀寫屬性不會觸發這些特殊方法
      • 如果需要,通常會使用這種方式跳過特殊方法
      • 要假定特殊方法從類上擷取,即便操作目标是執行個體也是如此。是以,特殊方法不會被同名執行個體屬性遮蓋
      • __delattr__(self, name)

        :隻要使用 del 語句删除屬性,就會調用這個方法
      • __dir__(self)

        :把對象傳給 dir 函數時調用,列出屬性
      • __getattr__(self, name)

        :僅當擷取指定的屬性失敗,搜尋過 obj、Class 和超類之後調用
      • __getattribute__(self, name)

        :嘗試擷取指定的屬性時總會調用這個方法,不過,尋找的屬性是特殊屬性或特殊方法時除外

        為了在擷取 obj 執行個體的屬性時不導緻無限遞歸,

        __getattribute__

        方法的實作要使用

        super().__getattribute__(obj, name)

      • __setattr__(self, name, value)

        :嘗試設定指定的屬性時總會調用這個方法,點号和 setattr 内置函數會觸發這個方法
  8. 總結:
    1. 詳解Python中的

      __init__

      __new__

    2. 在 Python 中,很多情況下類和函數可以互換。這不僅是因為 Python 沒有 new 運算符,還因為有特殊的

      __new__

      方法,可以把類變成工廠方法,生成不同類型的對象,或者傳回事先建構好的執行個體,而不是每次都建立一個新執行個體
    3. UAP:統一通路原則,Unifrom Access Principle
    4. new 方法接受的參數雖然也是和 init 一樣,但 init 是在類執行個體建立之後調用,而 new 方法正是建立這個類執行個體的方法

20. 屬性描述符

  1. 前言
    1. 描述符是對多個屬性運用相同存取邏輯的一種方式
    2. 描述符是實作了特定協定的類,這個協定包括

      __get__

      __set__

      __delete__

      方法
    3. 除了特性之外,使用描述符的 Python 功能還有方法及 classmethod 和 staticmethod 裝飾器
  2. 描述符示例:驗證屬性
    1. LineItem類第3版:一個簡單的描述符
      1. 描述符的用法是,建立一個執行個體,作為另一個類的類屬性
      2. 描述符類:實作描述符協定的類
      3. 托管類:把描述符執行個體聲明為類屬性的類
      4. 描述符執行個體:描述符類的各個執行個體,聲明為托管類的類屬性
      5. 托管執行個體:托管類的執行個體
      6. 儲存屬性:托管執行個體中存儲自身托管屬性的屬性
      7. 托管屬性:托管類中由描述符執行個體處理的公開屬性,值存儲在儲存屬性中。也就是說,描述符執行個體和儲存屬性為托管屬性建立了基礎
      8. 代碼
        class Quantity: #1
            def __init__(self, storage_name):
                self.storage_name = storage_name #2
            
            def __set__(self, instance, value): #3
                if value > 0:
                    instance.__dict__[self.storage_name] = value #4
                else:
                    raise ValueError('value must be > 0')
                    
        
        class LineItem:
            weight = Quantity('weight') #5
            price = Quantity('price') #6
            
            def __init__(self, description, weight, price): #7
                self.description = description
                self.weight = weight
                self.price = price
                
            def subtotal(self):
                return self.weight * self.price
                   
    2. LineItem類第4版:自動擷取儲存屬性的名稱
      1. 這裡可以使用内置的高階函數 getattr 和 setattr 存取值,無需使用

        instance.__dict__

        ,因為托管屬性和儲存屬性的名稱不同,是以把儲存屬性傳給 getattr 函數不會觸發描述符,不會像前面那樣出現無限遞歸
        class Quantity:
            __counter = 0 #1
            
            def __init__(self):
                cls = self.__class__ #2
                prefix = cls.__name__
                index = cls.__counter
                self.storage_name = '_{}#{}'.format(prefix, index) #3
                cls.__counter += 1 #4
                
            def __get__(self, instance, owner): #5
                return getattr(instance, self.storage_name) #6
            
            def __set__(self, instance, value): #6
                if value > 0:
                    setattr(instance, self.storage_name, value) #7
                else:
                    raise ValueError('value must be > 0')
                    
        class LineItem:
            weight = Quantity() #9
            price = Quantity()
            
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight
                self.price = price
                
            def subtotal(self):
                return self.weight * self.price
                   
      2. 為了給使用者提供内省和其他元程式設計技術支援,通過類通路托管屬性時,最好讓

        __get__

        方法傳回描述符執行個體
        def __get__(self, instance, owner): #5
            if instance is None:
            	return self
            else:
            	return getattr(instance, self.storage_name) #6
                   
      3. 描述符的正常用法:整潔的 LineItem 類;Quantity 描述符類現在位于導入的 model_v4c 子產品中
        import model_v4c as model
        
        
        class LineItem:
            weight = model.Quantity()
            price = model.Quantity()
            
            def __init__(self, description, weight, price):
                self.description = description
                self.weight = weight
                self.price = price
                
            def subtotal(self):
                return self.weight * self.price
                   
        1. Django 模型的字段就是描述符
      4. 使用特性工廠函數實作與上述示例中的描述符類相同的功能
        def quantity(): #1
            try:
                quantity.counter += 1 #2
            except AttributeError:
                quantity.counter = 0 #3
            storage_name = '_{}:{}'.format('quntity', quantity.counter) #4
            
            def qty_getter(instance): #5
                return getattr(instance, storage_name)
            
            def qty_setter(instance, value):
                if value > 0:
                    setattr(instance, storage_name, value)
                else:
                    raise ValueError('value must be > 0')
            
            return property(qty_getter, qty_setter)
                   
        1. 不能依靠類屬性在多次調用之間共享 counter,是以把它定義為 quantity 函數自身的屬性
        2. 作者更喜歡描述符類的方式,因為:
          1. 描述符類可以使用子類擴充;若想重用工廠函數中的代碼,除了複制粘貼,很難有其他方法
          2. 與使用函數屬性和閉包保持狀态相比,在類屬性和執行個體屬性中保持狀态更易于了解
        3. 從某種程度上來講,特性工廠函數模式較簡單,可是描述符類方式更易擴充,而且應用也更廣泛
    3. LineItem類第5版:一種新型描述符
      import abc
      
      class AutoStorage: #1
          __counter = 0
          
          
          def __init__(self):
              cls = self.__class__
              prefix = cls.__name__
              index = cls.__counter
              self.storage_name = '_{}#{}'.format(prefix, index)
              cls.__counter += 1
              
          def __get__(self, instance, owner):
              if instance is None:
                  return self
              else:
                  return getattr(instance, self.storage_name)
              
          def __set__(self, instance, value):
              setattr(instance, self.storage_name, value) #2
              
      
      class Validated(abc.ABC, AutoStorage): #3
          def __set__(self, instance, value):
              value = self.validate(instance, value) #4
              super().__set__(instance, value) #5
              
          @abc.abstractmethod
          def validate(self, instance, value): #6
              """return validated value or raise ValueError"""
              
      
      class Quantity(Validated): #7
          """a number greater than zero"""
          def validate(self, instance, value):
              if value <= 0:
                  raise ValueError('value must be > 0')
              return value
          
      
      class NonBlank(Validated):
          """a string with at least one ono-space character"""
          def validate(self, instance, value):
              value = value.strip()
              if len(value) == 0:
                  raise ValueError('value cannot be empty or blank')
              return value #8
          
      class LineItem:
          description = NonBlank()
          weight = Quantity()
          price = Quantity()
          
          def __init__(self, description, weight, price):
              self.description = description
              self.weight = weight
              self.price = price
              
          def subtotal(self):
              return self.weight * self.price
                 
      1. 這種描述符也叫覆寫型描述符,因為描述符的

        __set__

        方法使用托管執行個體中的同名屬性覆寫(即插手接管)了要設定的屬性
      2. 上述代碼的類圖
        《流暢的python》學習筆記及書評《流暢的python》學習筆記
  3. 覆寫型與非覆寫型描述符對比
    1. Python 存取屬性的方式特别不對等
      • 通過執行個體讀取屬性時,通常傳回的是執行個體中定義的屬性
      • 如果執行個體中沒有指定的屬性,那麼會擷取類屬性
      • 而為執行個體中的屬性指派時,通常會在執行個體中建立屬性,根本不影響類
    2. 覆寫型描述符
      1. 實作

        __set__

        方法的話,會覆寫對執行個體屬性的指派操作
      2. 特性也是覆寫型描述符:如果沒提供設值函數,property 類中的

        __set__

        方法會抛出 AttributeError 異常,指明那個屬性是隻讀的
      3. 代碼樣例
        def cls_name(obj_or_cls):
            cls = type(obj_or_cls)
            if cls is type:
                cls = obj_or_cls
            return cls.__name__.split('.')[-1]
        
        def display(obj):
            cls = type(obj)
            if cls is type:
                return '<class {}>'.format(obj.__name__)
            elif cls in [type(None), int]:
                return repr(obj)
            else:
                return '<{} object>'.format(cls_name(obj))
            
        def print_args(name, *args):
            pseudo_args = ', '.join(display(x) for x in args)
            print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
            
        class Overriding: #1
            """也稱資料描述符或強制描述符"""
            def __get__(self, instance, owner):
                print_args('get', self, instance, owner) #2
                
            def __set__(self, instance, value):
                print_args('set', self, instance, value)
                
        class OverridingNoGet: #3
            """沒有``__get__``方法的覆寫性描述符"""
            def __set__(self, instance, value):
                print_args('set', self, instance, value)
                
                
        class NonOverriding: #4
            """也稱非資料描述符或遮蓋型描述符"""
            def __get__(self, instance, owner):
                print_args('get', self, instance, owner)
                
        
        class Managed: #5
            over = Overriding()
            over_no_get = OverridingNoGet()
            non_over = NonOverriding()
            
            def spam(self): #6
                print('-> Managed.spam({})'.format(display(self)))
                   
    3. 沒有

      __get__

      方法的覆寫型描述符
      1. 執行個體屬性會遮蓋描述符,不過隻有讀操作是如此
      2. 讀取時,隻要有同名的執行個體屬性,描述符就會被遮蓋
    4. 非覆寫型描述符
      1. 沒有實作

        __set__

        方法的描述符是非覆寫型描述符
      2. obj 有個名為 non_over 的執行個體屬性,把 Managed 類的同名描述符屬性遮蓋掉
      3. 在上述幾個示例中,我們為幾個與描述符同名的執行個體屬性賦了值,結果依描述符中是否有

        __set__

        方法而有所不同
      4. 依附在類上的描述符無法控制為類屬性指派的操作。其實,這意味着為類屬性指派能覆寫描述符屬性
    5. 在類中覆寫描述符
      1. 不管描述符是不是覆寫型,為類屬性指派都能覆寫描述符
      2. 這是一種猴子更新檔技術
      3. 讀寫屬性的另一種不對等:
        1. 讀類屬性的操作可以由依附在托管類上定義有

          __get__

          方法的描述符處理
        2. 但是寫類屬性的操作不會由依附在托管類上定義有

          __set__

          方法的描述符處理
        3. 若想控制設定類屬性的操作,要把描述符依附在類的類上,即依附在元類上
  4. 方法是描述符
    1. 在類中定義的函數屬于綁定方法(bound method)
    2. 過托管類通路時,函數的

      __get__

      方法會傳回自身的引用
    3. 通過執行個體通路時,函數的

      __get__

      方法傳回的是綁定方法對象:一種可調用的對象,裡面包裝着函數,并把托管執行個體(例如 obj)綁定給函數的第一個參數(即 self),這與 functools.partial 函數的行為一緻
    4. function:函數;method:方法
    5. 函數會變成綁定方法,這是 Python 語言底層使用描述符的最好例證
  5. 描述符用法建議
    1. 使用特性以保持簡單
      • 内置的 property 類建立的其實是覆寫型描述符,

        __set__

        方法和

        __get__

        方法都實作了,即便不定義設值方法也是如此
      • 特性的

        __set__

        方法預設抛出 AttributeError: can’t set attribute
      • 是以建立隻讀屬性最簡單的方式是使用特性,這能避免下一條所述的問題
    2. 隻讀描述符必須有

      __set__

      方法
      • 如果使用描述符類實作隻讀屬性,要記住,

        __get__

        __set__

        兩個方法必須都定義
      • 否則,執行個體的同名屬性會遮蓋描述符
      • 隻讀屬性的

        __set__

        方法隻需抛出 AttributeError 異常,并提供合适的錯誤消息
    3. 用于驗證的描述符可以隻有

      __set__

      方法
      • 對僅用于驗證的描述符來說,

        __set__

        方法應該檢查 value 參數獲得的值,如果有效,使用描述符執行個體的名稱為鍵,直接在執行個體的

        __dict__

        屬性中設定
      • 這樣,從執行個體中讀取同名屬性的速度很快,因為不用經過

        __get__

        方法處理
    4. 僅有

      __get__

      方法的描述符可以實作高效緩存
      • 如果隻編寫了

        __get__

        方法,那麼建立的是非覆寫型描述符
      • 這種描述符可用于執行某些耗費資源的計算,然後為執行個體設定同名屬性,緩存結果
      • 同名執行個體屬性會遮蓋描述符,是以後續通路會直接從執行個體的

        __dict__

        屬性中擷取值,而不會再觸發描述符的

        __get__

        方法
    5. 非特殊的方法可以被執行個體屬性遮蓋
      • 由于函數和方法隻實作了

        __get__

        方法,它們不會處理同名執行個體屬性的指派操作
      • 特殊方法不受這個問題的影響
      • 釋器隻會在類中尋找特殊的方法,也就是說,repr(x) 執行的其實是

        x.__class__.__repr__(x)

        ,是以 x 的

        __repr__

        屬性對 repr(x) 方

        法調用沒有影響

      • 出于同樣的原因,執行個體的

        __getattr__

        屬性不會破壞正常的屬性通路規則
  6. 描述符的文檔字元串和覆寫删除操作
    1. 在描述符類中,實作正常的

      __get__

      和(或)

      __set__

      方法之外,可以實作

      __delete__

      方法,或者隻實作

      __delete__

      方法做到這一點
    2. python中函數和方法的差別
      • 函數:def定義的,或者内置的,或者lambda

        與類和執行個體無綁定關系的function都屬于函數(function)

      • 方法:跟類有關的,

        __init__

        def(self)

        與類和執行個體有綁定關系的function都屬于方法(method)

21. 類元程式設計

  1. 前言
    1. 類元程式設計是指在運作時建立或定制類的技藝
    2. 元類是類元程式設計最進階的工具:使用元類可以建立具有某種特質的全新類種,例如我們見過的抽象基類
    3. 除非開發架構,否則不要編寫元類
  2. 類工廠函數
    1. record_factory.py:一個簡單的類工廠函數
      def record_factory(cls_name, field_names):
          try:
              field_names = field_names.replace(',', ' ').split() #1
          except AttributeError: # 不能調用.replace或.split方法
              pass # 假定field_names本就是辨別符組成的序列
          field_names = tuple(field_names) #2
          
          def __init__(self, *args, **kwargs):
              attrs = dict(zip(self.__slots__, args))
              attrs.update(kwargs)
              for name, value in attrs.items():
                  setattr(self, name, value)
                  
          def __iter__(self): #4
              for name in self.__slots__:
                  yield getattr(self, name)
                  
          def __repr__(self): #5
              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
          ) #6
          
          return type(cls_name, (object,), cls_attrs) #7
                 
    2. 通常,我們把 type 視作函數,因為我們像函數那樣使用它。調用 type(my_object) 擷取對象所屬的類,作用與

      my_object.__class__

      相同
    3. type 當成類使用時,傳入三個參數可以建立一個類
    4. record_factory 函數建立的類,其執行個體有個局限——不能序列化,即不能使用 pickle 子產品裡的 dump/load 函數處理
  3. 定制描述符的類裝飾器
    1. 我們要在建立類時設定儲存屬性的名稱,用類裝飾器或元類可以做到這一點
    2. 類裝飾器與函數裝飾器非常類似,是參數為類對象的函數,傳回原來的類或修改後的類
    3. model_v6.py:一個類裝飾器
      def entity(cls): #1
          for key, attr in cls.__dict__.items(): #2
              if isinstance(attr, Validated): #3
                  type_name = type(attr).__name__
                  attr.strorage_name = '_{}#{}'.format(type_name, key) #4
          return cls #5
                 
    4. 類裝飾器有個重大缺點:隻對直接依附的類有效;這意味着,被裝飾的類的子類可能繼承也可能不繼承裝飾器所做的改動,具體情況視改動的方式而定
  4. 導入時和運作時比較
    1. 在導入時,解釋器會從上到下一次性解析完 .py 子產品的源碼,然後生成用于執行的位元組碼。如果句法有錯誤,就在此時報告。如果本地的

      __pycache__

      檔案夾中有最新的 .pyc 檔案,解釋器會跳過上述步驟,因為已經有運作所需的位元組碼了
    2. import 語句,它不隻是聲明,在程序中首次導入子產品時,還會運作所導入子產品中的全部頂層代碼——以後導入相同的子產品則使用緩存,隻做名稱綁定
    3. 那些頂層代碼可以做任何事,包括通常在“運作時”做的事,例如連接配接資料庫
    4. 是以,“導入時”與“運作時”之間的界線是模糊的:import 語句可以觸發任何“運作時”行為
    5. 解釋器在導入時定義頂層函數,但是僅當在運作時調用函數時才會執行函數的定義體
    6. 對類來說,情況就不同了:
      1. 在導入時,解釋器會執行每個類的定義體,甚至會執行嵌套類的定義體
      2. 執行類定義體的結果是,定義了類的屬性和方法,并建構了類對象
    7. 從這個意義上了解,類的定義體屬于“頂層代碼”,因為它在導入時運作
    8. 場景示範
      1. evalsupport.py
        print('<[100]> evalsupport module start')
        
        def deco_alpha(cls):
            print('<[200]> deco_alpha')
            def inner_1(self):
                print('<[300]> deco_alpha:inner_1')
                
            cls.method_y = inner_1
            return cls
        
        class MetaAleph(type):
            print('<[400]> MetaAleph body')
            
            def __init__(cls, name, bases, dic):
                print('<[500]> MetaAleph.__init__')
                
                def inner_2(self):
                    print('<[600]> MetaAleph.__init__:init_2')
                    
                cls.method_z = inner_2
                
        print('<[700]> evalsupport module end')
                   
      2. evaltime.py
        from evalsupport import deco_alpha
        
        print('<[1]> evaltime module start')
        
        
        class ClassOne():
            print('<[2]> ClassOne body')
            
            def __init__(self):
                print('<[3]> ClassOne.__init__')
                
            def __del__(self):
                print('<[4]> ClassOne.__del_-')
                
            def method_x(self):
                print('<[5]> ClassOne.method_x')
                
            class ClassTwo(object):
                print('<[6]> ClassTwo body')
                
        
        @deco_alpha
        class ClassThree():
            print('<[7]> ClassThree body')
            
            def method_y(self):
                print('<[8]> ClassThree.method_y')
                
        class ClassFour(ClassThree):
            print('<[9]> ClassFour body')
            
            def method_y(self):
                print('<[10]> ClassFour.method_y')
                
                
        if __name__ == '__main__':
            print('<[11]> ClassOne tests', 30 * '.')
            one = ClassOne()
            one.method_x()
            print('<[12]> ClassThree tests', 30 * '.')
            three = ClassThree()
            three.method_y()
            print('<[13]> CLassFour tests', 30 * '.')
            four = ClassFour()
            four.method_y()
            
        print('<[14]> evaltime module end')
                   
      3. 結果
        >>> import evaltime
        <[100]> evalsupport module start
        <[400]> MetaAleph body
        <[700]> evalsupport module end
        <[1]> evaltime module start
        <[2]> ClassOne body
        <[6]> ClassTwo body
        <[7]> ClassThree body
        <[200]> deco_alpha
        <[9]> ClassFour body
        <[14]> evaltime module end
                   
      4. 結論:
        1. 這個場景由簡單的 import evaltime 語句觸發
        2. 解釋器會執行所導入子產品及其依賴(evalsupport)中的每個類定義體
        3. 解釋器先計算類的定義體,然後調用依附在類上的裝飾器函數,這是合理的行為,因為必須先建構類對象,裝飾器才有類對象可處理
    9. 場景2:

      python evaltime.py

      1. 結果
        python evaltime.py
        <[100]> evalsupport module start
        <[400]> MetaAleph body
        <[700]> evalsupport module end
        <[1]> evaltime module start
        <[2]> ClassOne body
        <[6]> ClassTwo body
        <[7]> ClassThree body
        <[200]> deco_alpha
        <[9]> ClassFour body
        <[11]> ClassOne tests ..............................
        <[3]> ClassOne.__init__
        <[5]> ClassOne.method_x
        <[12]> ClassThree tests ..............................
        <[300]> deco_alpha:inner_1
        <[13]> CLassFour tests ..............................
        <[10]> ClassFour.method_y
        <[14]> evaltime module end
        <[4]> ClassOne.__del_-
                   
      2. 結論:
        1. 類裝飾器可能對子類沒有影響
        2. 當然,如果

          ClassFour.method_y

          方法使用

          super(...)

          調用

          ClassThree.method_y

          方法,我們便會看到裝飾器起作用,執行 inner_1 函數
  5. 元類基礎知識
    1. 元類是制造類的工廠,是用于建構類的類
    2. 根據 Python 對象模型,類是對象,是以類肯定是另外某個類的執行個體
    3. 預設情況下,Python 中的類是 type 類的執行個體
    4. type 是大多數内置的類和使用者定義的類的元類
    5. 左邊的示意圖強調 str、type 和 LineItem 是 object 的子類;右邊的示意圖則清楚地表明 str、object 和 LineItem 是 type 的執行個體
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
    6. object 類和 type 類之間的關系很獨特:object 是 type 的執行個體,而 type 是 object 的子類
    7. 所有類都直接或間接地是 type 的執行個體,不過隻有元類同時也是 type 的子類
    8. 元類(如 ABCMeta)從 type 類繼承了建構類的能力
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
    9. 所有類都是 type 的執行個體,但是元類還是 type 的子類,是以可以作為制造類的工廠
    10. 元類可以通過實作

      __init__

      方法定制執行個體。元類的

      __init__

      方法可以做到類裝飾器能做的任何事情,但是作用更大
    11. evaltime_meta.py:ClassFive 是 MetaAleph 元類的執行個體
      from evalsupport import deco_alpha
      from evalsupport import MetaAleph
      
      print('<[1]> evaltime_meta module start')
      
      @deco_alpha
      class ClassThree():
          print('<[2]> ClassThree body')
          
          def method_y(self):
              print('<[3]> ClassThree.method_y')
              
      class ClassFour(ClassThree):
          print('<[4]> ClassFour body')
          
          def method_y(self):
              print('<[5]> ClassFour.method_y')
      
      class ClassFive(metaclass=MetaAleph):
          print('<[6]> CLassFive body')
          
          def __init__(self):
              print('<[7]> ClassFive.__init__')
              
          def method_z(self):
              print('<[8]> ClassFive.method_z')
              
      class ClassSix(ClassFive):
          print('<[9]> ClassSix body')
          def method_z(self):
              print('<[10]> ClassSix.method_z')
              
              
      if __name__ == '__main__':
          print('<[11]> ClassThree tests', 30 * '.')
          three = ClassThree()
          three.method_y()
          print('<[12]> ClassFour tests', 30 * '.')
          four = ClassFour()
          four.method_y()
          print('<[13]> ClassFive tests', 30 * '.')
          five = ClassFive()
          five.method_z()
          print('<[14]> ClassSix tests', 30 * '.')
          six = ClassSix()
          six.method_z()
          
          
      print('<[15]> evaltime_meta module end')
                 
    12. 場景3:在 Python 控制台中以互動的方式導入 evaltime_meta.py 子產品
      >>> import evaltime_meta
      <[100]> evalsupport module start
      <[400]> MetaAleph body
      <[700]> evalsupport module end
      <[1]> evaltime_meta module start
      <[2]> ClassThree body
      <[200]> deco_alpha
      <[4]> ClassFour body
      <[6]> CLassFive body
      <[500]> MetaAleph.__init__
      <[9]> ClassSix body
      <[500]> MetaAleph.__init__
      <[15]> evaltime_meta module end
                 
    13. 場景4:在指令行中運作 evaltime_meta.py 子產品
      python evaltime_meta.py
      <[100]> evalsupport module start
      <[400]> MetaAleph body
      <[700]> evalsupport module end
      <[1]> evaltime_meta module start
      <[2]> ClassThree body
      <[200]> deco_alpha
      <[4]> ClassFour body
      <[6]> CLassFive body
      <[500]> MetaAleph.__init__
      <[9]> ClassSix body
      <[500]> MetaAleph.__init__
      <[11]> ClassThree tests ..............................
      <[300]> deco_alpha:inner_1
      <[12]> ClassFour tests ..............................
      <[5]> ClassFour.method_y
      <[13]> ClassFive tests ..............................
      <[7]> ClassFive.__init__
      <[600]> MetaAleph.__init__:init_2
      <[14]> ClassSix tests ..............................
      <[7]> ClassFive.__init__
      <[600]> MetaAleph.__init__:init_2
      <[15]> evaltime_meta module end
                 
    14. 編寫元類時,通常會把 self 參數改成 cls;在元類的

      __init__

      方法中,把第一個參數命名為 cls 能清楚地表明要建構的執行個體是類
    15. 裝飾器裝飾的類産生的效果不會影響其子類;而通過metaclass設定了原類的類,産生的效果會影響其子類
  6. 定制描述符的元類
    class EntityMeta(type):
        """元類,用于建立帶有驗證字段的業務實體"""
        def __init__(cls, name, bases, attr_dict):
            super().__init__(name, bases, attr_dict) #1
            for key, attr in attr_dict.items(): #2
                if isinstance(attr, Validated):
                    type_name = type(attr).__name__
                    attr.storage_name = '_{}#{}'.format(type_name, key)
                    
    class Entity(metaclass=EntityMeta): #3
        """帶有驗證字段的業務實體"""
               
    class LineItem(Entity):
    	...
               
  7. 元類的特殊方法

    __prepare__

    1. 元類或類裝飾器獲得映射時,屬性在類定義體中的順序已經丢失了(因為名稱到屬性的映射是字典)
    2. 這個問題的解決辦法是,使用 Python 3 引入的特殊方法

      __prepare__

    3. 這個特殊方法隻在元類中有用,而且必須聲明為類方法(即,要使用 @classmethod 裝飾器定義)
    4. 解釋器調用元類的

      __new__

      方法之前會先調用

      __prepare__

      方法,使用類定義體中的屬性建立映射
    5. __prepare__

      方法的第一個參數是元類,随後兩個參數分别是要建構的類的名稱和基類組成的元組,傳回值必須是映射
    6. 元類建構新類時,

      __prepare__

      方法傳回的映射會傳給

      __new__

      方法的最後一個參數,然後再傳給

      __init__

      方法
    7. 代碼
      import collections
      class EntityMeta(type):
          """元類,用于建立帶有驗證字段的業務實體"""
          
          @classmethod
          def __prepare__(cls, name, bases):
              return collections.OrderedDict()
          
          def __init__(cls, name, bases, attr_dict):
              super().__init__(name, bases, attr_dict)
              cls._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): #3
          """帶有驗證字段的業務實體"""
          
          @classmethod
          def field_names(cls):
              for name in cls._field_names:
                  yield name
                 
    8. 在現實世界中,架構和庫會使用元類協助程式員執行很多任務:
      • 驗證屬性
      • 一次把裝飾器依附到多個方法上
      • 序列化對象或轉換資料
      • 對象關系映射
      • 基于對象的持久存儲
      • 動态轉換使用其他語言編寫的類結構
  8. 類作為對象
    1. cls.__mro__

      :類的繼承關系和調用順序,方法解析順序,Method Resolution Order
    2. cls.__class__

      :執行個體調用

      __class__

      屬性時會指向該執行個體對應的類
    3. cls.__name__

      :類、 函數、方法、描述符或生成器對象的名稱
    4. cls.__bases__

      :由類的基類組成的元組
    5. cls.__qualname__

      :其值是類或函數的限定名稱,即從子產品的全局作用域到類的點分路徑
      《流暢的python》學習筆記及書評《流暢的python》學習筆記
    6. cls.__subclasses__()

      :這個方法傳回一個清單,包含類的直接子類
      • 這個方法的實作使用弱引用,防止在超類和子類之間出現循環引用
      • 子類在

        __bases__

        屬性中儲存指向超類的強引用
      • 這個方法傳回的清單中是記憶體裡現存的子類
    7. cls.mro()

      :建構類時,如果需要擷取儲存在類屬性

      __mro__

      中的超類元組,解釋器會調用這個方法;元類可以覆寫這個方法,定制要建構的類解析方法的順序
    8. dir(...)

      函數不會列出上述提到的任何一個屬性
  9. 小結:
    1. 元類可以定制類的層次結構。類裝飾器則不同,它隻能影響一個類,而且對後代可能沒有影響
    2. dunder

      :首尾有兩條下劃線的特殊方法和屬性的簡潔讀法(即把

      __len__

      讀成“dunder len”)
    3. ORM:Object-Relational Mapper(對象關系映射器)
    4. REPL:read-eval-print loop(讀取-求值-輸出循環)的簡稱
    5. 綁定方法(bound method):
      1. 通過執行個體通路的方法會綁定到那個執行個體上
      2. 方法其實是描述符,通路方法時,會傳回一個包裝自身的對象,把方法綁定到執行個體上
      3. 那個對象就是綁定方法
      4. 調用綁定方法時,可以不傳入 self 的值
      5. 例如,像

        my_method = my_obj.method

        這樣指派之後,可以通過

        my_method()

        調用綁定方法
    6. 并行指派(parallel assignment):使用類似 a, b = [c, d] 這樣的句法,把可疊代對象中的元素指派給多個變量,也叫解構指派。這是元組拆包的常見用途。
    7. 初始化方法(initializer):

      __init__

      方法更貼切的名稱(取代構造方法)。

      __init__

      方法的任務是初始化通過 self 參數傳入的執行個體。執行個體其實是由

      __new__

      方法建構的。
    8. 儲存屬性(storage attribute):托管執行個體中的屬性,用于存儲由描述符管理的屬性的值
    9. 在 Python 中,None 對象是單例
    10. 泛函數(generic function):以不同的方式為不同類型的對象實作相同操作的一組函數,

      functools.singledispatch

      ,在其他語言中,這叫多分派方法
    11. 非綁定方法(unbound method):直接通過類通路的執行個體方法沒有綁定到特定的執行個體上,是以把這種方法稱為“非綁定方法”
    12. 高階函數(higher-order function):以其他函數為參數的函數,例如 sorted、map 和 filter;或者,傳回值為函數的函數,例如 Python 中的裝飾器
    13. 猴子更新檔(monkey patching):在運作時動态修改子產品、類或函數,通常是添加功能或修正缺陷
      1. 猴子更新檔在記憶體中發揮作用,不會修改源碼,是以隻對目前運作的程式執行個體有效
      2. 為猴子更新檔破壞了封裝,而且容易導緻程式與更新檔代碼的實作細節緊密耦合,是以被視為臨時的變通方案,不是內建代碼的推薦方式
    14. 活性(liveness):異步系統、線程系統或分布式系統在“期待的事情終于發生”(即雖然期待的計算不會立即發生,但最終會完成)時展現出來的特性叫活性。如果系統死鎖了,活性也就沒有了。
    15. 可散列的(hashable)
      1. 在散列值永不改變,而且如果 a == b,那麼 hash(a) == hash(b) 也是 True 的情況下,如果對象既有

        __hash__

        方法,也有

        __eq__

        方法,那麼這樣的對象稱為可散列的對象
      2. 在内置的類型中,大多數不可變的類型都是可散列的
      3. 但是,僅當元組的每一個元素都是可散列的時,元組才是可散列的
    16. 描述符(descriptor):一個類,實作

      __get__

      __set__

      __delete__

      特殊方法中的一個或多個,其執行個體作為另一個類(托管類)的類屬性;描述符管理托管類中托管屬性的存取和删除,資料通常存儲在托管執行個體中
    17. 名稱改寫(name mangling):Python 解釋器在運作時自動把私有屬性

      __x

      重命名為

      _MyClass__x

    18. 平坦序列(flat sequence):這種序列類型存儲的是元素的值本身,而不是其他對象的引用
      1. 内置的類型中, str、bytes、bytearray、memoryview 和 array.array 是平坦序列
      2. 與之相對應的是容器序列,如:list、tuple 和 collections.deque
    19. 切片(slicing)
      1. 使用切片表示法生成序列的子集,例如

        my_sequence[2:6]

      2. 切片經常複制資料,生成新對象
      3. 然而,

        my_sequence[:]

        是對整個序列的淺複制
      4. memoryview 對象的切片雖是一個 memoryview 新對象,但會與源對象共享資料
    20. 弱引用(weak reference):一種特殊的對象引用方式,不計入訓示對象的引用計數。弱引用使用 weakref 子產品裡的某個函數和資料結建構立
    21. 蛇底式(snake_case):辨別符的一種命名約定,使用下劃線(_)連接配接單詞,例如

      run_until_complete

    22. 生成器函數(generator function):定義體中有 yield 關鍵字的函數。調用生成器函數得到的是生成器
    23. 屬性(attribute):在 Python 中,方法和資料屬性(即 Java 術語中的“字段”)都是屬性。方法也是屬性,隻不過恰好是可調用的對象(通常是函數,但也不一定)
    24. 特殊方法(special method):名稱特殊的方法,首尾各有兩條下劃線,例如

      __getitem__

    25. 統一通路原則(uniform access principle):Eiffel 語言之父 Bertrand Meyer 寫道:“不管服務是由存儲還是計算實作的,一個子產品提供的所有服務都應該通過統一的方式使用。”
      1. Python 中,可以使用特性和描述符實作統一通路原則
    26. 文檔字元串(docstring):documentation string 的簡稱
      1. 如果子產品、類或函數的第一個語句是字元串字面量,那個字元串會當作所在對象的文檔字元串,解釋器把那個字元串存儲在對象的

        __doc__

        屬性中
    27. 虛拟子類(virtual subclass):不繼承自超類,而是使用

      TheSuperClass.register(TheSubClass)

      注冊的類
    28. 序列化(serialization):把對象在記憶體中的結構轉換成便于存儲或傳輸的二進制或文本格式,而且以後可以在同一個系統或不同的系統中重建對象的副本。pickle 子產品能把任何 Python 對象序列化成二進制格式
    29. 鴨子類型(duck typing):多态的一種形式,在這種形式中,不管對象屬于哪個類,也不管聲明的具體接口是什麼,隻要對象實作了相應的方法,函數就可以在對象上執行操作
    30. 一等函數(first-class function):在語言中屬于一等對象的函數(即能在運作時建立,指派給變量,當作參數傳入,以及作為另一個函數的傳回值)。Python 中的函數都是一等函數
    31. 預激(prime,動詞):在協程上調用

      next(coro)

      ,讓協程向前運作到第一個 yield 表達式,準備好從後續的

      coro.send(value)

      調用中接收值
    32. 裝飾器(decorator)
      1. 一個可調用的對象 A,傳回另一個可調用的對象 B,在可調用的對象 C 的定義體之前使用句法 @A 調用
      2. Python 解釋器讀取這樣的代碼時,會調用 A©,把傳回的 B 綁定給之前賦予 C 的變量,也就是把 C的定義體換成 B
      3. 如果目标可調用對象 C 是函數,那麼 A 是函數裝飾器;如果 C 是類,那麼 A 是類裝飾器
  • ☁️ 我的CSDN:https://blog.csdn.net/qq_21579045
  • ❄️ 我的部落格園:https://www.cnblogs.com/lyjun/
  • ☀️ 我的Github:https://github.com/TinyHandsome
  • 🌈 我的bilibili:https://space.bilibili.com/8182822

碌碌謀生,謀其所愛。🌊 @李英俊小朋友