反射是程式檢視和修改自身結構的能力。Python 提供了強大的反射工具,讓你能夠動態地檢視物件、取得函式簽名、甚至修改執行中的程式。

先備知識

本章目標

學完本章後,你將能夠:

  1. 使用內建反射函式(getattr、setattr 等)
  2. 使用 inspect 模組分析物件
  3. 理解 getattrgetattribute 的差異
  4. 實作動態代理和 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'))   # False

vars() 和 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}

思考題

  1. 為什麼 hasattr 可能會觸發副作用?什麼情況下應該避免使用?
  2. __getattr____getattribute__ 在效能上有什麼差異?
  3. 如何用反射實作一個簡單的依賴注入框架?

實作練習

  1. 實作一個 @deprecated 裝飾器,使用 inspect 記錄呼叫位置
  2. 建立一個簡單的 RPC 框架,根據方法名稱動態呼叫遠端方法
  3. 實作一個物件比較工具,使用反射比較兩個物件的所有屬性

延伸閱讀


上一章:類別裝飾器與動態類別 下一模組:模組三:CPython 內部機制