2.4 反射與 inspect 模組
2.4 反射與 inspect 模組
反射是程式檢視和修改自身結構的能力。Python 提供了強大的反射工具,讓你能夠動態地檢視物件、取得函式簽名、甚至修改執行中的程式。
先備知識
本章目標
學完本章後,你將能夠:
- 使用內建反射函式(getattr、setattr 等)
- 使用 inspect 模組分析物件
- 理解 getattr 和 getattribute 的差異
- 實作動態代理和 Mock 物件
【原理層】反射的基本概念
什麼是反射?
反射是程式在執行時檢視自身結構的能力:
1class MyClass:
2 def __init__(self, value):
3 self.value = value
4
5 def method(self):
6 return self.value * 2
7
8obj = MyClass(10)
9
10# 反射:程式檢視自己
11print(type(obj)) # <class 'MyClass'>
12print(obj.__class__.__name__) # MyClass
13print(dir(obj)) # ['__class__', 'method', 'value', ...]
14print(hasattr(obj, 'value')) # True
15print(getattr(obj, 'value')) # 10內建反射函式
1obj = MyClass(10)
2
3# getattr - 取得屬性
4value = getattr(obj, 'value') # 10
5default = getattr(obj, 'missing', 'default') # 'default'
6
7# setattr - 設定屬性
8setattr(obj, 'value', 20)
9print(obj.value) # 20
10
11# hasattr - 檢查屬性是否存在
12print(hasattr(obj, 'value')) # True
13print(hasattr(obj, 'missing')) # False
14
15# delattr - 刪除屬性
16delattr(obj, 'value')
17print(hasattr(obj, 'value')) # Falsevars() 和 dict
1class Person:
2 species = "Human" # 類別屬性
3
4 def __init__(self, name):
5 self.name = name # 實例屬性
6
7p = Person("Alice")
8
9# 實例的 __dict__:只包含實例屬性
10print(vars(p)) # {'name': 'Alice'}
11print(p.__dict__) # {'name': 'Alice'}
12
13# 類別的 __dict__:包含類別屬性和方法
14print(vars(Person)) # {'species': 'Human', '__init__': ..., ...}【設計層】getattr vs getattribute
getattr
當正常屬性查找失敗時呼叫:
1class FlexibleObject:
2 def __init__(self):
3 self.existing = "I exist"
4
5 def __getattr__(self, name):
6 return f"動態屬性: {name}"
7
8obj = FlexibleObject()
9print(obj.existing) # I exist(正常查找成功)
10print(obj.missing) # 動態屬性: missing(__getattr__ 被呼叫)getattribute
所有屬性存取都會呼叫(包括存在的屬性):
1class LoggedObject:
2 def __init__(self, value):
3 object.__setattr__(self, 'value', value)
4
5 def __getattribute__(self, name):
6 print(f"存取屬性: {name}")
7 return object.__getattribute__(self, name)
8
9obj = LoggedObject(10)
10print(obj.value)
11# 輸出:
12# 存取屬性: value
13# 10注意:在 __getattribute__ 中要避免無限遞迴:
1class Dangerous:
2 def __getattribute__(self, name):
3 # 錯誤!這會造成無限遞迴
4 # return self.data[name]
5
6 # 正確:使用 object.__getattribute__
7 data = object.__getattribute__(self, 'data')
8 return data.get(name)【實作層】inspect 模組
檢視物件類型
1import inspect
2
3def my_function():
4 pass
5
6class MyClass:
7 def method(self):
8 pass
9
10print(inspect.isfunction(my_function)) # True
11print(inspect.ismethod(my_function)) # False
12print(inspect.isclass(MyClass)) # True
13
14obj = MyClass()
15print(inspect.ismethod(obj.method)) # True取得函式簽名
1import inspect
2
3def greet(name: str, greeting: str = "Hello") -> str:
4 """打招呼函式"""
5 return f"{greeting}, {name}!"
6
7# 取得簽名
8sig = inspect.signature(greet)
9print(sig) # (name: str, greeting: str = 'Hello') -> str
10
11# 檢視參數
12for param_name, param in sig.parameters.items():
13 print(f" {param_name}:")
14 print(f" default: {param.default}")
15 print(f" annotation: {param.annotation}")
16 print(f" kind: {param.kind}")
17
18# 輸出:
19# name:
20# default: <class 'inspect._empty'>
21# annotation: <class 'str'>
22# kind: POSITIONAL_OR_KEYWORD
23# greeting:
24# default: Hello
25# annotation: <class 'str'>
26# kind: POSITIONAL_OR_KEYWORD取得原始碼
1import inspect
2
3def example_function():
4 """這是範例函式"""
5 x = 1
6 return x + 1
7
8# 取得原始碼
9print(inspect.getsource(example_function))
10# def example_function():
11# """這是範例函式"""
12# x = 1
13# return x + 1
14
15# 取得文件字串
16print(inspect.getdoc(example_function))
17# 這是範例函式
18
19# 取得檔案位置
20print(inspect.getfile(example_function))取得呼叫堆疊
1import inspect
2
3def inner():
4 stack = inspect.stack()
5 for frame_info in stack:
6 print(f"{frame_info.function} at {frame_info.lineno}")
7
8def outer():
9 inner()
10
11def main():
12 outer()
13
14main()
15# 輸出:
16# inner at 5
17# outer at 9
18# main at 12
19# <module> at 14【實戰】實用應用
動態呼叫方法
1class Calculator:
2 def add(self, a, b):
3 return a + b
4
5 def subtract(self, a, b):
6 return a - b
7
8 def multiply(self, a, b):
9 return a * b
10
11def execute(calc, operation, a, b):
12 """動態執行運算"""
13 method = getattr(calc, operation, None)
14 if method and callable(method):
15 return method(a, b)
16 raise ValueError(f"不支援的運算: {operation}")
17
18calc = Calculator()
19print(execute(calc, "add", 5, 3)) # 8
20print(execute(calc, "multiply", 5, 3)) # 15簡單的 Mock 物件
1class Mock:
2 """簡單的 Mock 物件"""
3
4 def __init__(self):
5 self._calls = []
6
7 def __getattr__(self, name):
8 def method(*args, **kwargs):
9 self._calls.append({
10 'method': name,
11 'args': args,
12 'kwargs': kwargs,
13 })
14 return Mock() # 支援鏈式呼叫
15
16 return method
17
18 @property
19 def call_count(self):
20 return len(self._calls)
21
22 def assert_called_with(self, method, *args, **kwargs):
23 for call in self._calls:
24 if (call['method'] == method and
25 call['args'] == args and
26 call['kwargs'] == kwargs):
27 return True
28 raise AssertionError(f"{method} 沒有以預期的參數被呼叫")
29
30# 使用
31mock = Mock()
32mock.save("data", flush=True)
33mock.load("file.txt")
34
35print(mock.call_count) # 2
36mock.assert_called_with("save", "data", flush=True) # OK動態代理
1class LoggingProxy:
2 """記錄所有方法呼叫的代理"""
3
4 def __init__(self, target):
5 object.__setattr__(self, '_target', target)
6 object.__setattr__(self, '_log', [])
7
8 def __getattr__(self, name):
9 attr = getattr(self._target, name)
10
11 if callable(attr):
12 def logged_method(*args, **kwargs):
13 self._log.append({
14 'method': name,
15 'args': args,
16 'kwargs': kwargs,
17 })
18 return attr(*args, **kwargs)
19 return logged_method
20
21 return attr
22
23 def __setattr__(self, name, value):
24 setattr(self._target, name, value)
25
26 def get_log(self):
27 return self._log
28
29# 使用
30class Database:
31 def query(self, sql):
32 return f"執行: {sql}"
33
34 def insert(self, table, data):
35 return f"插入到 {table}"
36
37db = LoggingProxy(Database())
38db.query("SELECT * FROM users")
39db.insert("users", {"name": "Alice"})
40
41for entry in db.get_log():
42 print(entry)
43# {'method': 'query', 'args': ('SELECT * FROM users',), 'kwargs': {}}
44# {'method': 'insert', 'args': ('users', {'name': 'Alice'}), 'kwargs': {}}自動生成 API 文件
1import inspect
2
3def generate_api_doc(cls):
4 """為類別生成簡單的 API 文件"""
5 lines = [f"# {cls.__name__}", ""]
6
7 if cls.__doc__:
8 lines.extend([cls.__doc__, ""])
9
10 lines.append("## Methods")
11 lines.append("")
12
13 for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
14 if name.startswith('_'):
15 continue
16
17 sig = inspect.signature(method)
18 doc = inspect.getdoc(method) or "No description"
19
20 lines.append(f"### `{name}{sig}`")
21 lines.append("")
22 lines.append(doc)
23 lines.append("")
24
25 return '\n'.join(lines)
26
27class UserService:
28 """用戶服務類別"""
29
30 def create_user(self, name: str, email: str) -> dict:
31 """建立新用戶
32
33 Args:
34 name: 用戶名稱
35 email: 電子郵件
36
37 Returns:
38 新建立的用戶資料
39 """
40 pass
41
42 def get_user(self, user_id: int) -> dict:
43 """取得用戶資料"""
44 pass
45
46print(generate_api_doc(UserService))【框架應用】
pytest 的反射使用
1# pytest 使用反射來發現測試
2import inspect
3
4def discover_tests(module):
5 """模擬 pytest 的測試發現"""
6 tests = []
7
8 for name, obj in inspect.getmembers(module):
9 if name.startswith('test_') and inspect.isfunction(obj):
10 tests.append(obj)
11
12 return tests
13
14# 使用
15def test_addition():
16 assert 1 + 1 == 2
17
18def test_subtraction():
19 assert 2 - 1 == 1
20
21def helper_function(): # 不會被發現
22 pass
23
24import sys
25tests = discover_tests(sys.modules[__name__])
26for test in tests:
27 print(f"發現測試: {test.__name__}")FastAPI 的參數解析
1import inspect
2from typing import get_type_hints
3
4def parse_function_params(func):
5 """模擬 FastAPI 的參數解析"""
6 sig = inspect.signature(func)
7 hints = get_type_hints(func)
8
9 params = []
10 for name, param in sig.parameters.items():
11 params.append({
12 'name': name,
13 'type': hints.get(name, 'Any'),
14 'default': None if param.default is inspect.Parameter.empty else param.default,
15 'required': param.default is inspect.Parameter.empty,
16 })
17
18 return params
19
20def create_user(name: str, age: int, active: bool = True):
21 pass
22
23params = parse_function_params(create_user)
24for p in params:
25 print(p)
26# {'name': 'name', 'type': <class 'str'>, 'default': None, 'required': True}
27# {'name': 'age', 'type': <class 'int'>, 'default': None, 'required': True}
28# {'name': 'active', 'type': <class 'bool'>, 'default': True, 'required': False}思考題
- 為什麼
hasattr可能會觸發副作用?什麼情況下應該避免使用? __getattr__和__getattribute__在效能上有什麼差異?- 如何用反射實作一個簡單的依賴注入框架?
實作練習
- 實作一個
@deprecated裝飾器,使用inspect記錄呼叫位置 - 建立一個簡單的 RPC 框架,根據方法名稱動態呼叫遠端方法
- 實作一個物件比較工具,使用反射比較兩個物件的所有屬性
延伸閱讀
上一章:類別裝飾器與動態類別 下一模組:模組三:CPython 內部機制