閱讀筆記-Python精粹 來自專家的經驗精華

現代Python(3.6以上)開發的專家見解,來自《PYTHON ESSENTIAL REFERENCE》的作者 David Beazley

python-distilled

現代Python(3.6以上)開發的專家見解,來自《PYTHON ESSENTIAL REFERENCE》的作者 David Beazley

閱讀這本書主因為我想要對於Python細節有更紮實的了解。內容有很多細節是我新學到或者釐清的觀念。 在此進行筆記與紀錄。我會以書籍目錄作為架構,並條列式紀錄我覺得有趣的項目。

0. 序

  • 本書是在Python 3.9的時代撰寫的。它的核心概念是可以帶著這本書在荒島或者秘密金庫寫一些Python程式碼。重點是介紹一個現代化、經由整理匯集 (或精煉過的) 語言核心。
  • 保持程式的趣味性是很重要的,我希望我的書不僅能幫助你成為有生產力的Python程式設計師,還能捕捉到一些啟發人們運用Python來探索星空、在火星上駕駛直升機,以及在後院用水炮噴射松鼠的魔法。

1. Python 基礎

  • 位元操作運算子
    • x @ y 矩陣乘法
    • << 左移
    • ">>"右移
    • ^ XOR
    • ~ 位元否定
  • 一個程式中使用的所有值都是物件
  • 如果一個程式被作為主指令稿執行, __name__ 就會被設置為 __main__
  • 結語: Python: it fits your brain,簡單的基本功能或許就能完成問題

2. 運算子、運算式和資料操作

  • 可迭代物件運算
    • v1, v2, … = s 變數拆分
    • [a, *s, b] 、 (a, *s, b) 、 {a, *s, b} list, tuple, set值的展開(Expansion)
  • 拆分時,_ 變數用來表示要丟棄的、不重要的、用完即丟的值,例如:
(_, day, _), (hour, _, _) = datetime
  • list comprehension的變化
values = ['1', '2', '-4', 'n/a', '-3', '-5']
def toint(x)
    try:
        return int(x)
    except ValueError:
        return None
data2 = [toint(x) for x in values if toint(x) is not None]
# data2 = [1,2,-4,3,5]
# 也可以寫成
data3 = [v for x in values if (v:=toint(x)) is not None]
# data3 = [1,2,-4,3,5]
data4 = [v for x in values if (v:=toint(x)) is not None and v >= 0]
# data4 = [1,2,5]
  • generator expression: 在有需要時產生資料,可以大幅改善效能和記憶體使用率,在Generator章節時也會詳細說明
    • 此例子中產生器運算式沒有把整個檔案讀取並保存到記憶體中,當程式開始迭代時才會逐一被讀取,在這個過程中不會有整個檔案被載到記憶體中的時候,因此這是讀取1GB大檔案的高效率方法
f = open('data.txt')
lines = (t.strip() for t in f)
comments = (t for t in lines if t[0] == '#')
for c in comments:
    print(c)

3. 程式結構和流程控制

  • enumerate() 數值索引的內建函式
  • zip() 合併多個迭代結構的內建函式
  • 迴圈中行為:
    • break & continue: 只適用所執行的最內層迴圈
    • exception
    • else: 迴圈跑到完成時被執行
  • 例外處理的建議
    • 不要捕抓那些在程式碼中特定位置無法處理的例外 → 讓例外傳播到程式上層,用其他程式碼來處理
    • 抓錯誤時,盡量使except子句針對範圍縮小
    • 製作自己的例外來分辨 內建系統例外,以及刻意設置的例外
  • with 述句:
    • context manager的控制,例如檔案、鎖、連線

4. 物件 objects、型別 types以及協定 protocols

  • gc.collect() 把記憶體垃圾回收

  • copy:

    • 大部分情況不鼓勵使用deepcopy,遞迴拷貝所有包含物件是很慢的,除非要改變資料而不希望此改變影響到原始物件,但此情境有很多方法可以解決
    • deepcopy() 對於涉及系統或runtime狀態的物件如開啟的檔案、網路連線、執行緒、產生器等會失敗
  • Python所有物件都是一級(first-class)的。意思是所有可以被指定一個名稱的物件也都可以被當作資料。身為資料,物件可以儲存變數、作為引數傳遞、從函示中回傳、與其他物件做比較等

    • 例如字典裡可以儲存函式或類別,是消除if-elif-else 述句的常見技巧,也更彈性好維護
    _formats = {
        'text': TextFormatter,
        'csv': CSVFormatter,
        'html': HTMLFormatter
        }
        
    if format in formats:
        formatter = _formats[format]()
    else:
     raise RuntimeError('Bad format')
     
  • 整數在數學上是一種特殊的浮點數,因此整數對浮點數一無所知,但浮點數知道整數

  • 結語: Pythonic的關鍵

    • __repr__() 建立合適物件表徵
    • 以通用方式支援迭代
    • 使用with述句

5. 函式

  • 函式中的可變關鍵字引數 ** : 不限定數量的關鍵字引數會被放到字典中並引入函式,主要用來定義大量潛在的開放式組態選項(configuration options)的函式

  • *args, **kwargs : 常用來撰寫wrappers, 裝飾器, 代理器proxies

  • 建議減少為函式添加型別提示,因為他們會被直譯器忽略、也不會被檢查,除非你有積極運用會用到型別提示的程式碼檢查工具

  • 函式的例外處理成本是很昂貴的,如果注重效能,回傳None, False, -1或者其他值表示失敗,可能會更好

  • 突破遞迴限制的方法: 第六章產生器(generators)的例子

  • lambda: a small callback function

    • lambda 的替代方式: functools.partial()
    def func(a,b,c,d):
        print(a,b,c,d)
    f = partial(func,a=1,b=2) #固定a=1,b=2
    f(3,4) #func(1,2,3,4)
    f(10,20) #func(1,2,10,20)
    g  =partial(func,1,2,d=4) #固定a=1,b=2, d=4
    g(10) #func(1,2,10,4)
    • partial()lambda有類似的用途,但這兩者有一個重要的語意區別。使用 partial() 的時候,引數在部分函式(partial function)初次定義時就會被估算並繫結。使用零引數的lambda函式是在實際執行之後才會被估算和繫結,例如:
    def func(x,y):
        return x+y
    a=2
    b=3
    f=lambda:func(a,b)
    g=partial(func,a,b)
    a=10
    b=20
    f() # 30, 使用a, b 目前的值
    g() # 5, 使用a, b最初的值
    • 既然partial是完全估算過的, partial() 所創建的 callables (可呼叫物件)就能被序列化(serialized)為位元組(bytes)、儲存在檔案中、或甚至透過網際網路傳輸物件 (例如使用pickle標準程式庫模組)。這是使用lambda無法做到的事情。因此,在函式被四處傳遞(可能是在不同行程或不同機器上執行的Python直譯器之間)的應用中, partial()的用途更廣泛。
  • 把callback函式的回傳結果封裝在特殊實體,以便之後解開

    • 有利於型別檢查
    • 使用threads & processes等共時性concurrency的功能(例如thread pools) 時經常出現
  • 裝飾器的順序: @classmethod@staticmethod 之類的裝飾器通常必須被放置在最外層,因為和裝飾器回傳的值有關。

    • 同理,使用裝飾器工廠(decorator factory) 嵌套重複出現的裝飾器
@decorator1
@decoretor2
def func(x):
    pass
    
#裝飾器套用方式
func = decorator1(decorator2(func))

6. 產生器

  • 產生器 (generator) 和yield: 如果一個函式用了 yield 關鍵字,就定義了一個稱為產生器的物件,產生器主要用於產出要在迭代 (例如for迴圈) 中使用的值。所以他不會自己執行,只會用於迭代中。

  • 產生器如果在迭代中提前中斷,不會完全產生結果,所以需要像是try-final的context manager讓產生器終止時執行一些結果

    def countdown(n):
        print('Counting down from ', n)
        try:
            while n > 0:
                yield n
                n=n-1
        finally:
            print('Only made it to ', n)
  • 重複迭代產生器: 使用 __iter__()

class countdown:
    def __init__(self,start):
        self.start = start
    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1
  • 產生器的實際案例:

(1) 用於解決 pipelines和workflows有關的各種資料處理

# 在Python檔案的一個目錄中搜尋包含"spam"的所有註解
# 超深的巢狀控制流程,看到都暈了
import pathlib
import re

for path in pathlib.Path('.')rglob('*.py*):
    if path.exists():
        with path.open('rt', encoding='latin-1') as file:
            for line in file:
                m = re.match('.*(#.*)$', line)
                if m:
                    comment = m.group(1)
                    if 'spam' in comment:
                        print(comment)
                        
# 使用 yield 讓每個邏輯變得小型而獨立,使之更抽象 (sbstraction)
def get_paths(todir, pattern):
    for path in pathlib.Path(todir).rglob(pattern):
        if path.exists():
            yield path
def get_files(paths):
    for path in paths:
        with path.open('rt', encoding='latin=1') as file:
            yield file
def get_lines(files):
    for file in files:
        yield from file
def get_comments(lines):
    for line in lines:
        m = re.match('.*(#.*)$', line)
        if m:
            yield m.group(1)
def print_matching(lines, substring):
    for line in lines:
        if substring in line:
            print(substring)
paths = get_paths('.', '*.py')
files = get_files(paths)
lines = get_lines(files)
comments = get_comments(lines)
print_matching(comments, 'spam')

(2) 攤平巢狀結構

# 攤平巢狀串列的產生器函示
def flatten(items):
    for i in items:
        if isinstance(i, list):
        # 由於Python的遞迴限制,無法處理深層內嵌的巢狀結構
            yield from flatten(i)
        else:
            yield(i)    

# 使用stack以一種不同的方式驅動迭代,用此方法攤平不常見的幾百萬層深層資料結構,可以有效執行
def flatten(items):
    stack = [iter(items)]
    while stack:
        try:
            item = next(stack[-1])
            if isinstance(i, list):
                stack.append(iter(item))    
            else:
                yield(item)
        except StopIteration:
            stack.pop() 
    
    

7. 類別與物件導向程式設計

  • __repr__() 回傳一個字串,用以檢視一個物件
  • super() : 存取一個方法之前的定義
  • 藉由composition來避免繼承: 耦合較鬆散(loose coupling) → 如果物件不是parent class的特化版,或者只是做為構建其他東西的元件,就避免使用繼承
  • __setitem__ 內建型別修改資料內容常常不起作用,因為內建型別是用C語言實作,因此改使用 collections模組中有特殊的類別 UserDict , UserList , UserString 可以用來製作dict, list和str型別的安全子類別
  • @staticmethod : 靜態方法是一種普通的函式,只是剛好被定義在類別裡
  • @property 裝飾器被用來設定一個屬性的特性 (property attribute),並使用 getter/setter 進行獲取或設定 → 應用: 實作 read-only computed data attributes
  • 多重繼承與mixin:
    • 多重繼承的順序由MRO決定 (method resolution order)
    • 根據C3線性演化法 (linearization algorithm) 進行類別排序的繼承
    • 當類別使用 super() 時,會查看實體MRO以找到自己的位置
    • 設計方針:
      • 子類別會第一個被檢查,mixin類別共用一個共同的父類別,並由該父類別提供一個空的方法進行實作
      • mixin方法的實作應該有完全相同的函式特徵式(function signature)
      • 確保在所有地方使用了 super() ,如果有直接呼叫父類別的方法會破壞正確的呼叫串鏈
  • 用字典去實作不同物件的調度 (dispatch),更優雅與易於維護
  • 使用類別裝飾器讓程式碼更簡潔,但是啟動效能會比較差 → 使用 __init_subclass__() 來代替
  • 結語: 保持簡單

8. 模組與套件

  • 在函式中import 模組,看似會很慢,事實上因為查找模組只是一次字典查找,所以反對在函式中使用import主要是風格問題,如果有很少使用的模組,放到函式之中是可被接受的
  • 從一個模組中匯入選定的模組,事實上所有模組都會被載入並存在快取,但匯入選定的名稱會使整體風格一致,且不會讓命名空間混亂,增加可讀性
  • 建立套件 (packages) 的時候,應該總是創建適當的 __init__.py
    • __init__.py 可以控制套件的命名空間,此檔案的主要目的是建置和管理頂層套件命名空間的內容
    • 例如 collections模組實際上是一個套件, collections/__init__.py 檔案整合了幾個不同地方的定義,並將它們作為一個統一的命名空間呈現給使用者
  • python -m : 把一個子模組當作主指令稿來執行
    • python3 -m http.server
  • 部屬python套件: 簡約方法為使用 setuptools 或內建的 distutils 模組
  • 結語: 先從套件開始,養成從一開始就把所有程式當作一個套件來開發,是一個有意義的好習慣

9. 輸入與輸出

  • 標準程式庫模組,例如tempfile模組: 可以建立暫存檔案的模組
  • 結語: 請盡量使用內建功能