是否有可能" hack" Python的打印功能?

时间:2018-03-14 07:18:42

标签: python python-3.x printing python-internals

注意:此问题仅供参考。我很有兴趣看到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解释了它会怎么做?

4 个答案:

答案 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,或垃圾值可能只会引发TypeErrorAttributeError当你尝试使用它们时。例如,尝试创建一个只有RETURN_VALUE的代码对象,堆栈上没有任何内容(字节码b'S\0'适用于3.6 +,b'S'之前),或者带有{{1}的空元组当字节码中有co_consts或者LOAD_CONST 0递减1时,最高varnames实际上加载了freevar / cellvar单元格。为了一些真正的乐趣,如果你的LOAD_FAST错误,你的代码只会在调试器中运行时出现段错误。

使用lnotabbytecode不会保护您免受所有这些问题的影响,但是他们确实有一些基本的健全性检查,并且可以帮助您做一些好帮手,例如插入一大块代码,让它担心更新所有偏移和标签,这样你就不会出错,等等。 (另外,它们使您不必键入那个荒谬的6行构造函数,并且必须调试这样做的愚蠢错别字。)

现在进入#2。

我提到代码对象是不可变的。当然,竞争对手是一个元组,因此我们无法直接改变它。 const元组中的东西是一个字符串,我们也不能直接改变它。这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象。

但是,如果你可以直接更改字符串呢?

嗯,在封面下足够深,一切都只是指向某些C数据的指针,对吧?如果您正在使用CPython,则会a C API to access the objectsyou 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)

Monkey-patch 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.

或者如果你把它写到文件中:

test_file.py

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_startob_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;在第三方代码中打印语句。