一. 前言:程式有時候也要會照鏡子 #
寫 Python 久了,你一定會遇到這種情境:
- 拿到一個函式,但不知道它需要哪些參數
- 拿到一個物件,但不知道它有哪些方法
- 寫了 decorator,結果函式名稱、docstring、signature 都變成
wrapper - 想做一個小型 CLI,自動掃描模組裡可執行的 command
- 想產生 API 文件,但不想手動複製每個函式的簽名
這些問題的核心不是演算法,而是「程式如何在執行期間理解另一段程式」。
Python 內建的
inspect模組就是專門處理這件事。 它可以幫你讀函式簽名、列出物件成員、判斷某個值是不是 class/function/module、取得 docstring、拿到原始碼,甚至查看呼叫堆疊。 很多框架和開發工具背後都有類似技巧。 像 Typer 會讀 function signature 產生 CLI。 測試工具會讀 fixture 函式。 文件工具會掃描 module 和 class。 plugin 系統會找出符合規則的 object。 今天拍拍君就用幾個小例子,帶你把inspect變成日常工具箱裡的一支放大鏡。
二. 安裝:不用裝,它是標準庫 #
inspect 是 Python 標準庫。
直接 import:
import inspect
本文範例建議使用 Python 3.11+。
先建立一個檔案 demo_targets.py:
from __future__ import annotations
from dataclasses import dataclass
def greet(name: str, excited: bool = False) -> str:
"""Return a friendly greeting."""
suffix = "!" if excited else "."
return f"Hi, {name}{suffix}"
def fetch_article(slug: str, *, include_draft: bool = False) -> dict[str, str]:
"""Pretend to fetch one article."""
status = "draft" if include_draft else "published"
return {"slug": slug, "status": status}
@dataclass
class Article:
slug: str
title: str
def url(self) -> str:
return f"/learn/{self.slug}/"
class ArticleRepository:
def __init__(self) -> None:
self._items: dict[str, Article] = {}
def add(self, article: Article) -> None:
self._items[article.slug] = article
def get(self, slug: str) -> Article | None:
return self._items.get(slug)
接下來我們就拿它開刀。 不用怕,拍拍君下手很輕。
三. signature():讀懂函式需要什麼
#
inspect.signature() 是最常用的入口。
它會回傳一個 Signature 物件,裡面包含參數、預設值、annotation、回傳型別。
import inspect
from demo_targets import fetch_article, greet
sig = inspect.signature(greet)
print(sig)
print(sig.return_annotation)
for name, param in sig.parameters.items():
print(name)
print(" kind:", param.kind)
print(" default:", param.default)
print(" annotation:", param.annotation)
輸出會像這樣:
(name: 'str', excited: 'bool' = False) -> 'str'
str
name
kind: POSITIONAL_OR_KEYWORD
default: <class 'inspect._empty'>
annotation: str
excited
kind: POSITIONAL_OR_KEYWORD
default: False
annotation: bool
再看 fetch_article:
sig = inspect.signature(fetch_article)
for name, param in sig.parameters.items():
print(name, param.kind, param.default)
輸出:
slug POSITIONAL_OR_KEYWORD <class 'inspect._empty'>
include_draft KEYWORD_ONLY False
include_draft 是 keyword-only,因為函式定義裡有 *。
這類資訊很適合拿來做:
- CLI 自動參數產生
- plugin function 檢查
- API wrapper 驗證
- 簡單文件產生器 如果你看過 Typer 教學,就會發現 function signature 其實可以變成很自然的使用者介面。
四. Signature.bind():用 Python 規則檢查參數
#
Signature 不只能看。
它還能模擬 Python 呼叫函式時的參數綁定規則。
import inspect
from demo_targets import fetch_article
sig = inspect.signature(fetch_article)
bound = sig.bind("python-inspect", include_draft=True)
print(bound.arguments)
輸出:
{'slug': 'python-inspect', 'include_draft': True}
如果你傳錯位置參數:
sig.bind("python-inspect", True)
會得到:
TypeError: too many positional arguments
因為 include_draft 必須用 keyword。
也可以補上預設值:
bound = sig.bind("python-inspect")
bound.apply_defaults()
print(bound.arguments)
輸出:
{'slug': 'python-inspect', 'include_draft': False}
這比自己手刻 args / kwargs 檢查可靠很多。
Python 已經知道呼叫規則,就不要自己用字串硬猜了。
五. 實戰:寫一個迷你 command runner #
假設我們想做一個小任務工具。 規則很簡單:
- 模組裡名稱以
cmd_開頭的函式會變成 command - help 文字來自 docstring
- 參數檢查交給
Signature.bind()先建立tasks.py:
def cmd_build(target: str = "site") -> str:
"""Build one target."""
return f"building {target}"
def cmd_publish(slug: str, *, dry_run: bool = True) -> str:
"""Publish one article."""
mode = "dry run" if dry_run else "live"
return f"publishing {slug} ({mode})"
def helper_format_slug(value: str) -> str:
return value.strip().lower().replace(" ", "-")
接著建立 runner.py:
import inspect
import tasks
def discover_commands(module):
commands = {}
for name, value in inspect.getmembers(module, inspect.isfunction):
if name.startswith("cmd_"):
public_name = name.removeprefix("cmd_")
commands[public_name] = value
return commands
def print_help(commands):
for name, func in commands.items():
sig = inspect.signature(func)
doc = inspect.getdoc(func) or "No description."
print(f"{name}{sig}")
print(f" {doc}")
def run_command(commands, name, *args, **kwargs):
func = commands[name]
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
return func(*bound.args, **bound.kwargs)
commands = discover_commands(tasks)
print_help(commands)
print(run_command(commands, "build"))
print(run_command(commands, "publish", "python-inspect", dry_run=False))
執行:
python runner.py
輸出:
build(target: str = 'site') -> str
Build one target.
publish(slug: str, *, dry_run: bool = True) -> str
Publish one article.
building site
publishing python-inspect (live)
這就是框架的迷你版。 真實 CLI 框架還會處理型別轉換、錯誤格式、subcommand、shell completion。 但核心想法一樣:先讀函式結構,再把函式變成可操作介面。
六. getmembers():列出物件裡有什麼
#
inspect.getmembers(obj) 會列出物件的成員。
它回傳 (name, value) 的 list。
你可以加 predicate,讓它只回傳你要的種類。
import inspect
from demo_targets import ArticleRepository
methods = inspect.getmembers(ArticleRepository, inspect.isfunction)
for name, func in methods:
if not name.startswith("_"):
print(name, inspect.signature(func))
輸出:
add (self, article: 'Article') -> 'None'
get (self, slug: 'str') -> 'Article | None'
常用 predicate 有:
inspect.ismodule(value)
inspect.isclass(value)
inspect.isfunction(value)
inspect.ismethod(value)
inspect.iscoroutinefunction(value)
inspect.isgeneratorfunction(value)
這些判斷很適合寫 plugin loader。 例如:
import inspect
def discover_plugins(module):
plugins = []
for name, value in inspect.getmembers(module, inspect.isclass):
if getattr(value, "plugin_name", None):
plugins.append(value)
return plugins
這比 dir() 全部掃出來再亂試穩定多了。
七. getdoc() 和 unwrap():讓文件工具看懂你
#
直接讀 __doc__ 可以拿到 docstring。
但更推薦:
import inspect
from demo_targets import fetch_article
print(inspect.getdoc(fetch_article))
inspect.getdoc() 會整理縮排,也會處理繼承來的 docstring。
如果你要產生 CLI help 或 API 文件,它比直接讀 __doc__ 更順手。
再來看 decorator。
這段和 Python 裝飾器 有關,但角度不同。
那篇重點是怎麼寫 decorator。
這裡關心的是:被包裝後,工具還看不看得到原函式?
import functools
import inspect
def traced(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@traced
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
print(add.__name__)
print(inspect.signature(add))
print(inspect.getdoc(add))
print(inspect.unwrap(add))
因為有 functools.wraps,inspect 可以沿著 __wrapped__ 找回原函式。
如果你忘了 wraps,工具看到的通常只會是 wrapper(*args, **kwargs)。
所以拍拍君要再碎念一次:
寫 decorator 時,請加:
@functools.wraps(func)
這不是潔癖,是工具鏈真的會用到。
八. getsource():取得原始碼,但要有 fallback
#
inspect.getsource() 可以取得函式或類別的原始碼。
import inspect
from demo_targets import greet
print(inspect.getsource(greet))
輸出:
def greet(name: str, excited: bool = False) -> str:
"""Return a friendly greeting."""
suffix = "!" if excited else "."
return f"Hi, {name}{suffix}"
這很適合 debug helper、教學工具、notebook 輔助顯示。 但它不是萬能。 內建函式、C extension、互動式環境、動態產生的程式碼,都可能拿不到 source。 所以實務上要包起來:
def safe_getsource(value) -> str:
try:
return inspect.getsource(value)
except (OSError, TypeError):
return "# source unavailable"
工具可以聰明,但不能脆弱。
九. 實戰:自動產生 Markdown API 摘要 #
最後做一個小文件產生器。 規則:
- 只列 public function
- 顯示名稱、signature、docstring 第一行
- private helper 不輸出
建立
docgen.py:
import inspect
from types import ModuleType
def first_line_doc(value) -> str:
doc = inspect.getdoc(value)
if not doc:
return ""
return doc.splitlines()[0]
def render_function(name: str, func) -> str:
sig = inspect.signature(func)
summary = first_line_doc(func)
lines = [f"### `{name}{sig}`"]
if summary:
lines.extend(["", summary])
return "\n".join(lines)
def render_module(module: ModuleType) -> str:
parts = [f"# API: `{module.__name__}`"]
for name, value in inspect.getmembers(module, inspect.isfunction):
if name.startswith("_"):
continue
parts.extend(["", render_function(name, value)])
return "\n".join(parts)
使用:
import demo_targets
from docgen import render_module
print(render_module(demo_targets))
輸出會像:
# API: `demo_targets`
### `fetch_article(slug: 'str', *, include_draft: 'bool' = False) -> 'dict[str, str]'`
Pretend to fetch one article.
### `greet(name: 'str', excited: 'bool' = False) -> 'str'`
Return a friendly greeting.
這個版本很小,但已經可以看出 inspect 的價值。
當函式簽名改掉,文件會跟著變。
當 docstring 更新,摘要也會跟著變。
少一點手動同步,就少一點文件腐爛。
很樸素,但很有效。
十. 常見陷阱 #
第一,inspect 不是型別驗證器。
它能讀到 annotation,但不會幫你保證傳入值真的符合型別。
如果你需要資料驗證,請看 Pydantic 或自己寫明確規則。
第二,getmembers() 會列出很多東西。
包含 dunder member、繼承成員、import 進來的物件。
寫工具前要先定義清楚什麼叫 public API。
第三,getsource() 可能失敗。
一定要有 fallback。
第四,inspect.stack() 很方便,也很容易拖慢程式。
debug 可以用,核心業務邏輯和高頻 logging 要克制。
第五,decorator 要保留 metadata。
functools.wraps 會讓 inspect、文件工具、測試工具少踩很多坑。
結語 #
今天我們用 inspect 做了幾件事:
- 用
signature()讀函式參數與回傳型別 - 用
Signature.bind()套用 Python 的參數綁定規則 - 用
getmembers()掃描 module、class、function - 用
getdoc()取得整理過的 docstring - 用
unwrap()找回 decorator 背後的原函式 - 用
getsource()建立 debug / 文件工具inspect不是每天都會用。 但當你要寫 CLI、plugin loader、文件產生器、測試輔助、debug helper 時,它會突然變得很有用。 可以把它想成 Python 的放大鏡。 平常不用一直拿著。 但要看清楚程式結構時,它很可靠。 下次你想自動讀函式需要哪些參數,不要急著 parse 字串。 先問問inspect.signature()。 它比手刻 parser 可愛多了。