3.1 PyObject 與物件模型
3.1 PyObject 與物件模型
Python 中「一切皆物件」不只是一句口號,而是 CPython 實現的核心設計。理解 PyObject 是深入 Python 內部的第一步。
先備知識
- 進階系列 模組二:元編程
- 基本的 C 語言知識(結構體、指標)
本章目標
學完本章後,你將能夠:
- 理解 PyObject 結構
- 理解參考計數的工作原理
- 解釋「一切皆物件」的實現方式
- 觀察物件的記憶體佈局
【原理層】一切皆物件
什麼是「一切皆物件」?
在 Python 中,所有東西都是物件:
1# 數字是物件
2x = 42
3print(type(x)) # <class 'int'>
4print(x.__class__) # <class 'int'>
5print(x.bit_length()) # 6(呼叫方法)
6
7# 函式是物件
8def hello():
9 pass
10print(type(hello)) # <class 'function'>
11print(hello.__name__) # hello
12
13# 類別是物件
14class MyClass:
15 pass
16print(type(MyClass)) # <class 'type'>
17
18# 甚至 type 本身也是物件
19print(type(type)) # <class 'type'>PyObject 結構
在 C 語言層面,所有 Python 物件都基於 PyObject 結構:
1// CPython 原始碼中的定義(簡化版)
2typedef struct _object {
3 Py_ssize_t ob_refcnt; // 參考計數
4 PyTypeObject *ob_type; // 型別指標
5} PyObject;每個 Python 物件在記憶體中至少包含這兩個欄位:
1┌─────────────────────────────────────┐
2│ PyObject │
3├─────────────────────────────────────┤
4│ ob_refcnt (參考計數) 8 bytes │
5│ ob_type (型別指標) 8 bytes │
6├─────────────────────────────────────┤
7│ ... 物件特定的資料 ... │
8└─────────────────────────────────────┘變長物件:PyVarObject
對於長度可變的物件(如 list、str),使用 PyVarObject:
1typedef struct {
2 PyObject ob_base;
3 Py_ssize_t ob_size; // 元素數量
4} PyVarObject;1┌─────────────────────────────────────┐
2│ PyVarObject │
3├─────────────────────────────────────┤
4│ ob_refcnt (參考計數) 8 bytes │
5│ ob_type (型別指標) 8 bytes │
6│ ob_size (元素數量) 8 bytes │
7├─────────────────────────────────────┤
8│ ... 元素資料 ... │
9└─────────────────────────────────────┘【設計層】參考計數
工作原理
Python 使用參考計數來追蹤物件的使用:
1import sys
2
3a = [1, 2, 3]
4print(sys.getrefcount(a)) # 2(a 本身 + getrefcount 的參數)
5
6b = a # 增加參考
7print(sys.getrefcount(a)) # 3
8
9del b # 減少參考
10print(sys.getrefcount(a)) # 2參考計數的增減時機
1# 增加參考計數的操作
2x = obj # 賦值
3container.append(obj) # 加入容器
4func(obj) # 作為參數傳遞
5
6# 減少參考計數的操作
7del x # 刪除變數
8x = other # 重新賦值
9container.remove(obj) # 從容器移除
10函式返回 # 區域變數離開作用域參考計數的優缺點
| 優點 | 缺點 |
|---|---|
| 即時回收 | 無法處理循環參考 |
| 可預測的記憶體使用 | 每次操作都要更新計數 |
| 簡單易理解 | 多執行緒下需要鎖(GIL 的原因之一) |
【實作層】觀察物件
使用 id() 觀察記憶體位址
1a = [1, 2, 3]
2b = a
3c = [1, 2, 3]
4
5print(id(a)) # 140234567890112
6print(id(b)) # 140234567890112(同一物件)
7print(id(c)) # 140234567890176(不同物件)
8
9print(a is b) # True
10print(a is c) # False
11print(a == c) # True(值相等)小整數快取
CPython 對 -5 到 256 的整數進行快取:
1a = 256
2b = 256
3print(a is b) # True(同一物件)
4
5a = 257
6b = 257
7print(a is b) # False(不同物件)
8
9# 但在同一行的情況可能會被編譯器優化
10a, b = 257, 257
11print(a is b) # True(編譯時優化)字串駐留(String Interning)
簡單的字串會被自動駐留:
1a = "hello"
2b = "hello"
3print(a is b) # True(駐留)
4
5a = "hello world"
6b = "hello world"
7print(a is b) # False(含空格,不駐留)
8
9# 手動駐留
10import sys
11a = sys.intern("hello world")
12b = sys.intern("hello world")
13print(a is b) # True使用 ctypes 觀察記憶體
1import ctypes
2import sys
3
4def get_refcount(obj_id):
5 """直接從記憶體讀取參考計數"""
6 return ctypes.c_long.from_address(obj_id).value
7
8a = [1, 2, 3]
9obj_id = id(a)
10
11print(f"sys.getrefcount: {sys.getrefcount(a)}")
12print(f"ctypes 直接讀取: {get_refcount(obj_id)}")
13# 注意:sys.getrefcount 會多 1(因為參數傳遞)觀察物件大小
1import sys
2
3# 基本物件大小
4print(sys.getsizeof(None)) # 16
5print(sys.getsizeof(True)) # 28
6print(sys.getsizeof(0)) # 28
7print(sys.getsizeof(1)) # 28
8print(sys.getsizeof(10**100)) # 72(大整數)
9
10# 容器大小(不包含元素)
11print(sys.getsizeof([])) # 56
12print(sys.getsizeof([1, 2, 3])) # 88
13print(sys.getsizeof({})) # 64
14
15# 注意:getsizeof 不遞迴計算
16nested = [[1, 2], [3, 4]]
17print(sys.getsizeof(nested)) # 只計算外層 list【深入】PyTypeObject
型別物件的結構
每個型別(int、str、list 等)都是 PyTypeObject 的實例:
1// 簡化版
2typedef struct _typeobject {
3 PyObject_VAR_HEAD
4 const char *tp_name; // 型別名稱
5 Py_ssize_t tp_basicsize; // 基本大小
6 Py_ssize_t tp_itemsize; // 元素大小(變長物件)
7
8 // 方法槽(slots)
9 destructor tp_dealloc; // 解構函式
10 reprfunc tp_repr; // __repr__
11 hashfunc tp_hash; // __hash__
12 // ... 更多方法槽
13} PyTypeObject;在 Python 中觀察型別資訊
1# 型別的基本資訊
2print(int.__name__) # int
3print(int.__basicsize__) # 28(64-bit 系統)
4
5# 方法解析順序
6class A: pass
7class B(A): pass
8class C(B): pass
9
10print(C.__mro__)
11# (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)
12
13# 型別的型別
14print(type(int)) # <class 'type'>
15print(type(type)) # <class 'type'>(type 是自己的實例)為什麼 is 比 == 快?
1# is 只比較記憶體位址(一個指標比較)
2# == 需要呼叫 __eq__ 方法(可能很複雜)
3
4import timeit
5
6a = [1, 2, 3]
7b = a
8c = [1, 2, 3]
9
10# is 比較(非常快)
11print(timeit.timeit('a is b', globals=globals(), number=1000000))
12# 約 0.02 秒
13
14# == 比較(需要比較內容)
15print(timeit.timeit('a == c', globals=globals(), number=1000000))
16# 約 0.05 秒【實戰】效能影響
避免不必要的物件建立
1# 不好:每次迭代都建立新的 tuple
2for i in range(1000):
3 point = (i, i * 2)
4
5# 好:如果結構固定,考慮使用 __slots__ 的類別
6class Point:
7 __slots__ = ['x', 'y']
8 def __init__(self, x, y):
9 self.x = x
10 self.y = y
11
12# 或者使用 namedtuple
13from collections import namedtuple
14Point = namedtuple('Point', ['x', 'y'])使用物件池
1# 對於頻繁建立的小物件,考慮重複使用
2class ObjectPool:
3 def __init__(self, factory, max_size=100):
4 self._factory = factory
5 self._pool = []
6 self._max_size = max_size
7
8 def acquire(self):
9 if self._pool:
10 return self._pool.pop()
11 return self._factory()
12
13 def release(self, obj):
14 if len(self._pool) < self._max_size:
15 self._pool.append(obj)
16
17# 使用
18pool = ObjectPool(list)
19lst = pool.acquire()
20lst.extend([1, 2, 3])
21# 使用完畢
22lst.clear()
23pool.release(lst)思考題
- 為什麼 CPython 選擇 -5 到 256 作為小整數快取的範圍?
- 如果參考計數是 Python 物件的核心,那多執行緒時會發生什麼問題?
None是單例,這是如何實現的?
實作練習
- 寫一個函式,計算一個巢狀資料結構的「真實」記憶體使用量
- 使用
ctypes觀察 list 物件的內部結構 - 實驗不同大小的整數的
is行為
延伸閱讀
下一章:記憶體管理與垃圾回收