首页 文章

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

提问于
浏览
138

注意:此问题仅供参考 . 我很有兴趣看到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 回答

  • 5

    首先,实际上是一种不那么黑客的方式 . 我们想做的就是改变 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 s占用整个堆栈,可以处理的更正常 RuntimeError ,或者当你尝试使用它们时可能只会引发 TypeErrorAttributeError 的垃圾值 . 例如,尝试创建一个只有 RETURN_VALUE 的代码对象,堆栈上没有任何东西(前面是字节码 b'S\0' ,前面是 b'S' ),或者当字节码中有 LOAD_CONST 0 时, co_consts 为空元组,或者 varnames 递减1,所以最高 LOAD_FAST 实际上加载了freevar / cellvar单元格 . 为了一些真正的乐趣,如果你得到足够的错误,你的代码只会在调试器中运行时出现段错误 .

    使用 bytecodebyteplay 赢了'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 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 . :)你需要知道的最重要的技巧是 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项目 . (它除了试验你的本地构建的解释器之外,还要使用它 . )

    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 中的类型):

    >>> 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
    

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

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

  • 222

    Monkey-patch打印

    print 是内置函数,因此它将使用 builtins 模块中定义的 print 函数(或Python 2中的 __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 在外部模块中,每个 print 调用都将通过 custom_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.
    

    这就是你如何通过猴子修补来实现的.1233195_ print

    修改目标而不是打印

    如果您查看print的签名,您会注意到 file 参数,默认情况下为 sys.stdout . 请注意,这是一个动态默认参数(每次调用 printreally 都会查找 sys.stdout ),而不像Python中的普通默认参数 . 因此,如果你改变 sys.stdout print 将实际打印到不同的目标更方便Python也提供redirect_stdout函数(从Python 3.4开始,但很容易为早期的Python版本创建一个等效函数) .

    缺点是它不适用于不打印到 sys.stdoutprint 语句,并且创建自己的 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__ )以及如何仅在临时(使用contextmanagers)进行更改 .

  • 30

    捕获 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"))
    

    会打印

    嗨John Bye John

  • 2

    让我们结合框架内省吧!

    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}!")
    

    你会发现这个技巧是使用调用函数或方法的每个问候语的前言 . 这对于日志记录或调试非常有用;特别是因为它允许你“劫持”第三方代码中的打印语句 .

相关问题