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

cs9*_*s95 149 python printing python-3.x python-internals

注意:此问题仅供参考.我很有兴趣看到Python内部有多深入,可以使用它.

不久前,在一个问题内部开始讨论是否可以在调用之后/期间修改传递给print语句的字符串print.例如,考虑功能:

def print_something():
    print('This cat was scared.')
Run Code Online (Sandbox Code Playgroud)

现在,当print运行时,输出到终端应该显示:

This dog was scared.
Run Code Online (Sandbox Code Playgroud)

请注意,"cat"一词已被"dog"一词取代.在某处某处能够修改那些内部缓冲区来改变打印的内容.假设这是在没有原始代码作者的明确许可的情况下完成的(因此,黑客/劫持).

这个评论从智者@abarnert,尤其让我思考:

有几种方法可以做到这一点,但它们都非常丑陋,永远不应该完成.最简单的方法是code将函数内的对象替换为 具有不同co_consts 列表的对象.接下来可能会进入C API来访问str的内部缓冲区.[...]

所以,看起来这实际上是可行的.

这是我解决这个问题的天真方式:

>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.
Run Code Online (Sandbox Code Playgroud)

当然,这exec很糟糕,但这并没有真正回答这个问题,因为 print调用时/后调用它实际上并没有修改任何内容.

如果@abarnert解释了它会怎么做?

aba*_*ert 237

首先,实际上是一种不那么黑客的方式.我们要做的就是改变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)
Run Code Online (Sandbox Code Playgroud)

或者,类似地,你可以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()
Run Code Online (Sandbox Code Playgroud)

破解代码对象会出现什么问题?通常只是段错误,RuntimeError占用整个堆栈的RuntimeErrors,可以处理的更正常的s,或者可能只是引发TypeError或者AttributeError当你尝试使用它们时的垃圾值.例如,尝试创建一个只有RETURN_VALUE堆栈上没有任何内容的代码对象(之前b'S\0'是3.6+的字节码b'S'),或者co_constsLOAD_CONST 0字节码中有a时使用空元组,或者varnames递减1以使最高LOAD_FAST实际加载freevar/cellvar细胞.为了一些真正的乐趣,如果你lnotab弄错了,你的代码只会在调试器中运行时出现段错误.

使用bytecodebyteplay不会保护您免受所有这些问题的影响,但它们确实有一些基本的健全性检查,并且可以帮助您执行诸如插入大量代码之类的操作,并让它担心更新所有偏移和标签,以便您可以"弄错了,依此类推.(另外,它们使您不必键入那个荒谬的6行构造函数,并且必须调试这样做的愚蠢错别字.)


现在到#2.

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

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

好吧,在封面下足够深,一切都只是指向某些C数据的指针,对吧?如果你正在使用CPython,那么有一个C API来访问这些对象,你可以使用ctypes它从Python本身访问那个API,这是一个非常糟糕的想法,他们把它放在pythonapistdlib的ctypes模块中.:)你需要知道的最重要的技巧是它id(x)x内存中的实际指针(作为int).

不幸的是,字符串的C API不会让我们安全地获取已经冻结的字符串的内部存储.所以请安全地拧紧,让我们自己阅读头文件并找到存储空间.

如果你正在使用CPython 3.4 - 3.7(对于旧版本它是不同的,并且谁知道未来),来自由纯ASCII组成的模块的字符串文字将使用紧凑的ASCII格式存储,这意味着结构早期结束,ASCII字节的缓冲区紧跟在内存中.如果在字符串中放入非ASCII字符或某些非文字字符串,这将会破坏(如可能是段错误),但是您可以阅读其他4种方法来访问不同类型字符串的缓冲区.

为了使事情稍微容易一些,我正在使用superhackyinternals我的GitHub上的项目.(它故意不是可以安装的,因为你真的不应该使用它,除了试验你的本地构建的解释器之类的东西.)

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()
Run Code Online (Sandbox Code Playgroud)

如果你想玩这个东西,int在封面下比简单得多str.通过改变2to 的值来猜测你可以打破什么更容易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
Run Code Online (Sandbox Code Playgroud)

...假装代码框有一个无限长的滚动条.

我在IPython中尝试了同样的事情,并且我第一次尝试2在提示符下进行评估时,它进入了某种不间断的无限循环.据推测,它2在REPL循环中使用了数字,而股票解释器却没有?

  • @cᴏʟᴅsᴘᴇᴇᴅ代码修改是_arguably_合理的Python,尽管你通常只想更好地触摸代码对象(例如,通过自定义优化器运行字节码).另一方面,访问`PyUnicodeObject`的内部存储,这可能只是Python,因为Python解释器将运行它...... (11认同)
  • 你的第一个代码片段引发了`NameError:name'arg'未定义`.你的意思是:`args = [arg.replace('cat','dog')if isinstance(arg,str)else arg for arg in args]`?写这个的一个可以说是更好的方法是:`args = [str(arg).replace('cat','dog')arg in args]`.另一个甚至更短的选项:`args = map(lambda a:str(a).replace('cat','dog'),args)`.这有一个额外的好处,即`args`是懒惰的(这也可以通过用生成器替换上面的列表理解来实现 - "*args`以任何方式工作). (4认同)

MSe*_*ert 35

猴补丁 print

print是一个内置函数,因此它将使用模块(或Python 2)中print定义的函数.因此,无论何时您想要修改或更改内置函数的行为,您都可以简单地重新分配该模块中的名称.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
Run Code Online (Sandbox Code Playgroud)

之后,即使是在外部模块中,每个print呼叫都将通过.custom_printprint

但是,您实际上并不想打印其他文本,而是希望更改打印的文本.一种方法是在要打印的字符串中替换它:

_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
Run Code Online (Sandbox Code Playgroud)

事实上,如果你跑:

>>> def print_something():
...     print('This cat was scared.')
>>> print_something()
This dog was scared.
Run Code Online (Sandbox Code Playgroud)

或者,如果您将其写入文件:

test_file.py

def print_something():
    print('This cat was scared.')

print_something()
Run Code Online (Sandbox Code Playgroud)

并导入它:

>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.
Run Code Online (Sandbox Code Playgroud)

所以它确实按预期工作.

但是,如果您只是暂时想要猴子补丁打印,您可以将其包装在上下文管理器中:

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
Run Code Online (Sandbox Code Playgroud)

所以当你运行它时,它取决于上下文打印的内容:

>>> with ChangePrint() as x:
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.
Run Code Online (Sandbox Code Playgroud)

这就是你如何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'))
Run Code Online (Sandbox Code Playgroud)

但是这也有效:

>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.
Run Code Online (Sandbox Code Playgroud)

摘要

@abarnet已经提到了其中一些要点,但我想更详细地探讨这些选项.特别是如何跨模块修改它(使用builtins/ __builtin__)以及如何仅在临时(使用上下文管理器)进行更改.

  • 是的,任何人都应该真正想做的最接近这个问题的是`redirect_stdout`,所以有一个明确的答案导致这一点很好. (4认同)

Uri*_*ren 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()
Run Code Online (Sandbox Code Playgroud)

用法:

print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))
Run Code Online (Sandbox Code Playgroud)

会打印

嗨John Bye John


小智 5

让我们将其与框架内省结合起来!

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}!")
Run Code Online (Sandbox Code Playgroud)

你会发现这个技巧以调用函数或方法作为每个问候的开头。这对于记录或调试可能非常有用;特别是因为它可以让您“劫持”第三方代码中的打印语句。