快轉到主要內容
  1. 教學文章/

Python functools 完全攻略:讓函式變得更強大的秘密武器

·7 分鐘· loading · loading · ·
Python Functools Cache Functional-Programming
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄

一、前言
#

嗨,這裡是拍拍君!🐍

你有沒有覺得 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)的。listdict 不行,要轉成 tuplefrozenset

三、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 有幾個優勢:

  1. 可讀性更好 — 一眼就知道是基於哪個函式
  2. funcargskeywords 屬性 — 可以內省
  3. 可以 pickle — lambda 不行
  4. 效能略好 — 少一層函式呼叫
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)

拍拍君的建議:如果有內建函式可以做到,就不要用 reducereduce 適合沒有對應內建函式的「摺疊」操作。

六、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 標準庫裡的寶藏模組。今天我們學了八個最實用的工具:

  1. lru_cache / cache — 自動快取,加速遞迴和重複計算
  2. partial — 預填參數,減少重複
  3. wraps — 寫裝飾器的標配
  4. reduce — 序列摺疊
  5. singledispatch — 型別分派(窮人的函式多載)
  6. cached_property — 只算一次的屬性
  7. total_ordering — 自動補齊比較運算子

拍拍君覺得,lru_cachewraps 是最常用的兩個——如果你今天只能記住兩個,就記它們吧!

下次見啦!Happy coding!🐍✨

延伸閱讀
#

相關文章

Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python
MLX 入門教學:在 Apple Silicon 上跑機器學習
·4 分鐘· loading · loading
Python Mlx Apple-Silicon Machine-Learning Deep-Learning
FastAPI:Python 最潮的 Web API 框架
·5 分鐘· loading · loading
Python Fastapi Web Api Async
Docker for Python:讓你的程式在任何地方都能跑
·6 分鐘· loading · loading
Python Docker Container Devops 部署
Streamlit:用 Python 快速打造互動式資料應用
·8 分鐘· loading · loading
Python Streamlit Data-Visualization Web-App Dashboard
Python Logging:別再 print 了,用正經的方式記錄日誌吧
·6 分鐘· loading · loading
Python Logging Debug 標準庫