首先,实际上有一种不那么老套的方法。我们要做的就是改变print 打印的内容,对吧?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
或者,类似地,您可以使用猴子补丁 sys.stdout 而不是 print。
另外,exec … getsource … 的想法也没有错。嗯,当然有很多错误,但比下面的要少……
但如果您确实想修改函数对象的代码常量,我们可以这样做。
如果你真的想真正地玩弄代码对象,你应该使用像bytecode(当它完成时)或byteplay(直到那时,或者对于旧的 Python 版本)这样的库,而不是手动进行。即使对于这种微不足道的事情,CodeType 初始化程序也很痛苦。如果你真的需要做一些事情,比如修复lnotab,只有疯子才会手动完成。
此外,不用说,并非所有 Python 实现都使用 CPython 样式的代码对象。此代码将在 CPython 3.7 中运行,并且可能所有版本都至少回到 2.2 并进行一些小的更改(不是代码黑客的东西,而是生成器表达式之类的东西),但它不适用于任何版本的 IronPython。
import types
def print_function():
print ("This cat was scared.")
def main():
# A function object is a wrapper around a code object, with
# a bit of extra stuff like default values and closure cells.
# See inspect module docs for more details.
co = print_function.__code__
# A code object is a wrapper around a string of bytecode, with a
# whole bunch of extra stuff, including a list of constants used
# by that bytecode. Again see inspect module docs. Anyway, inside
# the bytecode for string (which you can read by typing
# dis.dis(string) in your REPL), there's going to be an
# instruction like LOAD_CONST 1 to load the string literal onto
# the stack to pass to the print function, and that works by just
# reading co.co_consts[1]. So, that's what we want to change.
consts = tuple(c.replace("cat", "dog") if isinstance(c, str) else c
for c in co.co_consts)
# Unfortunately, code objects are immutable, so we have to create
# a new one, copying over everything except for co_consts, which
# we'll replace. And the initializer has a zillion parameters.
# Try help(types.CodeType) at the REPL to see the whole list.
co = types.CodeType(
co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
co.co_stacksize, co.co_flags, co.co_code,
consts, co.co_names, co.co_varnames, co.co_filename,
co.co_name, co.co_firstlineno, co.co_lnotab,
co.co_freevars, co.co_cellvars)
print_function.__code__ = co
print_function()
main()
破解代码对象会出现什么问题?大多数只是段错误,RuntimeErrors 会占用整个堆栈,更正常的RuntimeErrors 可以处理,或者当您尝试使用它们时可能只会引发TypeError 或AttributeError 的垃圾值。例如,尝试创建一个代码对象,其中只有一个 RETURN_VALUE 堆栈上没有任何内容(字节码 b'S\0' 用于 3.6+,b'S' 之前),或者当有一个 LOAD_CONST 0 时为 co_consts 创建一个空元组字节码,或者 varnames 减 1,所以最高的 LOAD_FAST 实际上加载了一个 freevar/cellvar 单元。为了一些真正的乐趣,如果你把lnotab 弄错了,你的代码只会在调试器中运行时出现段错误。
使用 bytecode 或 byteplay 不会保护您免受所有这些问题的影响,但它们确实有一些基本的健全性检查,以及可以让您执行诸如插入一段代码并让它担心的好帮手更新所有偏移量和标签,这样你就不会弄错了,等等。 (另外,它们使您不必输入那个荒谬的 6 行构造函数,也不必调试由此产生的愚蠢的拼写错误。)
现在进入 #2。
我提到代码对象是不可变的。当然 const 是一个元组,所以我们不能直接改变它。而 const 元组中的东西是一个字符串,我们也不能直接改变它。这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象。
但是如果你可以直接改变一个字符串呢?
好吧,在幕后,一切都只是指向一些 C 数据的指针,对吧?如果您使用的是 CPython,则有 a C API to access the objects 和 you can use ctypes to access that API from within Python itself, which is such a terrible idea that they put a pythonapi right there in the stdlib's ctypes module。 :) 您需要知道的最重要的技巧是id(x) 是内存中指向x 的实际指针(作为int)。
不幸的是,字符串的 C API 无法让我们安全地获取已冻结字符串的内部存储。所以放心吧,让我们read the header files 自己找到那个存储空间。
如果您使用 CPython 3.4 - 3.7(旧版本不同,谁知道未来),来自纯 ASCII 模块的字符串文字将使用紧凑的 ASCII 格式存储,该格式表示结构提前结束,ASCII 字节的缓冲区紧跟在内存中。如果您在字符串中放置非 ASCII 字符或某些类型的非文字字符串,这将中断(可能是段错误),但您可以阅读其他 4 种方法来访问不同类型字符串的缓冲区。
为了让事情稍微简单一些,我使用了我的 GitHub 上的 superhackyinternals 项目。 (它是故意不可安装的,因为您真的不应该使用它,除非您尝试本地构建的解释器等。)
import ctypes
import internals # https://github.com/abarnert/superhackyinternals/blob/master/internals.py
def print_function():
print ("This cat was scared.")
def main():
for c in print_function.__code__.co_consts:
if isinstance(c, str):
idx = c.find('cat')
if idx != -1:
# Too much to explain here; just guess and learn to
# love the segfaults...
p = internals.PyUnicodeObject.from_address(id(c))
assert p.compact and p.ascii
addr = id(c) + internals.PyUnicodeObject.utf8_length.offset
buf = (ctypes.c_int8 * 3).from_address(addr + idx)
buf[:3] = b'dog'
print_function()
main()
如果你想玩这些东西,int 在幕后比str 简单得多。通过将2 的值更改为1,更容易猜出你会破坏什么,对吧?实际上,忘记想象,让我们去做吧(再次使用来自superhackyinternals 的类型):
>>> n = 2
>>> pn = PyLongObject.from_address(id(n))
>>> pn.ob_digit[0]
2
>>> pn.ob_digit[0] = 1
>>> 2
1
>>> n * 3
3
>>> i = 10
>>> while i < 40:
... i *= 2
... print(i)
10
10
10
…假设代码框有一个无限长的滚动条。
我在 IPython 中尝试了同样的事情,当我第一次尝试在提示符下评估 2 时,它进入了某种不间断的无限循环。大概它在其 REPL 循环中使用数字 2 来表示某些东西,而股票解释器不是?