Python 動態類型靈活但隱患:函數預期 str 卻收到 unicode(Py2)、int 卻傳入 float,測試覆蓋不足時極易上線崩潰。本文實現一個**基於裝飾器的運行時類型檢查器**,支持三級嚴格模式,零侵入業務代碼。
@accepts(int, int, int) 標註參數類型。NONE(0) 關閉、MEDIUM(1) 警告輸出 stderr、STRONG(2) 拋出 TypeError。__name__ 等)。import sys
# 嚴格級別常量
NONE, MEDIUM, STRONG = 0, 1, 2
def accepts(*types, **kw):
"""
參數類型檢查裝飾器。
:param types: 位置參數對應的預期類型元組
:param kw: 唯一合法關鍵字參數 debug=NONE|MEDIUM|STRONG
"""
debug = kw.get('debug', MEDIUM) # 默認 MEDIUM
if debug not in (NONE, MEDIUM, STRONG):
raise KeyError("debug must be 0, 1, or 2")
def decorator(f):
def new_f(*args):
if debug == NONE:
return f(*args)
# 參數數量檢查
if len(args) != len(types):
raise TypeError(
f"'{f.__name__}' expects {len(types)} arguments, got {len(args)}"
)
# 類型檢查
actual_types = tuple(type(arg) for arg in args)
if actual_types != types:
expected = ', '.join(t.__name__ for t in types)
actual = ', '.join(t.__name__ for t in actual_types)
msg = f"'{f.__name__}' expects ({expected}), but got ({actual})"
if debug == MEDIUM:
print >> sys.stderr, f"TypeWarning: {msg}"
elif debug == STRONG:
raise TypeError(msg)
return f(*args)
new_f.__name__ = f.__name__
new_f.__doc__ = f.__doc__
new_f.__dict__.update(f.__dict__)
return new_f
return decorator
# === 使用示例 ===
@accepts(int, int, int, debug=STRONG)
def average(x, y, z):
return (x + y + z) / 3
# average(5.5, 10, 15.0) # 拋出 TypeError: 'average' expects (int, int, int), but got (float, int, float)
# average(5, 10, 15) # 正常返回 10
accepts(*types, **kw) 返回真正裝飾器 decorator,實現參數化裝飾。type(arg) 獲取精確類型(不考慮繼承),tuple(map(type, args)) 構建實際類型元組與預期比較。__name__ 生成可讀信息。__name__、__doc__、__dict__(或用 functools.wraps 簡化)。debug,其他關鍵字拋出 KeyError。**kwargs 類型聲明。isinstance 更符合 Python 鴨子類型哲學(允許子類)。可添加 strict=True/False 選項。returns(type) 裝飾器或擴展 accepts 支持返回值聲明。NONE 模式。typing + mypy 靜態檢查、**Pydantic** 運行時驗證、**Typeguard** 庫(@typechecked)功能更完善。裝飾器實現的運行時類型檢查,以極低成本為動態語言添加「安全網」。雖非銀彈,但在缺乏靜態類型系統的項目中,能及時攔截顯而易見的類型錯誤,降低上線風險。**理解其原理,也能更好地評估現代類型檢查工具的優劣。**