Is it possible to “hack” Python's print function?

前端 未结 4 736
灰色年华
灰色年华 2020-11-30 18:02

Note: This question is for informational purposes only. I am interested to see how deep into Python\'s internals it is possible to go with this.

Not very long ago, a

4条回答
  •  死守一世寂寞
    2020-11-30 18:13

    Monkey-patch print

    print is a builtin function so it will use the print function defined in the builtins module (or __builtin__ in Python 2). So whenever you want to modify or change the behavior of a builtin function you can simply reassign the name in that module.

    This process is called 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
    

    After that every print call will go through custom_print, even if the print is in an external module.

    However you don't really want to print additional text, you want to change the text that is printed. One way to go about that is to replace it in the string that would be printed:

    _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
    

    And indeed if you run:

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

    Or if you write that to a file:

    test_file.py

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

    and import it:

    >>> import test_file
    This dog was scared.
    >>> test_file.print_something()
    This dog was scared.
    

    So it really works as intended.

    However, in case you only temporarily want to monkey-patch print you could wrap this in a context-manager:

    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
    

    So when you run that it depends on the context what is printed:

    >>> with ChangePrint() as x:
    ...     test_file.print_something()
    ... 
    This dog was scared.
    >>> test_file.print_something()
    This cat was scared.
    

    So that's how you could "hack" print by monkey-patching.

    Modify the target instead of the print

    If you look at the signature of print you'll notice a file argument which is sys.stdout by default. Note that this is a dynamic default argument (it really looks up sys.stdout every time you call print) and not like normal default arguments in Python. So if you change sys.stdout print will actually print to the different target even more convenient that Python also provides a redirect_stdout function (from Python 3.4 on, but it's easy to create an equivalent function for earlier Python versions).

    The downside is that it won't work for print statements that don't print to sys.stdout and that creating your own stdout isn't really straightforward.

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

    However this also works:

    >>> import contextlib
    >>> with contextlib.redirect_stdout(CustomStdout()):
    ...     test_file.print_something()
    ... 
    This dog was scared.
    >>> test_file.print_something()
    This cat was scared.
    

    Summary

    Some of these points have already be mentioned by @abarnet but I wanted to explore these options in more detail. Especially how to modify it across modules (using builtins/__builtin__) and how to make that change only temporary (using contextmanagers).

提交回复
热议问题