注意:此问题仅供参考。我很有兴趣看到Python的内部有多深入,可以使用它。
不久前,在某个question内开始讨论是否可以在调用print
之后/期间修改传递给print语句的字符串。例如,考虑函数:
def print_something():
print('This cat was scared.')
现在,当print
运行时,终端的输出应显示:
This dog was scared.
注意单词" cat"已经被“#34; dog"”这个词所取代。在某处某处能够修改那些内部缓冲区来改变打印的内容。假设这是在没有原始代码作者的明确许可的情况下完成的(因此,黑客攻击/劫持)。
明智的@abarnert的这个comment特别让我思考:
有几种方法可以做到这一点,但它们都非常难看,而且 永远不应该做。最不丑的方式是可能取代 函数内的
code
对象,其中一个具有不同的co_consts
名单。接下来可能会进入C API来访问str 内部缓冲区。 [...]
所以,看起来这实际上是可能的。
这是解决这个问题的天真方式:
>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.
当然,exec
很糟糕,但这并没有真正回答这个问题,因为它实际上并没有在 print
之后/之后修改任何调用。
如果@abarnert解释了它会怎么做?
答案 0 :(得分:234)
首先,实际上这种方式不那么苛刻。我们要做的就是改变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)
或者,类似地,您可以monkeypatch 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()
破解代码对象会出现什么问题?主要是段错误,RuntimeError
占用整个堆栈,可以处理更正常的RuntimeError
,或垃圾值可能只会引发TypeError
或AttributeError
当你尝试使用它们时。例如,尝试创建一个只有RETURN_VALUE
的代码对象,堆栈上没有任何内容(字节码b'S\0'
适用于3.6 +,b'S'
之前),或者带有{{1}的空元组当字节码中有co_consts
或者LOAD_CONST 0
递减1时,最高varnames
实际上加载了freevar / cellvar单元格。为了一些真正的乐趣,如果你的LOAD_FAST
错误,你的代码只会在调试器中运行时出现段错误。
使用lnotab
或bytecode
不会保护您免受所有这些问题的影响,但是他们确实有一些基本的健全性检查,并且可以帮助您做一些好帮手,例如插入一大块代码,让它担心更新所有偏移和标签,这样你就不会出错,等等。 (另外,它们使您不必键入那个荒谬的6行构造函数,并且必须调试这样做的愚蠢错别字。)
现在进入#2。
我提到代码对象是不可变的。当然,竞争对手是一个元组,因此我们无法直接改变它。 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。 :)你需要知道的最重要的技巧是byteplay
是内存中id(x)
的实际指针(作为x
)。
不幸的是,字符串的C API不会让我们安全地获取已经冻结的字符串的内部存储。所以请安全地使用,只需read the header files并自己找到存储空间。
如果您正在使用CPython 3.4 - 3.7(旧版本不同,以及谁知道将来),那么来自纯ASCII的模块的字符串文字就会出现使用紧凑的ASCII格式存储,这意味着struct早期结束,ASCII字节的缓冲区紧跟在内存中。如果在字符串中放入非ASCII字符或某些非文字字符串,这将会破坏(如可能是段错误),但您可以阅读其他4种方法来访问不同类型字符串的缓冲区。
为了让事情变得更轻松,我在GitHub上使用了superhackyinternals
项目。 (它故意不是可以安装的,因为你真的不应该使用它,除了试验你的本地构建的解释器之类的东西。)
int
如果你想玩这个东西,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
...假装代码框有一个无限长的滚动条。
我在IPython中尝试了同样的事情,并且第一次尝试在提示符下评估>>> 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
时,它进入了某种不间断的无限循环。据推测,它在其REPL循环中使用数字2
,而股票翻译不是?
答案 1 :(得分:34)
print
print
是内置函数,因此它将使用print
模块(或Python 2中的builtins
)中定义的__builtin__
函数。因此,无论何时您想要修改或更改内置函数的行为,您都可以简单地重新分配该模块中的名称。
此过程称为monkey-patching
。
# Store the real print function in another variable otherwise
# it will be inaccessible after being modified.
_print = print
# Actual implementation of the new print
def custom_print(*args, **options):
_print('custom print called')
_print(*args, **options)
# Change the print function globally
import builtins
builtins.print = custom_print
之后,即使print
位于外部模块中,每个custom_print
来电都会经过print
。
但是,如果您不想打印其他文字,则需要更改打印的文字。一种方法是在要打印的字符串中替换它:
_print = print
def custom_print(*args, **options):
# Get the desired seperator or the default whitspace
sep = options.pop('sep', ' ')
# Create the final string
printed_string = sep.join(args)
# Modify the final string
printed_string = printed_string.replace('cat', 'dog')
# Call the default print function
_print(printed_string, **options)
import builtins
builtins.print = custom_print
确实如果你跑:
>>> def print_something():
... print('This cat was scared.')
>>> print_something()
This dog was scared.
或者如果你把它写到文件中:
def print_something():
print('This cat was scared.')
print_something()
并导入它:
>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.
所以它确实按预期工作。
但是,如果您只是暂时想要猴子补丁打印,您可以将它包装在上下文管理器中:
import builtins
class ChangePrint(object):
def __init__(self):
self.old_print = print
def __enter__(self):
def custom_print(*args, **options):
# Get the desired seperator or the default whitspace
sep = options.pop('sep', ' ')
# Create the final string
printed_string = sep.join(args)
# Modify the final string
printed_string = printed_string.replace('cat', 'dog')
# Call the default print function
self.old_print(printed_string, **options)
builtins.print = custom_print
def __exit__(self, *args, **kwargs):
builtins.print = self.old_print
所以当你运行它时,它取决于上下文打印的内容:
>>> with ChangePrint() as x:
... test_file.print_something()
...
This dog was scared.
>>> test_file.print_something()
This cat was scared.
这样你就可以&#34; hack&#34; print
通过猴子修补。
print
如果您查看print
的签名,您会注意到file
参数默认为sys.stdout
。请注意,这是一个动态默认参数(每次调用sys.stdout
时真的查找print
)并且不像Python中的普通默认参数。因此,如果您更改sys.stdout
print
将实际打印到不同的目标更方便Python也提供redirect_stdout
函数(从Python 3.4开始,但它很容易创建早期Python版本的等效函数。)
缺点是,它不能用于print
不能打印到sys.stdout
的语句,并且创建自己的stdout
并不是真的简单。
import io
import sys
class CustomStdout(object):
def __init__(self, *args, **kwargs):
self.current_stdout = sys.stdout
def write(self, string):
self.current_stdout.write(string.replace('cat', 'dog'))
然而,这也有效:
>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
... test_file.print_something()
...
This dog was scared.
>>> test_file.print_something()
This cat was scared.
@abarnet已经提到了其中一些要点,但我想更详细地探讨这些选项。特别是如何跨模块修改它(使用builtins
/ __builtin__
)以及如何仅在临时(使用上下文管理器)进行更改。
答案 2 :(得分:6)
从print
函数捕获所有输出然后处理它的简单方法是将输出流更改为其他内容,例如:一个文件。
我将使用PHP
命名惯例(ob_start,ob_get_contents,...)
from functools import partial
output_buffer = None
print_orig = print
def ob_start(fname="print.txt"):
global print
global output_buffer
print = partial(print_orig, file=output_buffer)
output_buffer = open(fname, 'w')
def ob_end():
global output_buffer
close(output_buffer)
print = print_orig
def ob_get_contents(fname="print.txt"):
return open(fname, 'r').read()
用法:
print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))
会打印
嗨约翰 再见约翰
答案 3 :(得分:4)
让它与帧内省相结合!
import sys
_print = print
def print(*args, **kw):
frame = sys._getframe(1)
_print(frame.f_code.co_name)
_print(*args, **kw)
def greetly(name, greeting = "Hi")
print(f"{greeting}, {name}!")
class Greeter:
def __init__(self, greeting = "Hi"):
self.greeting = greeting
def greet(self, name):
print(f"{self.greeting}, {name}!")
你会发现这个技巧是使用调用函数或方法的每个问候语的前言。这对于日志记录或调试非常有用;特别是因为它可以让你劫持&#34;在第三方代码中打印语句。