注意:此问题仅供参考 . 我很有兴趣看到Python内部有多深入,可以使用它 .
不久前,关于在调用 print
之后/期间是否可以修改传递给print语句的字符串的讨论开始讨论了.1233127_ . 例如,考虑功能:
def print_something():
print('This cat was scared.')
现在,当运行 print
时,应显示到终端的输出:
This dog was scared.
请注意,“cat”一词已被“dog”一词取代 . 在某处某处能够修改那些内部缓冲区来改变打印的内容 . 假设这是在没有原始代码作者的明确许可的情况下完成的(因此,黑客/劫持) .
来自明智的@abarnert的这个comment特别让我思考:
有几种方法可以做到这一点,但它们都非常丑陋,永远不应该完成 . 最简单的方法是将函数内的代码对象替换为具有不同co_consts列表的代码对象 . 接下来可能会进入C API来访问str的内部缓冲区 . [...]
所以,看起来这实际上是可行的 .
这是我解决这个问题的天真方式:
>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.
当然, exec
很糟糕,但这并没有真正回答这个问题,因为在调用 print
之后/之后它实际上并没有修改任何东西 .
如果@abarnert解释了它会怎么做?
4 回答
首先,实际上是一种不那么黑客的方式 . 我们想做的就是改变
print
打印的内容吧?或者,类似地,你可以monkeypatch
sys.stdout
而不是print
.此外,
exec … getsource …
的想法没有错 . 嗯,当然它有很多错误,但不到这里的......但是如果你想修改函数对象的代码常量,我们就可以做到 .
如果你真的想要使用真实的代码对象,你应该使用像bytecode(当它完成时)或byteplay(直到那时,或者对于较旧的Python版本)而不是手动执行它的库 . 即使对于这个微不足道的事情,
CodeType
初始化程序也是一种痛苦;如果你真的需要做像修复lnotab
之类的东西,那么只有疯子才能手动完成 .此外,不言而喻,并非所有Python实现都使用CPython风格的代码对象 . 这段代码可以在CPython 3.7中运行,并且可能所有版本都返回到至少2.2并进行一些小的更改(而不是代码破解的东西,但是像生成器表达式这样的东西),但它不适用于任何版本的IronPython .
破解代码对象会出现什么问题?主要是段错误,
RuntimeError
s占用整个堆栈,可以处理的更正常RuntimeError
,或者当你尝试使用它们时可能只会引发TypeError
或AttributeError
的垃圾值 . 例如,尝试创建一个只有RETURN_VALUE
的代码对象,堆栈上没有任何东西(前面是字节码b'S\0'
,前面是b'S'
),或者当字节码中有LOAD_CONST 0
时,co_consts
为空元组,或者varnames
递减1,所以最高LOAD_FAST
实际上加载了freevar / cellvar单元格 . 为了一些真正的乐趣,如果你得到足够的错误,你的代码只会在调试器中运行时出现段错误 .使用
bytecode
或byteplay
赢了't protect you from all of those problems, but they do have some basic sanity checks, and nice helpers that let you do things like insert a chunk of code and let it worry about updating all offsets and labels so you can' t弄错了,依此类推 . (另外,它们使您不必键入那个荒谬的6行构造函数,并且必须调试这样做的愚蠢错别字 . )现在到#2 .
我提到代码对象是不可变的 . 当然,争论是一个元组,所以我们不能直接改变它 . const元组中的东西是一个字符串,我们也不能直接改变它 . 这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象 .
但是,如果你可以直接更改字符串怎么办?
好吧,在封面下足够深,一切都只是指向某些C数据的指针,对吧?如果你're using CPython, there' s 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赢得了't let us safely get at the internal storage of an already-frozen string. So screw safely, let'只是read the header files并自己找到了存储 .
如果你正在使用CPython 3.4 - 3.7(对于旧版本它是不同的,并且谁知道未来),将存储来自由纯ASCII组成的模块的字符串文字使用紧凑的ASCII格式,这意味着struct早期结束,ASCII字节的缓冲区紧跟在内存中 . 如果在字符串中放入非ASCII字符或某些非文字字符串,这将会破坏(如可能是段错误),但是您可以阅读其他4种方法来访问不同类型字符串的缓冲区 .
为了使事情稍微容易一些,我正在使用我的GitHub上的superhackyinternals项目 . (它除了试验你的本地构建的解释器之外,还要使用它 . )
如果你想玩这个东西,
int
在封面下比str
简单得多 . 通过将2
的值更改为1
可以更容易地猜出你可以打破什么,对吗?实际上,忘了想象,让我们这样做(再次使用superhackyinternals
中的类型):...假装代码框有一个无限长的滚动条 .
我在IPython中尝试了同样的事情,并且第一次尝试在提示符下评估
2
时,它进入了某种不间断的无限循环 . 据推测,它在REPL循环中使用数字2
,而股票解释器不是?Monkey-patch打印
print
是内置函数,因此它将使用builtins
模块中定义的print
函数(或Python 2中的__builtin__
) . 因此,无论何时您想要修改或更改内置函数的行为,您都可以简单地重新分配该模块中的名称 .此过程称为
monkey-patching
.之后,即使
print
在外部模块中,每个print
调用都将通过custom_print
.但是,您实际上并不想打印其他文本,而是希望更改打印的文本 . 一种方法是在要打印的字符串中替换它:
事实上,如果你跑:
或者,如果您将其写入文件:
test_file.py
并导入它:
所以它确实按预期工作 .
但是,如果您只是暂时想要猴子补丁打印,您可以将其包装在上下文管理器中:
所以当你运行它时,它取决于上下文打印的内容:
这就是你如何通过猴子修补来实现的.1233195_
print
修改目标而不是打印
如果您查看print的签名,您会注意到
file
参数,默认情况下为sys.stdout
. 请注意,这是一个动态默认参数(每次调用print
时 really 都会查找sys.stdout
),而不像Python中的普通默认参数 . 因此,如果你改变sys.stdout
print
将实际打印到不同的目标更方便Python也提供redirect_stdout函数(从Python 3.4开始,但很容易为早期的Python版本创建一个等效函数) .缺点是它不适用于不打印到
sys.stdout
的print
语句,并且创建自己的stdout
并不是那么简单 .但是这也有效:
摘要
@abarnet已经提到了其中一些要点,但我想更详细地探讨这些选项 . 特别是如何跨模块修改它(使用
builtins
/__builtin__
)以及如何仅在临时(使用contextmanagers)进行更改 .捕获
print
函数的所有输出然后处理它的一种简单方法是将输出流更改为其他内容,例如,一份文件 .我将使用
PHP
命名约定(ob_start,ob_get_contents,...)用法:
会打印
让我们结合框架内省吧!
你会发现这个技巧是使用调用函数或方法的每个问候语的前言 . 这对于日志记录或调试非常有用;特别是因为它允许你“劫持”第三方代码中的打印语句 .