注:本问题仅供参考。我很想了解Python的内部结构有多深。
不久前,在某个question内部开始讨论是否可以在调用print
之后/期间修改传递给print语句的字符串。例如,考虑函数:
def print_something():
print('This cat was scared.')
现在,当运行print
时,到终端的输出应该显示:
This dog was scared.
注意,“猫”这个词已经被“狗”这个词取代了。不知怎么的,某处的某个东西能够修改这些内部缓冲区来更改打印的内容。假设这是在没有原始代码作者明确许可的情况下完成的(因此,黑客/劫持)。
这个comment特别是来自聪明的@abarnert,让我想到:
There are a couple of ways to do that, but they're all very ugly, and should never be done. The least ugly way is to probably replace the
code
object inside the function with one with a differentco_consts
list. Next is probably reaching into the C API to access the str's internal buffer. [...]
看来这是可能的。
下面是我解决这个问题的天真方法:
>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.
当然,exec
是不好的,但这并不能真正回答问题,因为它在调用时/调用后实际上没有修改任何内容。
如果abarnert已经解释过了怎么办?
猴子补丁
print
print
是一个内置函数,因此它将使用在builtins
模块(或Python 2中的__builtin__
)中定义的print
函数。因此,每当您想修改或更改内置函数的行为时,只需在该模块中重新分配名称即可。这个过程称为
monkey-patching
。之后,每个
print
调用都将通过custom_print
,即使print
在外部模块中。但是,您并不真的想打印其他文本,而是想更改打印的文本。一种方法是将其替换为要打印的字符串:
如果你真的跑了:
或者如果您将其写入文件:
测试文件.py
并导入:
所以它真的按预期工作。
但是,如果您只是暂时想对修补程序打印进行修改,可以将其包装在上下文管理器中:
因此,当运行时,它取决于打印的上下文:
所以你就可以通过猴子修补来“黑客”了。
修改目标而不是
print
如果您查看^{} 的签名,您会注意到一个} 函数(从Python 3.4开始,但是很容易为早期的Python版本创建等效函数)。
file
参数,默认情况下是sys.stdout
。注意,这是一个动态默认参数(每次调用print
时它都会查找sys.stdout
),而不像Python中的普通默认参数。因此,如果您更改sys.stdout
print
将实际打印到不同的目标,甚至更方便,Python还提供了^{缺点是它不适用于不打印到
sys.stdout
的print
语句,并且创建自己的stdout
并不是很简单。不过,这也适用:
摘要
abarnet已经提到了其中的一些要点,但我想更详细地探讨这些选项。尤其是如何跨模块修改它(使用
builtins
/__builtin__
)以及如何使更改仅为临时的(使用ContextManager)。首先,实际上有一种更简单的方法。我们要做的就是改变什么样的指纹,对吧?
或者,类似地,您可以使用monkeypatch
sys.stdout
,而不是print
。而且,这个想法没什么问题。好吧,当然有很多问题,但是比下面的要少
但如果您确实想修改函数对象的代码常量,我们可以这样做。
如果您真的想真正使用代码对象,那么应该使用类似^{} (完成时)或^{} (在此之前,或者对于较旧的Python版本)的库,而不是手动执行。即使对于这种琐碎的事情,
CodeType
初始值设定项也是一种痛苦;如果你真的需要做像修复lnotab
这样的事情,只有疯子才会手动完成。另外,不用说,并不是所有的Python实现都使用CPython风格的代码对象。这段代码将在CPython 3.7中工作,可能所有的版本都会返回到至少2.2版本,只做一些小的更改(不是代码黑客的东西,而是像生成器表达式之类的东西),但是它不会在任何版本的IronPython中工作。
破解代码对象会出什么问题?大多数情况下,只有分段错误,
RuntimeError
会吞噬整个堆栈,更正常的RuntimeError
是可以处理的,或者垃圾值可能只会在您尝试使用它们时引发TypeError
或AttributeError
。例如,尝试创建一个代码对象,其中只有一个RETURN_VALUE
,堆栈上没有任何内容(3.6+,b'S'
之前为字节码b'S\0'
),或者当字节码中有一个LOAD_CONST 0
时,为varnames
创建一个空元组,或者将varnames
减少1,这样最高的LOAD_FAST
实际加载一个freevar/cellvar单元格。为了获得一些真正的乐趣,如果您的lnotab
足够错误,那么您的代码将只在调试器中运行时segfault。使用
bytecode
或byteplay
并不能保护您免受所有这些问题的影响,但它们确实有一些基本的健全性检查,以及一些很好的帮助程序,可以让您做一些事情,比如插入一段代码,让它担心更新所有偏移和标签,这样您就不会出错,等等。(另外,它们使您不必键入荒谬的6行构造函数,也不必调试由此产生的愚蠢的拼写错误。)现在转到#2。
我提到代码对象是不可变的。当然,const是一个元组,所以我们不能直接改变它。常量元组中的东西是一个字符串,我们也不能直接改变它。这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象。
但是如果你能直接改变一个字符串呢?
好吧,在足够深的封面下,一切都只是指向一些C数据的指针,对吧?如果您使用的是CPython,则有a C API to access the objects,和you can use ^{} to access that API from within Python itself, which is such a terrible idea that they put a ^{} right there in the stdlib's ^{} module 。:)最重要的技巧是
id(x)
是指向内存中x
的实际指针(作为int
)。不幸的是,字符串的C API不能让我们安全地获得已经冻结的字符串的内部存储。所以小心点,让我们自己找个储藏室。
如果您使用的是CPython 3.4-3.7(对于较旧的版本,这是不同的,谁知道将来会发生什么),那么来自纯ASCII模块的字符串文字将使用紧凑的ASCII格式存储,这意味着结构会提前结束,ASCII字节的缓冲区会立即出现在内存中。如果在字符串中放入非ASCII字符或某些类型的非文本字符串,这将中断(可能是segfault),但您可以读取其他4种访问不同类型字符串的缓冲区的方法。
为了让事情稍微简单一点,我在GitHub上使用了^{} 项目。(故意不安装pip,因为您不应该使用它,除非是在本地构建解释器等方面进行试验。)
如果你想玩这些东西,
int
在封面下比str
简单得多。通过将2
的值更改为1
可以更容易地猜出可以打断什么,对吧?实际上,忘了想象吧,我们就这样做(再次使用superhackyinternals
中的类型):…假设代码框有一个无限长的滚动条。
我在IPython中尝试了同样的方法,第一次尝试在提示下计算
2
,它进入了某种不可中断的无限循环。假设它在REPL循环中使用数字2
,而stock解释器不是?捕获
print
函数的所有输出并对其进行处理的一种简单方法是将输出流更改为其他内容,例如文件。我将使用
PHP
命名约定(ob_start,ob_get_contents,…)用法:
将打印
相关问题 更多 >
编程相关推荐