楔子
我们来介绍一个好玩的,看看如何修改 Python 的底层数据结构和运行时。了解虚拟机除了可以让我们写出更好的代码之外,还可以对 Python 进行改造。举个栗子:
是不是很有趣呢?通过 Python 内置的 ctypes 模块即可做到,而具体的实现方式我们一会儿说。所以本次我们的工具就是 ctypes 模块,需要你对它已经或多或少有一些了解,如果不了解的话可以看我的这篇文章,里面详细地介绍了 ctypes 模块的用法。
Python 的对象本质上就是 C 的 malloc 函数为结构体实例在堆区申请的一块内存,比如整数是 PyLongObject、浮点数是 PyFloatObject、列表是 PyListObject,以及所有的类型都是 PyTypeObject 等等。
下面就来构造这些数据结构并观察 Python 对象在运行时的表现。
演示所使用的 Python 版本是 3.8
免责声明:本文介绍的内容绝不能用于生产环境,仅仅只是为了更好地理解 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), # 类型对象一会说,这里就先用 void * 模拟 ("ob_type", c_void_p) ] 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 等价于 unsigned int digit ob_digit[1]; } 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_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;
解释一下里面每个成员的含义:
1)PyObject_VAR_HEAD:变长对象的公共头部;
2)ob_shash:保存该字节序列的哈希值,之所以选择保存是因为在很多场景都需要 bytes 对象的哈希值。而 Python 在计算字节序列的哈希值的时候,需要遍历每一个字节,因此开销比较大。所以会提前计算一次并保存起来,这样以后就不需要算了,可以直接拿来用,并且 bytes 对象是不可变的,所以哈希值是不变的;
3)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 # 用 \0 填充 print(b) # b'hello world\x00\x00\x00\x00'
除了 bytes 对象之外,Python 还有一个 bytearray 对象,它和 bytes 对象类似,只不过 bytes 对象是不可变的,而 bytearray 对象是可变的。
列表
列表可以说使用的非常广泛了,在初学列表的时候,有人会告诉你列表就是一个大仓库,什么都可以存放。但我们知道,列表中存放的元素其实都是泛型指针 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]
是不是很有趣呢?
元组
下面来看看元组,我们可以把元组看成是不支持元素添加、修改、删除等操作的列表。元组的实现机制非常简单,可以看作是在列表的基础上丢弃了增删改等操作。
typedef struct { PyObject_VAR_HEAD PyObject *ob_item[1]; } PyTupleObject;
元组的底层结构体定义也非常简单,一个引用计数、一个类型、一个指针数组。数组里面的 1 可以想象成 n,我们上面说过它的含义。并且我们发现不像列表,元组没有 allocated,这是因为它是不可变的,不支持扩容操作。
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) """
此时我们就成功修改了元组里面的元素,并且修改前后元组的地址没有改变。
所谓的对象是否可变,取决于解释器有没有将修改对象的接口暴露给我们,但站在解释器的角度上,没有什么可变不可变,都是可变的。
给类对象增加属性
我们知道类对象是有自己的属性字典的,但这个字典不允许修改,因为准确来说它不是字典,而是一个 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 tpl = ("hello", 123, "😒") # gc.get_referents(obj) 返回所有被 obj 引用的对象 # 以列表的形式返回 print(gc.get_referents(tpl)) # ['😒', 123, 'hello'] # 显然 tpl 引用的就是内部的三个元素 # 此外还有 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] # 覆盖 split 函数 d["split"] = lambda self, *args: "我被 split 了" # 这里需要调用 pythonapi.PyType_Modified 来更新上面所做的修改 # 如果没有这一步,那么是没有效果的 # 甚至还会出现丑陋的段错误,使得解释器异常退出 pythonapi.PyType_Modified(py_object(str)) print(s.split()) # 我被 split 了
但是还不够完善,因为函数的名字没有修改,而且覆盖之后原来的名字也找不到了。
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__ 之后,直接调用魔法方法的话是没有问题的,打印的是重写之后的结果。而使用操作符 + 时,却没有走我们重写之后的 __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__ 之后,直接调用魔法方法的话也是没有问题的,但是用操作符的方式就会报错,告诉我们字符串不支持减法操作,但明明实现了 __sub__ 方法啊。
首先类型对象有三个操作簇:
- tp_as_number:对象为数值时,所支持的操作;
- tp_as_sequence:对象为序列时,所支持的操作;
- tp_as_mapping:对象为映射时,所支持的操作;
它们都是结构体指针,指向的结构体中的每一个成员都是一个函数指针,指向的函数便是实例对象可执行的操作。以 int 类型为例:
int在底层对应PyLong_Type,它的tp_as_number成员被初始化为&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。
重载操作符
先说明一下,我们这里针对的都是内置的类。如果是自定义的类,那么利用 Python 的动态特性就足够了。
类对象有 4 个方法簇,分别是 tp_as_number, tp_as_sequence, tp_as_mapping, tp_as_async。这个 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 not (name 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 的时候 # 那么解释器会执行我们重写的方法 # 比如字符串和整数相加会出现TypeError,因此解释器会执行我们重写的 __add__ print("你" + 123) # ('你', 123) # 但如果是调用魔方方法,那么会直接走我们重写的 __add__ print("你".__add__("好")) # ('你', '好')
不过上述这个问题在 3.6 版本的时候是没有的,操作符会无条件地执行我们重写的魔法方法。但在 3.8 的时候出现了这个现象,至于更高版本的 Python,可以自己测试一下。
小结
以上我们就用 ctypes 玩了一些骚操作,内容还是有点单调,当然你也可以玩的再嗨一些。但是无论如何,一定不要在生产上使用,线上不要出现这种会改变解释器运行逻辑的代码。如果只是为了调试、或者想从实践的层面更深入地了解虚拟机,那么没事可以玩一玩。