第 32 課 · 進階 · 19 分鐘

Python functools

用快取、部分應用和摺疊復用函式行為。

functoolslru_cachepartialreduce

查價函式一天跑幾千次,價格卻從不變。加個快取只要一行。快取什麼時候會悄悄變成 bug?

functools 收集了操作函式的工具。lru_cache 記住結果,partial 提前固定部分參數,reduce 把序列摺疊成一個值。它們都很小,卻能減少重複的包裝程式碼。 你還會觀察快取命中、綁定關鍵字參數,並按型別分派。

快取重複呼叫

lru_cache 會按參數儲存結果。參數相同的第二次呼叫可以直接回傳快取值,不再執行函式體。快取需要無上限增長時,可以使用 maxsize=None。

from functools import lru_cache

@lru_cache(maxsize=None)
def square(n):
    print("calculating", n)
    return n * n

print(square(3))
print(square(3))

計算一次,復用兩次

用 partial 固定參數

partial 會建立一個新的可呼叫物件,其中一些參數已經填好。剩下的參數稍後提供。它適合把通用函式改造成特定用途,例如從 power 構造 square。

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
print(square(5))

專門化的函式

用 reduce 摺疊

reduce 從左到右累積應用一個二元函式。可選的初始值用來播種累加器。操作天然是摺疊時可以使用它;邏輯複雜時,清楚的迴圈往往更好。

from functools import reduce

print(reduce(lambda total, n: total + n, [1, 2, 3], 0))

多個值,一個結果

只快取純函式和可雜湊參數

lru_cache 用參數當鍵,所以每個參數都必須可雜湊。它還假設函式是純的:相同輸入應得到相同輸出。cache_info() 可以看到命中和未命中;測試裡狀態改變時,可以用 cache_clear() 清掉舊結果。

from functools import lru_cache

calls = []

@lru_cache(maxsize=None)
def delivery_fee(city, weight):
    calls.append(city)
    return 4 if weight <= 2 else 7

print(delivery_fee("Taipei", 1.5))
print(delivery_fee("Taipei", 1.5))
print(delivery_fee("Tainan", 1.5))
print(len(calls), delivery_fee.cache_info().hits)

命中時不再執行函式體

用 partial 固定關鍵字參數

partial 可以預填位置參數或關鍵字參數,回傳一個新的可呼叫物件。它適合總是需要同一前綴或同一格式化器的回呼。原函式不受影響,partial 還會保留綁定參數的引用,便於檢查。

from functools import partial

def render(template, name, *, prefix=""):
    return f"{prefix}{template.format(name=name)}"

greet = partial(render, "Hello, {name}!", prefix="> ")
print(greet("Maya"))
print(greet.func.__name__, greet.args, greet.keywords)

帶固定上下文的新函式

用 singledispatch 按型別分派

singledispatch 根據第一個參數的型別選擇實現。通用函式是兜底,register() 添加更具體的處理器。它能把型別分支從長長的 if/elif 裡拿出來,但不能代替對不可信輸入的驗證。

from functools import singledispatch

@singledispatch
def label(value):
    return "other"

@label.register
def _(value: int):
    return f"int:{value}"

@label.register
def _(value: str):
    return f"str:{value}"

print(label(3))
print(label("tea"))
print(label(3.5))

一個名字,多個型別處理器

本課要點

  • lru_cache 按函式參數快取結果。
  • 結果依賴變化的外部狀態時,必須清除快取。
  • partial 固定參數並回傳新的可呼叫物件。
  • reduce 把可迭代物件摺疊成一個值。
  • 邏輯複雜時,短迴圈可能比 reduce 更清楚。
  • 快取純函式和可雜湊呼叫,partial 固定上下文,singledispatch 替代型別分支。

常見錯誤

  • 快取結果依賴可變全局狀態的函式。
  • 以為 partial 會立刻呼叫函式。
  • reduce 的累加器型別重要時,忘記提供初始值。
  • 快取讀取可變全局狀態的函式,卻以為結果會自動失效。
輪到你

用 lru_cache 快取 square 函式,再用 partial(power, exponent=2) 構造一個 square。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← collections列舉 →