這份文件示範如何在錯誤 helper 中「自動抓呼叫者函式名」,
並比較 `sys._getframe` 與 `inspect.currentframe`。
## 1) 問題背景
你有一個共用 helper,例如 `_raise_non_json_container_error(…)`,希望錯誤訊息自動顯示呼叫者名稱:
– `count_leaf_params()`
– `count_level_widths()`
而不是每次手動傳字串。
## 2) Call Stack 的 index 意義
兩種方案本質都在讀 call stack。
– `0`:目前函式自己
– `1`:直接呼叫者(caller)
– `2`:caller 的 caller
– `n`:更上層
如果索引超過 stack 深度,會出現例外(常見是 `ValueError`)。
## 3) 方案 A:sys._getframe
### 範例
import sys
def _raise_non_json_container_error(obj, func_name=None):
if func_name is None:
try:
func_name = sys._getframe(1).f_code.co_name
except (ValueError, AttributeError):
func_name = "<unknown>"
raise TypeError(
f"{func_name}() only accepts JSON-compatible containers (dict/list). "
f"Got non-JSON container: {type(obj).__name__}"
)### 優點
– 寫法短。
– 效能通常較好。
– 對「抓 caller 名稱」這種小用途很直接。
### 缺點
– `_getframe` 是 CPython 風格的低階 API,語意上比較偏內部細節。
– 可讀性對新手略差。
## 4) 方案 B:inspect.currentframe
### 範例(建議版,含 frame 清理)
import inspect
def _raise_non_json_container_error(obj, func_name=None):
frame = None
try:
if func_name is None:
# 先拿到「目前這個 helper 自己」的 frame(等同 sys._getframe(0))
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None:
# 往上一層到 caller(等同 sys._getframe(1))
func_name = frame.f_back.f_code.co_name
else:
func_name = "<unknown>"
raise TypeError(
f"{func_name}() only accepts JSON-compatible containers (dict/list). "
f"Got non-JSON container: {type(obj).__name__}"
)
finally:
# 避免 frame 參考循環
del frame### 優點
– 可讀性高,很多人直覺會先想到這個。
– 語意較「教科書化」。
### 缺點
– 稍微冗長。
– 效能通常略慢於 `sys._getframe`。
– 要注意 frame 參考循環(建議 `del frame`)。
## 5) 兩者對照總結
註:若兩邊都「只做一次性取值、不保存 frame 物件」,風險都很低。真正需要特別清理的是「把 frame 保存到變數/結構中」的情境。
## 6) 在你目前專案的建議
你的情境是「錯誤訊息 helper 自動帶 caller 名稱」,且路徑為熱路徑中的小工具函式:
– 如果你重視簡潔與實務效率:用 `sys._getframe(1)` 很合理。
– 如果你重視可讀性與教學一致性:用 `inspect.currentframe()` 也可以。
目前你已驗證 `sys` 寫法可正常輸出:
– `count_leaf_params()`
– `count_level_widths()`
在這個案子可直接採 `sys` 版本。
## 7) 最小可用測試片段
import sys
def _raise_non_json_container_error(obj, func_name=None):
if func_name is None:
func_name = sys._getframe(1).f_code.co_name
raise TypeError(f"{func_name}() got {type(obj).__name__}")
def foo(x):
if isinstance(x, (tuple, set)):
_raise_non_json_container_error(x)
try:
foo((1, 2))
except TypeError as e:
print(e)
# foo() got tuple## 8) 常見誤解
– 誤解:`1` 是固定魔法數字。
– 事實:`1` 代表「上一層 caller」,不是只能用 1。
– 誤解:`inspect` 看起來多寫了 `.f_back`,所以邏輯不同。
– 事實:邏輯相同。`inspect.currentframe()` 先拿到自己,再 `.f_back` 到 caller;等價於 `sys._getframe(1)`。
– 誤解:只能用 `inspect`。
– 事實:`sys` 和 `inspect` 都能做到,取捨在可讀性與簡潔度。
– 誤解:一定要手動傳 `func_name`。
– 事實:可設計成 optional,預設自動偵測,必要時手動覆蓋。
推薦hahow線上學習python: https://igrape.net/30afN