一、前言 #
嗨,這裡是拍拍君!🐍
你有沒有覺得 Python 的函式已經很好用了?那你一定還沒認識 functools——Python 標準庫裡最被低估的模組之一。
functools 提供了一系列高階函式工具,可以讓你的程式碼更簡潔、更高效、更 Pythonic。從快取機制到函式裝飾、從偏函式到泛型分派,這個模組簡直是函式程式設計的瑞士刀 🔧
今天拍拍君帶你把 functools 裡最實用的六大工具全部搞懂!
二、lru_cache — 自動快取函式結果
#
問題:重複計算太浪費 #
來看經典的費波那契數列:
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
# fib(35) 要算好幾秒... 😢
fib(35) 會遞迴呼叫上億次,因為相同的計算被重複做了無數遍。
解法:加上 lru_cache
#
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(35)) # 9227465,瞬間完成!
print(fib(100)) # 354224848179261915075,一樣秒算
lru_cache 會把函式的輸入和輸出記住,下次遇到相同輸入直接回傳快取結果。LRU = Least Recently Used,超過 maxsize 時會自動淘汰最久沒用的快取。
進階用法 #
# 無限快取(不淘汰)
@lru_cache(maxsize=None)
def expensive_query(user_id):
# 模擬資料庫查詢
print(f"查詢 user {user_id}...")
return {"id": user_id, "name": f"User_{user_id}"}
# 第一次會真的查詢
result1 = expensive_query(42) # 印出 "查詢 user 42..."
# 第二次直接回傳快取
result2 = expensive_query(42) # 沒有印出任何東西!
# 查看快取統計
print(expensive_query.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=None, currsize=1)
# 清除快取
expensive_query.cache_clear()
Python 3.9+ 的 cache
#
如果你不需要限制快取大小,Python 3.9 提供了更簡潔的寫法:
from functools import cache
@cache
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(100))
# 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
@cache 等同於 @lru_cache(maxsize=None),語法更乾淨。
⚠️ 注意:
lru_cache的參數必須是可雜湊(hashable)的。list、dict不行,要轉成tuple或frozenset。
三、partial — 預先填入部分參數
#
問題:重複傳相同的參數 #
import json
# 每次都要寫 ensure_ascii=False, indent=2
print(json.dumps(data1, ensure_ascii=False, indent=2))
print(json.dumps(data2, ensure_ascii=False, indent=2))
print(json.dumps(data3, ensure_ascii=False, indent=2))
解法:用 partial 建立專用版本
#
from functools import partial
import json
# 建立預設參數的 JSON 序列化函式
json_dump_zh = partial(json.dumps, ensure_ascii=False, indent=2)
data = {"名字": "拍拍君", "技能": ["Python", "教學", "賣萌"]}
print(json_dump_zh(data))
輸出:
{
"名字": "拍拍君",
"技能": [
"Python",
"教學",
"賣萌"
]
}
實用場景:log 函式 #
from functools import partial
import logging
logger = logging.getLogger("pypy")
# 建立不同等級的 log 函式
debug = partial(logger.log, logging.DEBUG)
info = partial(logger.log, logging.INFO)
error = partial(logger.log, logging.ERROR)
info("拍拍君啟動了!")
error("糟糕,出錯了!")
partial vs lambda
#
你可能會想:「這不就是 lambda 嗎?」
# 這兩個功能一樣
json_dump_zh = partial(json.dumps, ensure_ascii=False, indent=2)
json_dump_zh = lambda data: json.dumps(data, ensure_ascii=False, indent=2)
但 partial 有幾個優勢:
- 可讀性更好 — 一眼就知道是基於哪個函式
- 有
func、args、keywords屬性 — 可以內省 - 可以 pickle — lambda 不行
- 效能略好 — 少一層函式呼叫
p = partial(json.dumps, ensure_ascii=False)
print(p.func) # <function dumps at 0x...>
print(p.keywords) # {'ensure_ascii': False}
四、wraps — 寫裝飾器的好朋友
#
問題:裝飾器會「吃掉」原函式的資訊 #
def timer(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 花了 {time.time() - start:.3f} 秒")
return result
return wrapper
@timer
def slow_function():
"""這是一個很慢的函式"""
import time
time.sleep(1)
print(slow_function.__name__) # "wrapper" 😱
print(slow_function.__doc__) # None 😱
裝飾器把原函式的名字和文件都蓋掉了!
解法:用 wraps 保留原函式資訊
#
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 花了 {time.time() - start:.3f} 秒")
return result
return wrapper
@timer
def slow_function():
"""這是一個很慢的函式"""
import time
time.sleep(1)
print(slow_function.__name__) # "slow_function" ✅
print(slow_function.__doc__) # "這是一個很慢的函式" ✅
print(slow_function.__wrapped__) # 原始函式的參考 ✅
💡
@wraps是寫裝飾器的標配,拍拍君建議你寫裝飾器時永遠都加上它。這不只是好習慣,也會讓 IDE 的自動補全和文件工具正確運作。
完整的裝飾器模板 #
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 前處理
result = func(*args, **kwargs)
# 後處理
return result
return wrapper
把這個模板記下來,以後寫裝飾器就不會出錯了!
五、reduce — 把序列「摺疊」成單一值
#
基本概念 #
reduce 會把一個二元函式依序套用在序列的元素上,最後得到一個值:
from functools import reduce
# 計算 1 * 2 * 3 * 4 * 5
product = reduce(lambda a, b: a * b, [1, 2, 3, 4, 5])
print(product) # 120
# 過程:
# step 1: 1 * 2 = 2
# step 2: 2 * 3 = 6
# step 3: 6 * 4 = 24
# step 4: 24 * 5 = 120
實用範例 #
from functools import reduce
# 合併多個字典
dicts = [
{"a": 1},
{"b": 2},
{"c": 3, "a": 99}, # a 會被覆蓋
]
merged = reduce(lambda d1, d2: {**d1, **d2}, dicts)
print(merged) # {'a': 99, 'b': 2, 'c': 3}
# 找最大公因數(GCD)
from math import gcd
numbers = [12, 18, 24, 36]
result = reduce(gcd, numbers)
print(result) # 6
# 用初始值
total = reduce(lambda acc, x: acc + x, [10, 20, 30], 100)
print(total) # 160(100 + 10 + 20 + 30)
reduce vs 內建函式
#
很多時候你不需要 reduce:
# ❌ 殺雞用牛刀
total = reduce(lambda a, b: a + b, numbers)
maximum = reduce(lambda a, b: a if a > b else b, numbers)
# ✅ 用內建函式
total = sum(numbers)
maximum = max(numbers)
拍拍君的建議:如果有內建函式可以做到,就不要用 reduce。reduce 適合沒有對應內建函式的「摺疊」操作。
六、singledispatch — Python 的函式多載
#
問題:根據型別做不同事情 #
# 傳統做法:一堆 if/elif
def format_value(value):
if isinstance(value, str):
return f'"{value}"'
elif isinstance(value, (int, float)):
return f"{value:,}"
elif isinstance(value, list):
return f"[{len(value)} items]"
elif isinstance(value, dict):
return f"{{{len(value)} keys}}"
else:
return str(value)
這種寫法會越來越長、越來越亂。
解法:用 singledispatch
#
from functools import singledispatch
@singledispatch
def format_value(value):
"""預設處理"""
return str(value)
@format_value.register(str)
def _(value):
return f'"{value}"'
@format_value.register(int)
@format_value.register(float)
def _(value):
return f"{value:,}"
@format_value.register(list)
def _(value):
return f"[{len(value)} items]"
@format_value.register(dict)
def _(value):
return f"{{{len(value)} keys}}"
# 使用
print(format_value("拍拍君")) # "拍拍君"
print(format_value(1234567)) # 1,234,567
print(format_value(3.14)) # 3.14
print(format_value([1, 2, 3])) # [3 items]
print(format_value({"a": 1})) # {1 keys}
Python 3.7+ type hint 語法 #
@format_value.register
def _(value: bytes):
return f"<{len(value)} bytes>"
print(format_value(b"hello")) # <5 bytes>
用 type hint 就不用在 register() 裡傳型別了,更乾淨!
singledispatchmethod — 用在類別裡
#
from functools import singledispatchmethod
class Formatter:
@singledispatchmethod
def format(self, value):
return str(value)
@format.register(str)
def _(self, value):
return f'"{value}"'
@format.register(int)
def _(self, value):
return f"{value:,}"
fmt = Formatter()
print(fmt.format("拍拍君")) # "拍拍君"
print(fmt.format(42)) # 42
七、cached_property — 只算一次的屬性
#
問題:每次存取都重新計算 #
class DataAnalyzer:
def __init__(self, data):
self.data = data
@property
def stats(self):
"""這個計算很耗時"""
print("計算中...")
return {
"mean": sum(self.data) / len(self.data),
"max": max(self.data),
"min": min(self.data),
}
analyzer = DataAnalyzer([1, 2, 3, 4, 5])
print(analyzer.stats) # 印出 "計算中..."
print(analyzer.stats) # 又印出 "計算中..." 😩
解法:用 cached_property
#
from functools import cached_property
class DataAnalyzer:
def __init__(self, data):
self.data = data
@cached_property
def stats(self):
"""只計算一次!"""
print("計算中...")
return {
"mean": sum(self.data) / len(self.data),
"max": max(self.data),
"min": min(self.data),
}
analyzer = DataAnalyzer([1, 2, 3, 4, 5])
print(analyzer.stats) # 印出 "計算中...",回傳結果
print(analyzer.stats) # 直接回傳快取,不再計算!✅
重設快取 #
# 刪除快取,下次存取會重新計算
del analyzer.stats
print(analyzer.stats) # 重新計算
⚠️
cached_property只在 Python 3.8+ 可用,而且需要類別的__dict__是可寫的(不能跟__slots__一起用)。
八、total_ordering — 自動補齊比較運算子
#
問題:定義比較要寫一堆方法 #
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
# 要實作 __eq__, __lt__, __le__, __gt__, __ge__...
# 好累 😫
解法:只寫兩個,其他自動補齊 #
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other):
return (self.major, self.minor, self.patch) == \
(other.major, other.minor, other.patch)
def __lt__(self, other):
return (self.major, self.minor, self.patch) < \
(other.major, other.minor, other.patch)
def __repr__(self):
return f"v{self.major}.{self.minor}.{self.patch}"
# 只定義了 __eq__ 和 __lt__,但所有比較都能用!
v1 = Version(1, 0, 0)
v2 = Version(1, 2, 0)
v3 = Version(2, 0, 0)
print(v1 < v2) # True
print(v2 >= v1) # True ← 自動產生的!
print(v3 > v2) # True ← 自動產生的!
# 還能排序
versions = [v3, v1, v2]
print(sorted(versions)) # [v1.0.0, v1.2.0, v2.0.0]
💡
total_ordering需要你定義__eq__加上__lt__、__le__、__gt__、__ge__其中一個,它會幫你補齊其餘的。
九、速查表 #
| 工具 | 用途 | 一句話 |
|---|---|---|
lru_cache |
快取函式結果 | 算過的不用再算 |
cache |
無限快取(3.9+) | lru_cache(maxsize=None) 的縮寫 |
partial |
預填參數 | 把通用函式變專用函式 |
wraps |
保留函式資訊 | 寫裝飾器必備 |
reduce |
摺疊序列 | 把 list 變成一個值 |
singledispatch |
函式多載 | 根據型別分派 |
cached_property |
快取屬性 | @property + 只算一次 |
total_ordering |
自動補齊比較 | 寫兩個得六個 |
結語 #
functools 是 Python 標準庫裡的寶藏模組。今天我們學了八個最實用的工具:
lru_cache/cache— 自動快取,加速遞迴和重複計算partial— 預填參數,減少重複wraps— 寫裝飾器的標配reduce— 序列摺疊singledispatch— 型別分派(窮人的函式多載)cached_property— 只算一次的屬性total_ordering— 自動補齊比較運算子
拍拍君覺得,lru_cache 和 wraps 是最常用的兩個——如果你今天只能記住兩個,就記它們吧!
下次見啦!Happy coding!🐍✨