用一篇文章告诉你如何篡改 Python 虚拟机

简介: 用一篇文章告诉你如何篡改 Python 虚拟机

楔子




我们来介绍一个好玩的,看看如何修改 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

当然了,用数组实现大整数的思路其实没什么新鲜的,难点在于大整数的数学运算的具体实现,它们才是重点,也是比较考验编程功底的地方。



image.svg

字节序列



image.svg


字节序列就是 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 入手,我们看一下它的底层结构。


e3837fae415129284769dbc84b177e48.png

所谓的 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__ 方法啊。


首先类型对象有三个操作簇:


19f47a43598c2905d8e997ddd278c04f.png


  • tp_as_number:对象为数值时,所支持的操作;
  • tp_as_sequence:对象为序列时,所支持的操作;
  • tp_as_mapping:对象为映射时,所支持的操作;


它们都是结构体指针,指向的结构体中的每一个成员都是一个函数指针,指向的函数便是实例对象可执行的操作。以 int 类型为例:


00c2909fc83083e5a76402bfabd72df3.png


int在底层对应PyLong_Type,它的tp_as_number成员被初始化为&long_as_number,我们来看一下。


8f4fb3aae75ba6c9302879b156676b88.png


因此 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 玩了一些骚操作,内容还是有点单调,当然你也可以玩的再嗨一些。但是无论如何,一定不要在生产上使用,线上不要出现这种会改变解释器运行逻辑的代码。如果只是为了调试、或者想从实践的层面更深入地了解虚拟机,那么没事可以玩一玩。

相关文章
|
21天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
17天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2564 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
15天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
13天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
17天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1556 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
19天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
828 14
|
14天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
621 7
|
8天前
|
Docker 容器
Docker操作 (五)
Docker操作 (五)
170 69
|
8天前
|
Docker 容器
Docker操作 (三)
Docker操作 (三)
167 69
|
19天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
629 53
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界