楔子
之前分析了那么久的虛擬機,多少會有點無聊,那么本次我們來介紹一個好玩的,看看如何修改 Python 解釋器的底層數據結構和運行時。了解虛擬機除了可以讓我們寫出更好的代碼之外,還可以對 Python 進行改造。舉個栗子:
是不是很有趣呢?通過 Python 內置的 ctypes 模塊即可做到,而具體實現方式我們一會兒說。所以本次我們的工具就是 ctypes 模塊(Python 版本為 3.8),需要你對它已經或多或少有一些了解,哪怕只有一點點也是沒關系的。
注意:本次介紹的內容絕不能用於生產環境,僅僅只是為了更好地理解 Python 虛擬機、或者做測試的時候使用,用於生產環境是絕對的大忌。
不可用於生產環境!!!
不可用於生產環境!!!
不可用於生產環境!!!
那么廢話不多說,下面就開始吧。
使用 Python 表示 C 的數據結構
Python 是用 C 實現的,如果想在 Python 的層面修改底層邏輯,那么我們肯定要能夠將 C 的數據結構用 Python 表示出來。而 ctypes 提供了大量的類,專門負責做這件事情,下面按照類型屬性分別介紹。
數值類型
C 語言的數值類型分為如下:
int:整型
unsigned int:無符號整型
short:短整型
unsigned short:無符號短整型
long:長整形
unsigned long:無符號長整形
long long:64 位機器上等同於 long
unsigned long long:64 位機器上等同於 unsigned long
float:單精度浮點型
double:雙精度浮點型
long double:看成是 double 即可
_Bool:布爾類型
ssize_t:等同於 long 或者 long long
size_t:等同於 unsigned long 或者 unsigned long long
和 Python 以及 ctypes 之間的對應關系如下:
下面來演示一下:
import ctypes
# 下面都是 ctypes 中提供的類,將 Python 中的數據傳進去,就可以轉換為 C 的數據
print(ctypes.c_int(1)) # c_long(1)
print(ctypes.c_uint(1)) # c_ulong(1)
print(ctypes.c_short(1)) # c_short(1)
print(ctypes.c_ushort(1)) # c_ushort(1)
print(ctypes.c_long(1)) # c_long(1)
print(ctypes.c_ulong(1)) # c_ulong(1)
# c_longlong 等價於 c_long,c_ulonglong 等價於 c_ulong
print(ctypes.c_longlong(1)) # c_longlong(1)
print(ctypes.c_ulonglong(1)) # c_ulonglong(1)
print(ctypes.c_float(1.1)) # c_float(1.100000023841858)
print(ctypes.c_double(1.1)) # c_double(1.1)
# 在64位機器上,c_longdouble等於c_double
print(ctypes.c_longdouble(1.1)) # c_double(1.1)
print(ctypes.c_bool(True)) # c_bool(True)
# 相當於c_longlong和c_ulonglong
print(ctypes.c_ssize_t(10)) # c_longlong(10)
print(ctypes.c_size_t(10)) # c_ulonglong(10)
而 C 的數據轉成 Python 的數據也非常容易,只需要在此基礎上調用一下 value 即可。
import ctypes
print(ctypes.c_int(1024).value) # 1024
print(ctypes.c_int(1024).value == 1024) # True
字符類型
C 語言的字符類型分為如下:
char:一個 ascii 字符或者 -128~127 的整型
wchar:一個 unicode 字符
unsigned char:一個 ascii 字符或者 0~255 的一個整型
和 Python 以及 ctypes 之間的對應關系如下:
舉個栗子:
import ctypes
# 必須傳遞一個字節(里面是 ascii 字符),或者一個 int,來代表 C 里面的字符
print(ctypes.c_char(b"a")) # c_char(b'a')
print(ctypes.c_char(97)) # c_char(b'a')
# 和 c_char 類似,但是 c_char 既可以傳入單個字節、也可以傳整型
# 而這里的 c_byte 和則要求必須傳遞整型
print(ctypes.c_byte(97)) # c_byte(97)
# 傳遞一個 unicode 字符,當然 ascii 字符也是可以的,並且不是字節形式
print(ctypes.c_wchar("憨")) # c_wchar('憨')
# 同樣只能傳遞整型,
print(ctypes.c_ubyte(97)) # c_ubyte(97)
數組
下面看看如何構造一個 C 中的數組:
import ctypes
# C 里面創建數組的方式如下:int a[5] = {1, 2, 3, 4, 5}
# 使用 ctypes 的話
array = (ctypes.c_int * 5)(1, 2, 3, 4, 5)
# (ctypes.c_int * N) 等價於 int a[N],相當於構造出了一個類型,然后再通過類似函數調用的方式指定數組的元素即可
# 這里指定元素的時候直接輸入數字即可,會自動轉成 C 中的 int,當然我們也可以使用 c_int 手動包裝
print(len(array)) # 5
print(array) # <__main__.c_int_Array_5 object at 0x7f96276fd4c0>
for i in range(len(array)):
print(array[i], end=" ") # 1 2 3 4 5
print()
array = (ctypes.c_char * 3)(97, 98, 99)
print(list(array)) # [b'a', b'b', b'c']
我們看一下數組在 Python 里面的類型,因為數組存儲的元素類型為 c_int、數組長度為 5,所以這個數組在 Python 里面的類型就是 c_int_Array_5,而打印的時候則顯示為 c_int_Array_5 的實例對象。我們可以調用 len 方法獲取長度,也可以通過索引的方式去指定的元素,並且由於內部實現了迭代器協議,我們還可以使用 for 循環去遍歷,或者使用 list 直接轉成列表等等,都是可以的。
結構體
結構體應該是 C 里面最重要的結構之一了,假設 C 里面有這樣一個結構體:
typedef struct {
int field1;
float field2;
long field3[5];
} MyStruct;
要如何在 Python 里面表示它呢?
import ctypes
# C 中的結構體在 Python 里面顯然通過類來實現,但是這個類一定要繼承 ctypes.Structure
class MyStruct(ctypes.Structure):
# 結構體的每一個成員對應一個元組,第一個元素為字段名,第二個元素為類型
# 然后多個成員放在一個列表中,並用變量 _fields_ 指定
_fields_ = [
("field1", ctypes.c_int),
("field2", ctypes.c_float),
("field3", (ctypes.c_long * 5)),
]
# field1、field2、field3 就類似函數參數一樣,可以通過位置參數、關鍵字參數指定
s = MyStruct(field1=ctypes.c_int(123),
field2=ctypes.c_float(3.14),
field3=(ctypes.c_long * 5)(11, 22, 33, 44, 55))
print(s) # <__main__.MyStruct object at 0x7ff9701d0c40>
print(s.field1) # 123
print(s.field2) # 3.140000104904175
print(s.field3) # <__main__.c_long_Array_5 object at 0x7ffa3a5f84c0>
就像實例化一個普通的類一樣,然后也可以像獲取實例屬性一樣獲取結構體成員。這里獲取之后會自動轉成 Python 中的數據,比如 c_int 類型會自動轉成 int,c_float 會自動轉成 float,而數組由於 Python 沒有內置,所以直接打印為 "c_long_Array_5 的實例對象"。
指針
指針是 C 語言靈魂,而且絕大部分的 Bug 也都是指針所引起的,那么指針類型在 Python 里面如何表示呢?非常簡單,通過 ctypes.POINTER 即可表示 C 的指針類型,比如:
C 中的 int *,在 Python 里面就是 ctypes.POINTER(c_int)
C 中的 float *,在 Python 里面就是 ctypes.POINTER(c_float)
from ctypes import *
class MyStruct(Structure):
_fields_ = [
("field1", POINTER(c_long)),
("field2", POINTER(c_double)),
]
所以通過 POINTER(類型) 即可表示對應類型的指針,而獲取指針則是通過 pointer 函數。
# 在 C 里面就相當於,long a = 1024; long *p = &a;
p = pointer(c_long(1024))
print(p) # <__main__.LP_c_long object at 0x7ff3639d0dc0>
print(p.__class__) # <class '__main__.LP_c_long'>
# pointer 可以獲取任意類型的指針
print(pointer(c_float(3.14)).__class__) # <class '__main__.LP_c_float'>
print(pointer(c_double(2.71)).__class__) # <class '__main__.LP_c_double'>
同理,我們也可以通過指針獲取指向的值,也就是對指針進行解引用。
from ctypes import *
p = pointer(c_long(123))
# 調用 contents 即可獲取指向的值,相當於對指針進行解引用
print(p.contents) # c_long(123)
print(p.contents.value) # 123
# 如果對 p 再使用一次 pointer 函數,那么相當於獲取 p 的指針
# 此時相當於二級指針 long **,所以類型為 LP_LP_c_long
print(pointer(pointer_p)) # <__main__.LP_LP_c_long object at 0x7fe6121d0bc0>
# 三級指針,類型為 LP_LP_LP_c_long
print(pointer(pointer(pointer_p))) # <__main__.LP_LP_LP_c_long object at 0x7fb2a29d0bc0>
# 三次解引用,獲取對應的值
print(pointer(pointer(pointer_p)).contents.contents.contents) # c_long(123)
print(pointer(pointer(pointer_p)).contents.contents.contents.value) # 123
總的來說,還是比較好理解的。但我們知道,在 C 中數組等於數組首元素的地址,我們除了傳一個指針過去之外,傳數組也是可以的。
from ctypes import *
class MyStruct(Structure):
_fields_ = [
("field1", POINTER(c_long)),
("field2", POINTER(c_double)),
]
# 結構體也可以先創建,再實例化成員
s = MyStruct()
s.field1 = pointer(c_long(1024))
s.field2 = (c_double * 3)(3.14, 1.732, 2.71)
數組在作為參數傳遞的時候會退化為指針,所以此時數組的長度信息就丟失了,使用 sizeof 計算出來的結果就是一個指針的大小。因此將數組作為參數傳遞的時候,應該將當前數組的長度信息也傳遞過去,否則可能會訪問非法的內存。
然后在 C 里面還有 char *、wchar_t *、void *,這些指針在 ctypes 里面專門提供了幾個類與之對應。
from ctypes import *
# c_char_p 就是 c 里面字符數組了,其實我們可以把它看成是 Python 中的 bytes 對象
# char *s = "hello world";
# 那么這里面也要傳遞一個 bytes 類型的字符串,返回一個地址
print(c_char_p(b"hello world")) # c_char_p(140451925798832)
# 直接傳遞一個字符串,同樣返回一個地址
print(c_wchar_p("古明地覺")) # c_wchar_p(140451838245008)
函數
最后看一下如何在 Python 中表示 C 的函數,首先 C 的函數可以有多個參數,但只有一個返回值。舉個栗子:
long add(long *a, long *b) {
return *a + *b;
}
這個函數接收兩個 long *、返回一個 long,那么這種函數類型要如何表示呢?答案是通過 ctypes.CFUNCTYPE。
from ctypes import *
# 第一個參數是函數的返回值類型,然后函數的參數寫在后面,有多少寫多少
# 比如這里的函數返回一個 long,接收兩個 long *,所以就是
t = CFUNCTYPE(c_long, POINTER(c_long), POINTER(c_long))
# 如果函數不需要返回值,那么寫一個 None 即可
# 然后得到一個類型 t,此時的類型 t 就等同於 C 中的 typedef long (*t)(long*, long*);
# 定義一個 Python 函數,a、b 為 long *,返回值為 c_long
def add(a, b):
return a.contents.value + b.contents.value
# 將我們自定義的函數傳進去,就得到了 C 語言可以識別的函數
c_add = t(add)
print(c_add) # <CFunctionType object at 0x7fa52fa29040>
print(
c_add(pointer(c_long(22)),
pointer(c_long(33)))
) # 55
類型轉換
以上就是 C 中常見的數據結構,然后再說一下類型轉化,ctypes 提供了一個 cast 函數,可以將指針的類型進行轉化。
from ctypes import *
# cast 的第一個參數接收的必須是某種指針的 ctypes 對象,第二個參數是 ctypes 指針類型
# 這里相當於將 long * 轉成了 float *
p1 = pointer(c_long(123))
p2 = cast(p1, POINTER(c_float))
print(p2) # <__main__.LP_c_float object at 0x7f91be201dc0>
print(p2.contents) # c_float(1.723597111119525e-43)
指針在轉換之后,還是引用相同的內存塊,所以整型指針轉成浮點型指針之后,打印的結果亂七八糟。當然數組也可以轉化,我們舉個栗子:
from ctypes import *
t1 = (c_int * 3)(1, 2, 3)
# 將 int * 轉成 long *
t2 = cast(t1, POINTER(c_long))
print(t2[0]) # 8589934593
原來數組元素是 int 類型(4 字節),現在轉成了 long(8 字節),但是內存塊並沒有變。因此 t2 獲取元素時會一次性獲取 8 字節,所以 t1[0] 和 t1[1] 組合起來等價於 t2[0]。
from ctypes import *
t1 = (c_int * 3)(1, 2, 3)
t2 = cast(t1, POINTER(c_long))
print(t2[0]) # 8589934593
print((2 << 32 & 0xFFFFFFFFFFFFFFFF) + (1 & 0xFFFFFFFFFFFFFFFF)) # 8589934593
模擬底層數據結構,觀察運行時表現
我們說 Python 的對象本質上就是 C 的 malloc 函數為結構體實例在堆區申請的一塊內存,比如整數是 PyLongObject、浮點數是 PyFloatObject、列表是 PyListObject,以及所有的類型都是 PyTypeObject 等等。那么在介紹完 ctypes 的基本用法之后,下面就來構造這些數據結構來觀察 Python 對象在運行時的表現。
浮點數
這里先說浮點數,因為浮點數比整數要簡單,先來看看底層的定義。
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
除了 PyObject 這個公共的頭部信息之外,只有一個額外的 ob_fval,用於存儲具體的值,而且直接使用的 C 中的 double。
from ctypes import *
class PyObject(Structure):
"""PyObject,所有對象底層都會有這個結構體"""
_fields_ = [
("ob_refcnt", c_ssize_t),
("ob_type", c_void_p) # 類型對象一會說,這里就先用 void * 模擬
]
class PyFloatObject(PyObject):
"""定義 PyFloatObject,繼承 PyObject"""
_fields_ = [
("ob_fval", c_double)
]
# 創建一個浮點數
f = 3.14
# 構造 PyFloatObject,可以通過對象的地址進行構造
# float_obj 就是浮點數 f 在底層的表現形式
float_obj = PyFloatObject.from_address(id(f))
print(float_obj.ob_fval) # 3.14
# 修改一下
print(f"f = {f},id(f) = {id(f)}") # f = 3.14,id(f) = 140625653765296
float_obj.ob_fval = 1.73
print(f"f = {f},id(f) = {id(f)}") # f = 1.73,id(f) = 140625653765296
我們修改 float_obj.ob_fval 也會影響 f,並且修改前后 f 的地址沒有發生改變。同時我們也可以觀察一個對象的引用計數,舉個栗子:
f = 3.14
float_obj = PyFloatObject.from_address(id(f))
# 此時 3.14 這個浮點數對象被 3 個變量所引用
print(float_obj.ob_refcnt) # 3
# 再來一個
f2 = f
print(float_obj.ob_refcnt) # 4
f3 = f
print(float_obj.ob_refcnt) # 5
# 刪除變量
del f2, f3
print(float_obj.ob_refcnt) # 3
所以這就是引用計數機制,當對象被引用,引用計數加 1;當引用該對象的變量被刪除,引用計數減 1;當對象的引用計數為 0 時,對象被銷毀。
整數
再來看看整數,我們知道 Python 中的整數是不會溢出的,換句話說,它可以計算無窮大的數。那么問題來了,它是怎么辦到的呢?想要知道答案,只需看底層的結構體定義即可。
typedef struct {
PyObject_VAR_HEAD
digit ob_digit[1]; // digit 等價於 unsigned int
} PyLongObject;
明白了,原來 Python 的整數在底層是用數組存儲的,通過串聯多個無符號 32 位整數來表示更大的數。
from ctypes import *
class PyVarObject(Structure):
_fields_ = [
("ob_refcnt", c_ssize_t),
("ob_type", c_void_p),
("ob_size", c_ssize_t)
]
class PyLongObject(PyVarObject):
_fields_ = [
("ob_digit", (c_uint32 * 1))
]
num = 1024
long_obj = PyLongObject.from_address(id(num))
print(long_obj.ob_digit[0]) # 1024
# PyLongObject 的 ob_size 除了表示 ob_digit 數組的長度,此時顯然為 1
print(long_obj.ob_size) # 1
# 但是在介紹整型的時候說過,ob_size 除了表示 ob_digit 數組的長度之外,還表示整數的符號
# 我們將 ob_size 改成 -1,再打印 num
long_obj.ob_size = -1
print(num) # -1024
# 我們悄悄地將 num 改成了負數
當然我們也可以修改值:
num = 1024
long_obj = PyLongObject.from_address(id(num))
long_obj.ob_digit[0] = 4096
print(num) # 4096
digit 是 32 位無符號整型,不過雖然占 32 個位,但是只用 30 個位,這也意味着一個 digit 能存儲的最大整數就是 2 的 30 次方減 1。如果數值再大一些,那么就需要兩個 digit 來存儲,第二個 digit 的最低位從 31 開始。
# 此時一個 digit 能夠存儲的下,所以 ob_size 為 1
num1 = 2 ** 30 - 1
long_obj1 = PyLongObject.from_address(id(num1))
print(long_obj1.ob_size) # 1
# 此時一個 digit 存不下了,所以需要兩個 digit,因此 ob_size 為 2
num2 = 2 ** 30
long_obj2 = PyLongObject.from_address(id(num2))
print(long_obj2.ob_size) # 2
當然了,用整數數組實現大整數的思路其實平白無奇,但難點在於大整數 數學運算 的實現,它們才是重點,也是也比較考驗編程功底的地方。
字節串
字節串也就是 Python 中的 bytes 對象,在存儲或網絡通訊時,傳輸的都是字節串。bytes 對象在底層的結構體為 PyBytesObject,看一下相關定義。
typedef struct {
PyObject_VAR_HEAD
Py_hash_t ob_shash;
char ob_sval[1];
} PyBytesObject;
我們解釋一下里面的成員對象:
PyObject_VAR_HEAD:變長對象的公共頭部
ob_shash:保存該字節序列的哈希值,之所以選擇保存是因為在很多場景都需要 bytes 對象的哈希值。而 Python 在計算字節序列的哈希值的時候,需要遍歷每一個字節,因此開銷比較大。所以會提前計算一次並保存起來,這樣以后就不需要算了,可以直接拿來用,並且 bytes 對象是不可變的,所以哈希值是不變的
ob_sval:這個和 PyLongObject 中的 ob_digit 的聲明方式是類似的,雖然聲明的時候長度是 1, 但具體是多少則取決於 bytes 對象的字節數量。這是 C 語言中定義"變長數組"的技巧, 雖然寫的長度是 1, 但是你可以當成 n 來用, n 可取任意值。顯然這個 ob_sval 存儲的是所有的字節,因此 Python 中的 bytes 對象在底層是通過字符數組存儲的。而且數組會多申請一個空間,用於存儲 \0,因為 C 中是通過 \0 來表示一個字符數組的結束,但是計算 ob_size 的時候不包括 \0
from ctypes import *
class PyVarObject(Structure):
_fields_ = [
("ob_refcnt", c_ssize_t),
("ob_type", c_void_p),
("ob_size", c_ssize_t)
]
class PyBytesObject(PyVarObject):
_fields_ = [
("ob_shash", c_ssize_t),
# 這里我們就將長度聲明為 100
("ob_sval", (c_char * 100))
]
b = b"hello"
bytes_obj = PyBytesObject.from_address(id(b))
# 長度
print(bytes_obj.ob_size, len(b)) # 5 5
# 哈希值
print(bytes_obj.ob_shash) # 967846336661272849
print(hash(b)) # 967846336661272849
# 修改哈希值,再調用 hash 函數會發現結果變了
# 說明 hash(b) 會直接獲取底層已經計算好的 ob_shash 成員的值
bytes_obj.ob_shash = 666
print(hash(b)) # 666
# 修改 ob_sval
bytes_obj.ob_sval = b"hello world"
print(b) # b'hello'
# 我們看到打印的依舊是 "hello",原因是 ob_size 為 5,只會選擇前 5 個字節
# 修改之后再次打印
bytes_obj.ob_size = 11
print(b) # b'hello world'
bytes_obj.ob_size = 15
print(b) # b'hello world\x00\x00\x00\x00'
除了 bytes 對象之外,Python 中還有一個 bytearray 對象,它和 bytes 對象類似,只不過 bytes 對象是不可變的,而 bytearray 對象是可變的。
列表
Python 中的列表可以說使用的非常廣泛了,在初學列表的時候,有人會告訴你列表就是一個大倉庫,什么都可以存放。但我們知道,列表中存放的元素其實都是泛型指針 PyObject *。
下面來看看列表的底層結構:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
我們看到里面有如下成員:
PyObject_VAR_HEAD: 變長對象的公共頭部信息
ob_item:一個二級指針,指向一個 PyObject * 類型的指針數組,這個指針數組保存的便是對象的指針,而操作底層數組都是通過 ob_item 來進行操作的。
allocated:容量, 我們知道列表底層是使用了 C 的數組, 而底層數組的長度就是列表的容量
from ctypes import *
class PyVarObject(Structure):
_fields_ = [
("ob_refcnt", c_ssize_t),
("ob_type", c_void_p),
("ob_size", c_ssize_t)
]
class PyListObject(PyVarObject):
_fields_ = [
# ctypes 下面有一個 py_object 類,它等價於底層的 PyObject *
# 但 ob_item 類型為 **PyObject,所以這里類型聲明為 POINTER(py_object)
("ob_item", POINTER(py_object)),
("allocated", c_ssize_t)
]
lst = [1, 2, 3, 4, 5]
list_obj = PyListObject.from_address(id(lst))
# 列表在計算長度的時候,會直接獲取 ob_size 成員的值,該值負責維護列表的長度
# 對元素進行增加、刪除,ob_size 也會動態變化
print(list_obj.ob_size) # 5
print(len(lst)) # 5
# 修改 ob_size 為 2,打印列表只會顯示兩個元素
list_obj.ob_size = 2
print(lst) # [1, 2]
try:
lst[2] # 訪問索引為 2 的元素會越界
except IndexError as e:
print(e) # list index out of range
# 修改元素,注意:ob_item 里面的元素是 PyObject*,所以這里需要調用 py_object 轉一下
list_obj.ob_item[0] = py_object("😂")
print(lst) # ['😂', 2]
元組
下面來看看元組,我們可以把元素看成不支持元素添加、修改、刪除等操作的列表。元組的實現機制非常簡單,可以看做是在列表的基礎上丟棄了增刪改等操作。既然如此,那要元組有什么用呢?畢竟元組的功能只是列表的子集。元組存在的最大一個特點就是,它可以作為字典的 key、以及可以作為集合的元素。因為字典和集合存儲數據的原理是哈希表,對於列表這樣的可變對象來說是可以動態改變的,而哈希值是一開始就計算好的,顯然如果支持動態修改的話,那么哈希值肯定會變,這是不允許的。所以如果我們希望字典的 key 是一個序列,顯然元組再適合不過了。
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
可以看到,對於不可變對象來說,它底層結構體定義也非常簡單。一個引用計數、一個類型、一個指針數組。這里的 1 可以想象成 n,我們上面說過它的含義。並且我們發現不像列表,元組沒有 allocated,這是因為它是不可變的,不支持擴容操作。
這里再對比一下元組和列表的 ob_item 成員,PyTupleObject 的 ob_item 是一個指針數組,數組里面是泛型指針 PyObject *;而 PyListObject 的 ob_item 是一個二級指針,該指針指向了一個存放 PyObject * 的指針數組的首元素。
所以 Python 中的 "列表本身" 和 "列表里面的值" 在底層是分開存儲的,因為 PyListObject 結構體實例並沒有存儲相應的指針數組,而是存儲了指向這個指針數組首元素的二級指針。顯然我們添加、刪除、修改元素等操作,都是通過這個二級指針來間接操作這個指針數組。這么做的原因就在於對象一旦被創建,那么它在內存中的大小就不可以變了,因此這就意味着那些可以容納可變長度數據的可變對象,要在內部維護一個指向可變大小的內存區域的指針,遵循這樣的規則可以使維護對象的工作變得非常簡單。
試想一下這樣一個場景:一旦允許對象的大小可在運行期改變,那么假設在內存中有對象 A,並且其后面緊跟着對象 B。如果運行的某個時候,A 的大小增大了,這就意味着必須將 A 整個移動到內存中的其他位置,否則 A 增大的部分會覆蓋掉原本屬於 B 的數據。只要將 A 移動到內存的其他位置,那么所有指向 A 的指針就必須立即得到更新。可想而知這樣的工作是多么的繁瑣,而通過一個指針去操作就變得簡單多了。
可以看到 PyListObject 實例本身和指針數組之間是分離的,兩者通過二級指針(ob_item)建立聯系;但元組不同,它的大小不允許改變,因此 PyTupleObject 直接存儲了指針數組本身(ob_item)。
from ctypes import *
class PyVarObject(Structure):
_fields_ = [
("ob_refcnt", c_ssize_t),
("ob_type", c_void_p),
("ob_size", c_ssize_t)
]
class PyTupleObject(PyVarObject):
_fields_ = [
# 這里我們假設里面可以存 10 個元素
("ob_item", (py_object * 10)),
]
tpl = (11, 22, 33)
tuple_obj = PyTupleObject.from_address(id(tpl))
print(tuple_obj.ob_size) # 3
print(len(tpl)) # 3
# 這里我們修改元組內的元素
print(f"修改前:id(tpl) = {id(tpl)},tpl = {tpl}")
tuple_obj.ob_item[0] = py_object("🍑")
print(f"修改后:id(tpl) = {id(tpl)},tpl = {tpl}")
"""
修改前:id(tpl) = 140570376749888,tpl = (11, 22, 33)
修改后:id(tpl) = 140570376749888,tpl = ('🍑', 22, 33)
"""
此時我們就成功修改了元組里面的元素,並且修改前后元組的地址沒有改變。
要是以后誰跟你說 Python 元組里的元素不能修改,就拿這個例子堵他嘴。好吧,元組就是不可變的,舉這個例子有點不太合適。
給類對象增加屬性
我們知道類對象(或者說類型對象)是有自己的屬性字典的,但這個字典不允許修改,因為准確來說它不是字典,而是一個 mappingproxy 對象。
print(str.__dict__.__class__) # <class 'mappingproxy'>
try:
str.__dict__["嘿"] = "蛤"
except Exception as e:
print(e) # 'mappingproxy' object does not support item assignment
我們無法通過修改 mappingproxy 對象來給類增加屬性,因為它不支持增加、修改以及刪除操作。當然對於自定義的類可以通過 setattr 方法實現,但是內置的類是行不通的,內置的類無法通過 setattr 進行屬性添加。因此如果想給內置的類增加屬性,只能通過 mappingproxy 入手,我們看一下它的底層結構。
所謂的 mappingproxy 就是對字典包了一層,並只提供了查詢功能。而且從函數 mappingproxy_len、mappingproxy_getitem 可以看出,mappingproxy 對象的長度就是內部字典的長度,獲取 mappingproxy 對象的元素實際上就是獲取內部字典的元素,因此操作 mappingproxy 對象就等價於操作其內部的字典。
所以我們只要能拿到 mappingproxy 對象內部的字典,那么可以直接操作字典來修改類屬性。而 Python 有一個模塊叫 gc,它可以幫我們實現這一點,舉個栗子:
import gc
lst = ["hello", 123, "😒"]
# gc.get_referents(obj) 返回所有被 obj 引用的對象
print(gc.get_referents(lst)) # ['😒', 123, 'hello']
# 顯然 lst 引用的就是內部的三個元素
# 此外還有 gc.get_referrers(obj),它是返回所有引用了 obj 的對象
那么問題來了,你覺得 mappingproxy 對象引用了誰呢?顯然就是內部的字典。
import gc
# str.__dict__ 是一個 mappingproxy 對象,這里拿到其內部的字典,
d = gc.get_referents(str.__dict__)[0]
# 隨便增加一個屬性
d["嘿"] = "蛤"
print(str.嘿) # 蛤
print("嘿".嘿) # 蛤
# 當然我們也可以增加一個函數,記得要有一個 self 參數
d["smile"] = lambda self: self + "😊"
print("微笑".smile()) # 微笑😊
print(str.smile("微笑")) # 微笑😊
但是需要注意的是,我們上面添加的是之前沒有的新屬性,如果是覆蓋一個已經存在的屬性或者函數,那么還缺一步。
from ctypes import *
import gc
s = "hello world"
print(s.split()) # ['hello', 'world']
d = gc.get_referents(str.__dict__)[0]
d["split"] = lambda self, *args: "我被 split 了" # 覆蓋 split 函數
# 可以通過 pythonapi 來調用 CPython 對外暴露的 API,后面會說
# 這里需要調用 pythonapi.PyType_Modified 來更新上面所做的修改
# 如果沒有這一步,那么是沒有效果的,甚至還會出現丑陋的段錯誤,使得解釋器異常退出
pythonapi.PyType_Modified(py_object(str))
print(s.split()) # 我被 split 了
不過上面的代碼還有一個缺點,那就是函數的名字沒有修改:
from ctypes import *
import gc
s = "hello world"
print(s.split.__name__) # split
d = gc.get_referents(str.__dict__)[0]
d["split"] = lambda self, *args: "我被 split 了" # 覆蓋 split 函數
pythonapi.PyType_Modified(py_object(str))
print(s.split.__name__) # <lambda>
我們看到函數在修改之后名字就變了,匿名函數的名字就叫 <lambda>,所以我們可以再完善一下。
from ctypes import *
import gc
def patch_builtin_class(cls, name, value):
"""
:param cls: 要修改的類
:param name: 屬性名或者函數名
:param value: 值
:return:
"""
if type(cls) is not type:
raise ValueError("cls 必須是一個內置的類對象")
# 獲取 cls.__dict__ 內部的字典
cls_attrs = gc.get_referents(cls.__dict__)[0]
# 如果該屬性或函數不存在,結果為 None;否則將值取出來,賦值給 old_value
old_value = cls_attrs.get(name, None)
# 將 name、value 組合起來放到 cls_attrs 中,為 cls 這個類添磚加瓦
cls_attrs[name] = value
# 如果 old_value 為 None,說明我們添加了的一個新的屬性或函數
# 如果 old_value 不為 None,說明我們覆蓋了的一個已存在的屬性或函數
if old_value is not None:
try:
# 將原來函數的 __name__、__qualname__ 賦值給新的函數
# 如果不是函數,而是普通屬性,那么會因為沒有 __name__ 而拋出 AttributeError
# 這里我們直接 pass 掉即可,無需關心
value.__name__ = old_value.__name__
value.__qualname__ = old_value.__qualname__
except AttributeError:
pass
# 但是原來的屬性或函數最好也不要丟棄,我們可以改一個名字
# 假設我們修改 split 函數,那么修改之后,原來的 split 就需要通過 _str_split 進行調用
cls_attrs[f"_{cls.__name__}_{name}"] = old_value
# 不要忘了最關鍵的一步
pythonapi.PyType_Modified(py_object(cls))
s = "hello world"
print(s.title()) # Hello World
# 修改內置屬性
patch_builtin_class(str, "title", lambda self: "我單詞首字母大寫了")
print(s.title()) # 我單詞首字母大寫了
print(s.title.__name__) # title
# 而原來的 title 則需要通過 _str_title 進行調用
print(s._str_title()) # Hello World
很明顯,我們不僅可以修改 str,任意的內置的類都是可以修改的。
lst = [1, 2, 3]
# 將 append 函數換成 pop 函數
patch_builtin_class(list, "append", lambda self: list.pop(self))
# 我們知道 append 需要接收一個參數,但這里我們不需要傳,因為函數已經被換掉了
lst.append()
print(lst) # [1, 2]
# 而原來的 append 函數,則需要通過 _list_append 進行調用
lst._list_append(666)
print(lst) # [1, 2, 666]
我們還可以添加一個類方法或靜態方法:
patch_builtin_class(
list,
"new",
classmethod(lambda cls, n: list(range(n)))
)
print(list.new(5)) # [0, 1, 2, 3, 4]
還是很有趣的,但需要注意的是,我們目前的 patch_builtin_class 只能為類添加屬性或函數,但其 "實例對象" 使用操作符時的表現是無法操控的。什么意思呢?我們舉個栗子:
a, b = 3, 4
# 每一個操作背后都被抽象成了一個魔法方法
print(int.__add__(a, b)) # 7
print(a.__add__(b)) # 7
print(a + b) # 7
# 重寫 __add__
patch_builtin_class(int, "__add__", lambda self, other: self * other)
print(int.__add__(a, b)) # 12
print(a.__add__(b)) # 12
print(a + b) # 7
我們看到重寫了 __add__ 之后,直接調用魔法方法的話是沒有問題的,打印的是重寫之后的結果。而使用操作符的話(a + b),卻沒有走我們重寫之后的 __add__,所以 a + b 的結果還是 7。
s1, s2 = "hello", "world"
patch_builtin_class(str, "__sub__", lambda self, other: (self, other))
print(s1.__sub__(s2)) # ('hello', 'world')
try:
s1 - s2
except TypeError as e:
print(e) # unsupported operand type(s) for -: 'str' and 'str'
我們重寫了 __sub__ 之后,直接調用魔法方法的話也是沒有問題的,但是用操作符(s1 - s2)就會報錯,告訴我們字符串不支持減法操作,但我們明明實現了 __sub__ 方法啊。想要知道原因並改變它,我們就要先知道類對象在底層是怎么實現的。
類對象的底層結構 PyTypeObject
首先思考兩個問題:
1. 當在內存中創建對象、分配空間的時候,解釋器要給該對象分配多大的空間?顯然不能隨便分配,那么該對象的內存信息在什么地方?
2. 一個對象是支持相應的操作的,解釋器怎么判斷該對象支持哪些操作呢?再比如一個整型可以和一個整型相乘,但是一個列表也可以和一個整型相乘,即使是相同的操作,但不同類型的對象執行也會有不同的結果,那么此時解釋器又是如何進行區分的?
想都不用想,這些信息肯定都在對象所對應的類型對象中。而且占用的空間大小實際上是對象的一個元信息,這樣的元信息和其所屬類型是密切相關的,因此它一定會出現在與之對應的類型對象當中。至於支持的操作就更不用說了,我們平時自定義類的時候,方法都寫在什么地方,顯然都是寫在類里面,因此一個對象支持的操作顯然定義在類型對象當中。
而將一個對象和其類型對象關聯起來的,毫無疑問正是該對象內部的 PyObject 中的 ob_type,也就是類型的指針。我們通過對象的 ob_type 成員即可獲取指向的類型對象的指針,通過該指針可以獲取存儲在類型對象中的某些元信息。
下面我們來看看類型對象在底層是怎么定義的:
typedef struct _typeobject {
// 頭部信息,PyVarObject ob_base; 里面包含了 引用計數、類型、ob_size
// 而創建這個結構體實例的話,Python 提供了一個宏,PyVarObject_HEAD_INIT(type, size)
// 傳入類型和 ob_size 可以直接創建,至於引用計數則默認為 1
PyObject_VAR_HEAD
// 創建之后的類名
const char *tp_name;
// 大小,用於申請空間的,注意了,這里是兩個成員
Py_ssize_t tp_basicsize, tp_itemsize;
// 析構方法__del__,當刪除實例對象時會調用這個操作
// typedef void (*destructor)(PyObject *); 函數接收一個 PyObject *,沒有返回值
destructor tp_dealloc;
// 打印其實例對象時調用的函數
// typedef int (*printfunc)(PyObject *, FILE *, int); 函數接收一個PyObject *、FILE * 和 int
printfunc tp_print;
// 獲取屬性,內部的 __getattr__ 方法, typedef PyObject *(*getattrfunc)(PyObject *, char *);
getattrfunc tp_getattr;
// 設置屬性,內部的 __setattr__ 方法,typedef int (*setattrfunc)(PyObject *, char *, PyObject *);
setattrfunc tp_setattr;
// Python3.5 新增,協程對象所擁有的方法
PyAsyncMethods *tp_as_async;
// 內部的 __repr__方法,typedef PyObject *(*reprfunc)(PyObject *);
reprfunc tp_repr;
// 一個對象作為數值所有擁有的方法
PyNumberMethods *tp_as_number;
// 一個對象作為序列所有擁有的方法
PySequenceMethods *tp_as_sequence;
// 一個對象作為映射所有擁有的方法
PyMappingMethods *tp_as_mapping;
// 內部的 __hash__ 方法,typedef Py_hash_t (*hashfunc)(PyObject *);
hashfunc tp_hash;
// 內部的 __call__ 方法, typedef PyObject * (*ternaryfunc)(PyObject *, PyObject *, PyObject *);
ternaryfunc tp_call;
// 內部的 __str__ 方法,typedef PyObject *(*reprfunc)(PyObject *);
reprfunc tp_str;
// 獲取屬性,typedef PyObject *(*getattrofunc)(PyObject *, PyObject *);
getattrofunc tp_getattro;
// 設置屬性,typedef int (*setattrofunc)(PyObject *, PyObject *, PyObject *);
setattrofunc tp_setattro;
// 用於實現緩沖區協議,實現了該協議可以和 Numpy 的 array 無縫集成
PyBufferProcs *tp_as_buffer;
// 這個類的特點,比如:
// Py_TPFLAGS_HEAPTYPE:是否在堆區申請空間
// Py_TPFLAGS_BASETYPE:是否允許這個類被其它類繼承
// Py_TPFLAGS_IS_ABSTRACT:是否為抽象類
// Py_TPFLAGS_HAVE_GC: 是否被垃圾回收跟蹤
unsigned long tp_flags;
// 這個類的注釋
const char *tp_doc;
// 用於檢測是否出現循環引用,和下面的 tp_clear 是一組
// typedef int (*traverseproc)(PyObject *, visitproc, void *);
traverseproc tp_traverse;
// 清除對包含對象的引用
inquiry tp_clear;
// 富比較,typedef PyObject *(*richcmpfunc) (PyObject *, PyObject *, int);
richcmpfunc tp_richcompare;
// 弱引用,不需要關心
Py_ssize_t tp_weaklistoffset;
// __iter__方法,typedef PyObject *(*getiterfunc) (PyObject *);
getiterfunc tp_iter;
// __next__方法,typedef PyObject *(*iternextfunc) (PyObject *);
iternextfunc tp_iternext;
// 內部的方法
struct PyMethodDef *tp_methods;
// 內部的成員
struct PyMemberDef *tp_members;
// 用於實現 getset
struct PyGetSetDef *tp_getset;
// 繼承的基類
struct _typeobject *tp_base;
// 內部的屬性字典
PyObject *tp_dict;
// 描述符,__get__ 方法,typedef PyObject *(*descrgetfunc) (PyObject *, PyObject *, PyObject *);
descrgetfunc tp_descr_get;
// 描述符,__set__ 方法
descrsetfunc tp_descr_set;
// 生成的實例對象是否有屬性字典
Py_ssize_t tp_dictoffset;
// 初始化函數,typedef int (*initproc)(PyObject *, PyObject *, PyObject *);
initproc tp_init;
// 為實例對象分配空間的函數,typedef PyObject *(*allocfunc)(struct _typeobject *, Py_ssize_t);
allocfunc tp_alloc;
// __new__ 方法,typedef PyObject *(*newfunc)(struct _typeobject *, PyObject *, PyObject *);
newfunc tp_new;
// 釋放一個實例對象,typedef void (*freefunc)(void *); 一般會在析構函數中調用
freefunc tp_free;
// typedef int (*inquiry)(PyObject *); 是否被 gc 跟蹤
inquiry tp_is_gc;
// 繼承哪些類,這里可以指定繼承多個類
PyObject *tp_bases;
// __mro__
PyObject *tp_mro;
// 下面的就不用管了
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
unsigned int tp_version_tag;
destructor tp_finalize;
#ifdef COUNT_ALLOCS
Py_ssize_t tp_allocs;
Py_ssize_t tp_frees;
Py_ssize_t tp_maxalloc;
struct _typeobject *tp_prev;
struct _typeobject *tp_next;
#endif
} PyTypeObject;
#endif
而 Python 中的類對象(類型對象)在底層就是一個 PyTypeObject 實例,它保存了實例對象的元信息,描述對象的類型。所以 Python 中的實例對象在底層對應不同的結構體實例,而類對象則是對應同一個結構體實例,換句話說無論是 int、str、dict,還是其它的類對象,它們在 C 的層面都是由 PyTypeObject 這個結構體實例化得到的,只不過成員的值不同,PyTypeObject 這個結構體在實例化之后得到的類型對象也不同。
這里我們重點看一下里面的 tp_as_number、tp_as_sequence、tp_as_mapping 三個成員,它們表示實例對象為數值、序列、映射時所支持的操作。它們都是指向結構體的指針,該結構體中的每一個成員都是一個函數指針,指向的函數便是實例對象可執行的操作。
我們再看一下類對象 int 在底層的定義:
我們注意到它的類型被設置成了 PyType_Type,所以在 Python 里面 int 的類型為 type。然后重點是 tp_as_number 成員,它被初始化為 &long_as_number,而整型對象不支持序列和映射操作,所以 tp_as_sequence、tp_as_mapping 設置為 0。當然這三者都是指向結構體的指針類型,我們看一下 long_as_number。
因此 PyNumberMethods 的成員就是整數所有擁有的魔法方法,當然也包括浮點數。
至此,整個結構就很清晰了。
若想改變操作符的表現行為,我們需要修改的是 tp_as_* 里面的成員的值,而不是簡單的修改屬性字典。比如我們想修改 a + b 的表現行為,那么就將類對象的 tp_as_number 里面的 nb_add 給改掉。如果是整形,那么就覆蓋掉 long_add,也就是 "PyLong_Type -> long_as_number -> nb_add";同理,如果是浮點型,那么就覆蓋掉 float_add,也就是 "PyFloat_Type -> float_as_number -> nb_add"。
重寫操作符
我們說類對象里面有 4 個方法集,分別是 tp_as_number、tp_as_sequence、tp_as_mapping、tp_as_async,如果我們想改變操作符的表現結果,那么就重寫里面對應的函數即可。
from ctypes import *
import gc
# 將這些對象提前聲明好,之后再進行成員的初始化
class PyObject(Structure): pass
class PyTypeObject(Structure): pass
class PyNumberMethods(Structure): pass
class PySequenceMethods(Structure): pass
class PyMappingMethods(Structure): pass
class PyAsyncMethods(Structure): pass
class PyFile(Structure): pass
PyObject._fields_ = [("ob_refcnt", c_ssize_t),
("ob_type", POINTER(PyTypeObject))]
PyTypeObject._fields_ = [
('ob_base', PyObject),
('ob_size', c_ssize_t),
('tp_name', c_char_p),
('tp_basicsize', c_ssize_t),
('tp_itemsize', c_ssize_t),
('tp_dealloc', CFUNCTYPE(None, py_object)),
('printfunc', CFUNCTYPE(c_int, py_object, POINTER(PyFile), c_int)),
('getattrfunc', CFUNCTYPE(py_object, py_object, c_char_p)),
('setattrfunc', CFUNCTYPE(c_int, py_object, c_char_p, py_object)),
('tp_as_async', CFUNCTYPE(PyAsyncMethods)),
('tp_repr', CFUNCTYPE(py_object, py_object)),
('tp_as_number', POINTER(PyNumberMethods)),
('tp_as_sequence', POINTER(PySequenceMethods)),
('tp_as_mapping', POINTER(PyMappingMethods)),
('tp_hash', CFUNCTYPE(c_int64, py_object)),
('tp_call', CFUNCTYPE(py_object, py_object, py_object, py_object)),
('tp_str', CFUNCTYPE(py_object, py_object)),
# 不需要的可以不用寫
]
# 方法集就是一個結構體實例,結構體成員都是函數
# 所以這里我們要相關的函數類型聲明好
inquiry = CFUNCTYPE(c_int, py_object)
unaryfunc = CFUNCTYPE(py_object, py_object)
binaryfunc = CFUNCTYPE(py_object, py_object, py_object)
ternaryfunc = CFUNCTYPE(py_object, py_object, py_object, py_object)
lenfunc = CFUNCTYPE(c_ssize_t, py_object)
ssizeargfunc = CFUNCTYPE(py_object, py_object, c_ssize_t)
ssizeobjargproc = CFUNCTYPE(c_int, py_object, c_ssize_t, py_object)
objobjproc = CFUNCTYPE(c_int, py_object, py_object)
objobjargproc = CFUNCTYPE(c_int, py_object, py_object, py_object)
PyNumberMethods._fields_ = [
('nb_add', binaryfunc),
('nb_subtract', binaryfunc),
('nb_multiply', binaryfunc),
('nb_remainder', binaryfunc),
('nb_divmod', binaryfunc),
('nb_power', ternaryfunc),
('nb_negative', unaryfunc),
('nb_positive', unaryfunc),
('nb_absolute', unaryfunc),
('nb_bool', inquiry),
('nb_invert', unaryfunc),
('nb_lshift', binaryfunc),
('nb_rshift', binaryfunc),
('nb_and', binaryfunc),
('nb_xor', binaryfunc),
('nb_or', binaryfunc),
('nb_int', unaryfunc),
('nb_reserved', c_void_p),
('nb_float', unaryfunc),
('nb_inplace_add', binaryfunc),
('nb_inplace_subtract', binaryfunc),
('nb_inplace_multiply', binaryfunc),
('nb_inplace_remainder', binaryfunc),
('nb_inplace_power', ternaryfunc),
('nb_inplace_lshift', binaryfunc),
('nb_inplace_rshift', binaryfunc),
('nb_inplace_and', binaryfunc),
('nb_inplace_xor', binaryfunc),
('nb_inplace_or', binaryfunc),
('nb_floor_divide', binaryfunc),
('nb_true_divide', binaryfunc),
('nb_inplace_floor_divide', binaryfunc),
('nb_inplace_true_divide', binaryfunc),
('nb_index', unaryfunc),
('nb_matrix_multiply', binaryfunc),
('nb_inplace_matrix_multiply', binaryfunc)]
PySequenceMethods._fields_ = [
('sq_length', lenfunc),
('sq_concat', binaryfunc),
('sq_repeat', ssizeargfunc),
('sq_item', ssizeargfunc),
('was_sq_slice', c_void_p),
('sq_ass_item', ssizeobjargproc),
('was_sq_ass_slice', c_void_p),
('sq_contains', objobjproc),
('sq_inplace_concat', binaryfunc),
('sq_inplace_repeat', ssizeargfunc)]
# 將這些魔法方法的名字和底層的結構體成員組合起來
magic_method_dict = {
"__add__": ("tp_as_number", "nb_add"),
"__sub__": ("tp_as_number", "nb_subtract"),
"__mul__": ("tp_as_number", "nb_multiply"),
"__mod__": ("tp_as_number", "nb_remainder"),
"__pow__": ("tp_as_number", "nb_power"),
"__neg__": ("tp_as_number", "nb_negative"),
"__pos__": ("tp_as_number", "nb_positive"),
"__abs__": ("tp_as_number", "nb_absolute"),
"__bool__": ("tp_as_number", "nb_bool"),
"__inv__": ("tp_as_number", "nb_invert"),
"__invert__": ("tp_as_number", "nb_invert"),
"__lshift__": ("tp_as_number", "nb_lshift"),
"__rshift__": ("tp_as_number", "nb_rshift"),
"__and__": ("tp_as_number", "nb_and"),
"__xor__": ("tp_as_number", "nb_xor"),
"__or__": ("tp_as_number", "nb_or"),
"__int__": ("tp_as_number", "nb_int"),
"__float__": ("tp_as_number", "nb_float"),
"__iadd__": ("tp_as_number", "nb_inplace_add"),
"__isub__": ("tp_as_number", "nb_inplace_subtract"),
"__imul__": ("tp_as_number", "nb_inplace_multiply"),
"__imod__": ("tp_as_number", "nb_inplace_remainder"),
"__ipow__": ("tp_as_number", "nb_inplace_power"),
"__ilshift__": ("tp_as_number", "nb_inplace_lshift"),
"__irshift__": ("tp_as_number", "nb_inplace_rshift"),
"__iand__": ("tp_as_number", "nb_inplace_and"),
"__ixor__": ("tp_as_number", "nb_inplace_xor"),
"__ior__": ("tp_as_number", "nb_inplace_or"),
"__floordiv__": ("tp_as_number", "nb_floor_divide"),
"__div__": ("tp_as_number", "nb_true_divide"),
"__ifloordiv__": ("tp_as_number", "nb_inplace_floor_divide"),
"__idiv__": ("tp_as_number", "nb_inplace_true_divide"),
"__index__": ("tp_as_number", "nb_index"),
"__matmul__": ("tp_as_number", "nb_matrix_multiply"),
"__imatmul__": ("tp_as_number", "nb_inplace_matrix_multiply"),
"__len__": ("tp_as_sequence", "sq_length"),
"__concat__": ("tp_as_sequence", "sq_concat"),
"__repeat__": ("tp_as_sequence", "sq_repeat"),
"__getitem__": ("tp_as_sequence", "sq_item"),
"__setitem__": ("tp_as_sequence", "sq_ass_item"),
"__contains__": ("tp_as_sequence", "sq_contains"),
"__iconcat__": ("tp_as_sequence", "sq_inplace_concat"),
"__irepeat__": ("tp_as_sequence", "sq_inplace_repeat")
}
keep_method_alive= {}
keep_method_set_alive= {}
# 以上就准備就緒了,下面再將之前的 patch_builtin_class 函數補充一下即可
def patch_builtin_class(cls, name, value):
"""
:param cls: 要修改的類
:param name: 屬性名或者函數名
:param value: 值
:return:
"""
if type(cls) is not type:
raise ValueError("cls 必須是一個內置的類對象")
cls_attrs = gc.get_referents(cls.__dict__)[0]
old_value = cls_attrs.get(name, None)
cls_attrs[name] = value
if old_value is not None:
try:
value.__name__ = old_value.__name__
value.__qualname__ = old_value.__qualname__
except AttributeError:
pass
cls_attrs[f"_{cls.__name__}_{name}"] = old_value
pythonapi.PyType_Modified(py_object(cls))
# 以上邏輯不變,然后對參數 name 進行檢測
# 如果是魔方方法、並且 value 是一個可調用對象,那么修改操作符,否則直接 return
if name not in magic_method_dict and callable(value):
return
# 比如 name 是 __sub__,那么 tp_as_name, rewrite == "tp_as_number", "nb_sub"
tp_as_name, rewrite = magic_method_dict[name]
# 獲取類對應的底層結構,PyTypeObject 實例
type_obj = PyTypeObject.from_address(id(cls))
# 根據 tp_as_name 判斷到底是哪一個方法集,這里我們沒有實現 tp_as_mapping 和 tp_as_async
struct_method_set_class = (PyNumberMethods if tp_as_name == "tp_as_number"
else PySequenceMethods if tp_as_name == "tp_as_sequence"
else PyMappingMethods if tp_as_name == "tp_as_mapping"
else PyAsyncMethods)
# 獲取具體的方法集(指針)
struct_method_set_ptr = getattr(type_obj, tp_as_name, None)
if not struct_method_set_ptr:
# 如果不存在此方法集,我們實例化一個,然后設置到里面去
struct_method_set = struct_method_set_class()
# 注意我們要傳一個指針進去
setattr(type_obj, tp_as_name, pointer(struct_method_set))
# 然后對指針進行解引用,獲取方法集,也就是對應的結構體實例
struct_method_set = struct_method_set_ptr.contents
# 遍歷 struct_method_set_class,判斷到底重寫的是哪一個魔法方法
cfunc_type = None
for field, ftype in struct_method_set_class._fields_:
if field == rewrite:
cfunc_type = ftype
# 構造新的函數
cfunc = cfunc_type(value)
# 更新方法集
setattr(struct_method_set, rewrite, cfunc)
# 至此我們的功能就完成了,但還有一個非常重要的點,就是上面的 cfunc
# 雖然它是一個底層可以識別的 C 函數,但它本質上仍然是一個 Python 對象
# 其內部維護了 C 級數據,賦值之后底層會自動提取,而這一步不會增加引用計數
# 所以這個函數結束之后,cfunc 就被銷毀了(連同內部的 C 級數據)
# 這樣后續再調用相關操作符的時候就會出現段錯誤,解釋器異常退出
# 因此我們需要在函數結束之前創建一個在外部持有它的引用
keep_method_alive[(cls, name)] = cfunc
# 當然還有我們上面的方法集,也是同理
keep_method_set_alive[(cls, name)] = struct_method_set
代碼量還是稍微有點多的,但是不難理解,我們將這些代碼放在一個單獨的文件里面,文件名就叫 unsafe_magic.py,然后導入它。
from unsafe_magic import patch_builtin_class
patch_builtin_class(int, "__getitem__", lambda self, item: "_".join([str(self)] * item))
patch_builtin_class(str, "__matmul__", lambda self, other: (self, other))
patch_builtin_class(str, "__sub__", lambda self, other: other + self)
你覺得之后會發生什么呢?我們測試一下:
怎么樣,是不是很好玩呢?
from unsafe_magic import patch_builtin_class
patch_builtin_class(tuple, "append", lambda self, item: self + (item, ))
t = ()
print(t.append(1).append(2).append(3).append(4)) # (1, 2, 3, 4)
因此 Python 給開發者賦予的權限是非常高的,你可以玩出很多意想不到的新花樣。
另外再多說一句,當對象不支持某個操作符的時候,我們能夠讓它實現該操作符;但如果對象已經實現了某個操作符,那么其邏輯就改不了了,舉個栗子:
from unsafe_magic import patch_builtin_class
# str 沒有 __div__,我們可以為其實現,此時字符串便擁有了除法的功能
patch_builtin_class(str, "__div__", lambda self, other: (self, other))
print("hello" / "world") # ('hello', 'world')
# 但 __add__ 是 str 本身就有的,也就是說字符串本身就可以相加
# 而此時我們就無法覆蓋加法這個操作符了
patch_builtin_class(str, "__add__", lambda self, other: (self, other))
print("你" + "好") # 你好
# 我們看到使用加號,並沒有走我們重寫之后的 __add__ 方法,因為字符串本身就支持加法運算
# 但也有例外,就是當出現 TypeError 的時候,那么解釋器會執行我們重寫的方法
# 字符串和整數相加會出現異常,因此解釋器會執行我們重寫的 __add__
print("你" + 123) # ('你', 123)
# 但如果是調用魔方方法,那么會直接走我們重寫的 __add__,前面說過的
print("你".__add__("好")) # ('你', '好')
不過上述這個問題在 3.6 版本的時候是沒有的,操作符會無條件地執行我們重寫的魔法方法。但在 3.8 的時候出現了這個現象,可以自己測試一下。
最后再來說一說 Python/C API,Python 解釋器暴露了大量的 C 一級的 API 供我們調用,而調用方式可以通過 ctypes.pythonapi 來實現。我們之前用過一次,就是 pythonapi.PyType_Modified。那么再舉個例子來感受一下:
from ctypes import *
lst = [1, 2, 3]
# 函數原型:PyList_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem)
# 調用的時候類型一定要匹配,否則很容易導致解釋器異常退出
pythonapi.PyList_SetItem(py_object(lst), 1, py_object(666))
print(lst) # [1, 666, 3]
ctypes.pythonapi 用的不是很多,像 Python 提供的 C 級 API 一般在編寫擴展的時候有用。
小結
以上我們就用 ctypes 玩了一些騷操作,內容還是有點單調,當然你也可以玩的再嗨一些。但是無論如何,一定不要在生產上使用,線上不要出現這種會改變解釋器運行邏輯的代碼。如果只是為了調試、或者想從實踐的層面更深入的了解虛擬機,那么沒事可以玩一玩。