閱讀筆記-Python精粹 來自專家的經驗精華
現代Python(3.6以上)開發的專家見解,來自《PYTHON ESSENTIAL REFERENCE》的作者 David Beazley

現代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模組: 可以建立暫存檔案的模組
- 結語: 請盡量使用內建功能