Python 中「一切皆物件」不只是一句口號,而是 CPython 實現的核心設計。理解 PyObject 是深入 Python 內部的第一步。

先備知識

本章目標

學完本章後,你將能夠:

  1. 理解 PyObject 結構
  2. 理解參考計數的工作原理
  3. 解釋「一切皆物件」的實現方式
  4. 觀察物件的記憶體佈局

【原理層】一切皆物件

什麼是「一切皆物件」?

在 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)

思考題

  1. 為什麼 CPython 選擇 -5 到 256 作為小整數快取的範圍?
  2. 如果參考計數是 Python 物件的核心,那多執行緒時會發生什麼問題?
  3. None 是單例,這是如何實現的?

實作練習

  1. 寫一個函式,計算一個巢狀資料結構的「真實」記憶體使用量
  2. 使用 ctypes 觀察 list 物件的內部結構
  3. 實驗不同大小的整數的 is 行為

延伸閱讀


下一章:記憶體管理與垃圾回收