如何猴子修补python列表__setitem__方法

时间:2016-07-08 01:19:43

标签: python ctypes introspection

我想修补Python列表,特别是用自定义代码替换__setitem__方法。请注意,我不是试图扩展,而是覆盖内置类型。例如:

>>> # Monkey Patch  
... # Replace list.__setitem__ with a Noop
...
>>> myList = [1,2,3,4,5]
>>> myList[0] = "Nope"
>>> myList
[1, 2, 3, 4, 5]

是的,我知道对于python代码来说,这是一个彻头彻尾的变态事情。不,我的用例并没有多大意义。尽管如此,可以做到吗?

可能的途径:

示范示例

我实际上设法覆盖了方法本身,如下所示:

import ctypes

def magic_get_dict(o):
    # find address of dict whose offset is stored in the type
    dict_addr = id(o) + type(o).__dictoffset__
    # retrieve the dict object itself
    dict_ptr = ctypes.cast(dict_addr, ctypes.POINTER(ctypes.py_object))
    return dict_ptr.contents.value

def magic_flush_mro_cache():
    ctypes.PyDLL(None).PyType_Modified(ctypes.cast(id(object), ctypes.py_object))

print(list.__setitem__)
dct = magic_get_dict(list)
dct['__setitem__'] = lambda s, k, v: s
magic_flush_mro_cache()
print(list.__setitem__)

x = [1,2,3,4,5]
print(x.__setitem__)
x.__setitem__(0,10)
x[1] = 20
print(x)

其中输出以下内容:

➤ python3 override.py
<slot wrapper '__setitem__' of 'list' objects>
<function <lambda> at 0x10de43f28>
<bound method <lambda> of [1, 2, 3, 4, 5]>
[1, 20, 3, 4, 5]

但是如输出所示,这似乎不会影响设置项目的正常语法(x[0] = 0

替代方案:猴子修补单个列表实例

作为一个较小的替代方案,如果我能够修补单个列表的实例,这也可以工作。也许通过将列表的类指针更改为自定义类。

2 个答案:

答案 0 :(得分:3)

聚会有点晚了,不过,这就是答案。

正如 user2357112 在上面的评论中所暗示的,修改 dict 是不够的,因为 __getitme__(和其他双下划线名称)被映射到它们的插槽,并且不会在不调用 {{1 }}(没有导出,所以会有点棘手)。

受上述评论的启发,这里有一个使 update_slot 成为特定列表的空操作的工作示例:

__setitem__

编辑
请参阅 here 为什么不支持开始。主要是作为 explained by Guido van Rossum:

<块引用>

这是故意禁止的,以防止对内置类型的意外致命更改(对您从未想过的代码部分是致命的)。此外,这样做是为了防止更改影响驻留在地址空间中的不同解释器,因为内置类型(与用户定义的类不同)在所有此类解释器之间共享。

我还搜索了 cpython# assuming v3.8 (tested on Windows x64 and Ubuntu x64) # definition of PyTypeObject: https://github.com/python/cpython/blob/3.8/Include/cpython/object.h#L177 # no extensive testing was performed and I'll let other decide if this is a good idea or not, but it's possible import ctypes Py_TPFLAGS_HEAPTYPE = (1 << 9) # calculate the offset of the tp_flags field offset = ctypes.sizeof(ctypes.c_ssize_t) * 1 # PyObject_VAR_HEAD.ob_base.ob_refcnt offset += ctypes.sizeof(ctypes.c_void_p) * 1 # PyObject_VAR_HEAD.ob_base.ob_type offset += ctypes.sizeof(ctypes.c_ssize_t) * 1 # PyObject_VAR_HEAD.ob_size offset += ctypes.sizeof(ctypes.c_void_p) * 1 # tp_name offset += ctypes.sizeof(ctypes.c_ssize_t) * 2 # tp_basicsize+tp_itemsize offset += ctypes.sizeof(ctypes.c_void_p) * 1 # tp_dealloc offset += ctypes.sizeof(ctypes.c_ssize_t) * 1 # tp_vectorcall_offset offset += ctypes.sizeof(ctypes.c_void_p) * 7 # tp_getattr+tp_setattr+tp_as_async+tp_repr+tp_as_number+tp_as_sequence+tp_as_mapping offset += ctypes.sizeof(ctypes.c_void_p) * 6 # tp_hash+tp_call+tp_str+tp_getattro+tp_setattro+tp_as_buffer tp_flags = ctypes.c_ulong.from_address(id(list) + offset) assert(tp_flags.value == list.__flags__) # should be the same lst1 = [1,2,3] lst2 = [1,2,3] dont_set_me = [lst1] # these lists cannot be set # define new method orig = list.__setitem__ def new_setitem(self, *args): if [_ for _ in dont_set_me if _ is self]: # check for identical object in list print('Nope') else: return orig(self, *args) tp_flags.value |= Py_TPFLAGS_HEAPTYPE # add flag, to allow type_setattro to continue list.__setitem__ = new_setitem # set method, this will already call PyType_Modified and update_slot tp_flags.value &= (~Py_TPFLAGS_HEAPTYPE) # remove flag print(lst1, lst2) # > [1, 2, 3] [1, 2, 3] lst1[0],lst2[0]='x','x' # > Nope print(lst1, lst2) # > [1, 2, 3] ['x', 2, 3] 的所有用法,它们似乎都与 GC 或某些验证有关。

所以我想如果:

  • 你不会改变类型结构(我相信上面没有)
  • 您没有在同一个进程中使用多个解释器
  • 您移除标志并立即将其恢复为单线程状态
  • 当标志被移除时,你实际上并没有做任何会影响 GC 的事情

你没事<此处的通用免责声明>。

答案 1 :(得分:0)

无法完成。如果你强制使用CTypes,那么你将比其他任何东西更快地崩溃Python运行时 - 因为很多事情只是使用Python数据类型。