¿Es posible "hackear" la función de impresión de Python?

151

Nota: Esta pregunta es solo para fines informativos. Me interesa ver qué tan profundo en el interior de Python es posible llegar con esto.

No hace mucho tiempo, comenzó una discusión dentro de una determinada pregunta con respecto a si las cadenas pasadas a las declaraciones impresas podrían modificarse después / durante la llamada print. Por ejemplo, considere la función:

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

Ahora, cuando printse ejecuta, la salida al terminal debería mostrar:

This dog was scared.

Observe que la palabra "gato" ha sido reemplazada por la palabra "perro". Algo en alguna parte pudo modificar esos búferes internos para cambiar lo que se imprimió. Suponga que esto se hace sin el permiso explícito del autor del código original (por lo tanto, piratería / secuestro).

Este comentario del sabio @abarnert, en particular, me hizo pensar:

Hay un par de formas de hacerlo, pero todas son muy feas y nunca deberían hacerse. La forma menos fea es probablemente reemplazar el codeobjeto dentro de la función con uno con una co_consts lista diferente . Lo siguiente es probablemente llegar a la API de C para acceder al búfer interno del str. [...]

Entonces, parece que esto es realmente posible.

Aquí está mi manera ingenua de abordar este problema:

>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.

Por supuesto, execes malo, pero eso realmente no responde a la pregunta, porque en realidad no modifica nada durante la print llamada cuando / después .

¿Cómo se haría como lo explicó @abarnert?

cs95
fuente
3
Por cierto, el almacenamiento interno para ints es mucho más simple que las cadenas y flota aún más. Y, como beneficio adicional, es mucho más obvio por qué es una mala idea cambiar el valor de 42a 23que por qué es una mala idea cambiar el valor de "My name is Y"a "My name is X".
abarnert

Respuestas:

244

Primero, en realidad hay una forma mucho menos hacky. Todo lo que queremos hacer es cambiar lo que printimprime, ¿verdad?

_print = print
def print(*args, **kw):
    args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
            for arg in args)
    _print(*args, **kw)

O, de manera similar, puedes usar monkeypatch en sys.stdoutlugar de print.


Además, no hay nada malo con la exec … getsource …idea. Bueno, por supuesto, hay muchas cosas malas, pero menos de lo que sigue aquí ...


Pero si desea modificar las constantes de código del objeto de función, podemos hacerlo.

Si realmente quiere jugar con objetos de código de verdad, debe usar una biblioteca como bytecode(cuando esté terminada) o byteplay(hasta entonces, o para versiones anteriores de Python) en lugar de hacerlo manualmente. Incluso para algo tan trivial, el CodeTypeinicializador es un dolor; Si realmente necesita hacer cosas como arreglar lnotab, solo un loco lo haría manualmente.

Además, no hace falta decir que no todas las implementaciones de Python usan objetos de código de estilo CPython. Este código funcionará en CPython 3.7, y probablemente todas las versiones vuelvan al menos a 2.2 con algunos cambios menores (y no las cosas de pirateo de códigos, sino cosas como expresiones generadoras), pero no funcionará con ninguna versión de 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()

¿Qué podría salir mal al hackear objetos de código? La mayoría de las veces son solo segfaults, RuntimeErrors que se comen toda la pila, RuntimeErrors más normales que se pueden manejar o valores de basura que probablemente solo aumentarán una TypeErroro AttributeErrorcuando intentes usarlos. Por ejemplo, intente crear un objeto de código con solo un RETURN_VALUEsin nada en la pila (bytecode b'S\0'para 3.6+, b'S'antes), o con una tupla vacía para co_constscuando hay un LOAD_CONST 0en el bytecode, o con varnamesdecrementado por 1 para que el más alto LOAD_FASTrealmente cargue un freevar / cellvar cell. Para divertirse realmente, si se lnotabequivoca lo suficiente, su código solo será predeterminado cuando se ejecute en el depurador.

Usar bytecodeo byteplayno lo protegerá de todos esos problemas, pero tienen algunas comprobaciones básicas de cordura y buenos ayudantes que le permiten hacer cosas como insertar un fragmento de código y dejar que se preocupe por actualizar todas las compensaciones y etiquetas para que pueda ' No te equivoques, y así sucesivamente. (Además, evitan que tengas que escribir ese ridículo constructor de 6 líneas y tener que depurar los errores tontos que surgen al hacerlo).


Ahora al # 2.

Mencioné que los objetos de código son inmutables. Y, por supuesto, los concursos son una tupla, por lo que no podemos cambiar eso directamente. Y la cosa en la tupla constante es una cadena, que tampoco podemos cambiar directamente. Es por eso que tuve que construir una nueva cadena para construir una nueva tupla para construir un nuevo objeto de código.

Pero, ¿y si pudieras cambiar una cadena directamente?

Bueno, lo suficientemente profundo debajo de las cubiertas, todo es solo un puntero a algunos datos C, ¿verdad? Si está utilizando CPython, hay una API C para acceder a los objetos , y puede usarla ctypespara acceder a esa API desde Python, lo cual es una idea tan terrible que ponen un pythonapiderecho allí en el ctypesmódulo de stdlib . :) El truco más importante que necesita saber es que id(x)es el puntero real xen la memoria (como un int).

Desafortunadamente, la API de C para cadenas no nos permitirá acceder de forma segura al almacenamiento interno de una cadena ya congelada. Así que atornille con seguridad, solo leamos los archivos de encabezado y encontremos ese almacenamiento nosotros mismos.

Si está utilizando CPython 3.4 - 3.7 (es diferente para versiones anteriores, y quién sabe para el futuro), un literal de cadena de un módulo que está hecho de ASCII puro se almacenará utilizando el formato ASCII compacto, lo que significa la estructura termina temprano y el búfer de bytes ASCII sigue inmediatamente en la memoria. Esto se interrumpirá (como probablemente en segfault) si coloca un carácter no ASCII en la cadena, o ciertos tipos de cadenas no literales, pero puede leer sobre las otras 4 formas de acceder al búfer para diferentes tipos de cadenas.

Para hacer las cosas un poco más fáciles, estoy usando el superhackyinternalsproyecto en mi GitHub. (Intencionalmente no es pip-installable porque realmente no deberías usar esto excepto para experimentar con tu versión local del intérprete y similares).

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

Si quieres jugar con estas cosas, intes mucho más simple bajo las sábanas que str. Y es mucho más fácil adivinar qué puedes romper cambiando el valor de 2a 1, ¿verdad? En realidad, olvida imaginarte, hagámoslo (usando los tipos de superhackyinternalsnuevo):

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

... finja que el cuadro de código tiene una barra de desplazamiento de longitud infinita.

Intenté lo mismo en IPython, y la primera vez que intenté evaluar 2en el indicador, entró en una especie de bucle infinito ininterrumpido. Presumiblemente está usando el número 2para algo en su ciclo REPL, mientras que el intérprete de valores no lo está.

abarnert
fuente
11
@ cᴏʟᴅsᴘᴇᴇᴅ La codificación de código es posiblemente Python razonable, aunque generalmente solo desea tocar objetos de código por razones mucho mejores (por ejemplo, ejecutar el código de bytes a través de un optimizador personalizado). Accediendo al almacenamiento interno de un PyUnicodeObject, por otro lado, eso es probablemente solo Python en el sentido de que un intérprete de Python lo ejecutará ...
abarnert
44
Su primer fragmento de código aumenta NameError: name 'arg' is not defined. ¿Quiso decir args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]:? Una forma podría decirse que mejor escribir esto sería: args = [str(arg).replace('cat', 'dog') for arg in args]. Otro, aún más corto, opción: args = map(lambda a: str(a).replace('cat', 'dog'), args). Esto tiene el beneficio adicional de que argses perezoso (lo que también podría lograrse reemplazando la comprensión de la lista anterior con un generador, *argsfunciona de cualquier manera).
Konstantin
1
@ cᴏʟᴅsᴘᴇᴇᴅ Sí, IIRC Solo estoy usando la PyUnicodeObjectdefinición de estructura, pero al copiar eso en la respuesta, creo que solo me interpongo, y creo que el léame y / o los comentarios de la fuente para superhackyinternalsexplicar realmente cómo acceder al búfer (al menos lo suficientemente bien como para recordarme la próxima vez que me importe; no estoy seguro de si será suficiente para alguien más ...), que no quería entrar aquí. La parte relevante es cómo pasar de un objeto Python en vivo a su PyObject *vía ctypes. (Y tal vez simulando aritmética de puntero, evitando char_pconversiones automáticas , etc.)
abarnert
1
@ jpmc26 No creo que deba hacerlo antes de importar módulos, siempre que lo haga antes de que se impriman. Los módulos harán la búsqueda del nombre cada vez, a menos que se unan explícitamente printa un nombre. También puede enlazar el nombre printpara ellos: import yourmodule; yourmodule.print = badprint.
leewz
1
@abarnert: Me he dado cuenta de que a menudo has advertido sobre hacer esto (por ejemplo, "nunca quieres hacer esto realmente" , "por qué es una mala idea cambiar el valor" , etc.). No está exactamente claro qué podría salir mal (sarcasmo), ¿estaría dispuesto a explicar un poco más sobre eso? Posiblemente podría ayudar a aquellos tentados a intentarlo ciegamente.
l'L'l
37

Parche de mono print

printes una función integrada, por lo que utilizará la printfunción definida en el builtinsmódulo (o __builtin__en Python 2). Por lo tanto, cada vez que desee modificar o cambiar el comportamiento de una función integrada, simplemente puede reasignar el nombre en ese módulo.

Este proceso se llama 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

Después de eso, todas las printllamadas se realizarán custom_print, incluso si printestán en un módulo externo.

Sin embargo, realmente no desea imprimir texto adicional, desea cambiar el texto que se imprime. Una forma de hacerlo es reemplazarlo en la cadena que se imprimiría:

_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

Y de hecho si corres:

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

O si escribe eso en un archivo:

test_file.py

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

print_something()

e importarlo:

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

Por lo tanto, realmente funciona según lo previsto.

Sin embargo, en caso de que solo desee temporalmente imprimir un parche de mono, puede envolver esto en un administrador de contexto:

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

Entonces, cuando ejecuta eso, depende del contexto lo que se imprime:

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

Así es como puedes "hackear" printparches de mono.

Modificar el objetivo en lugar del print

Si mira la firma de print, notará un fileargumento que es sys.stdoutpor defecto. Tenga en cuenta que este es un argumento predeterminado dinámico ( realmente se ve sys.stdoutcada vez que llama print) y no como los argumentos predeterminados normales en Python. Entonces, si cambia sys.stdout print, en realidad se imprimirá en el destino diferente, incluso más conveniente que Python también proporciona una redirect_stdoutfunción (desde Python 3.4 en adelante, pero es fácil crear una función equivalente para versiones anteriores de Python).

La desventaja es que no funcionará para las printdeclaraciones que no se imprimen sys.stdouty que crear las suyas stdoutno es realmente sencillo.

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

Sin embargo, esto también funciona:

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

Resumen

@Abarnet ya ha mencionado algunos de estos puntos, pero quería explorar estas opciones con más detalle. Especialmente cómo modificarlo a través de módulos (usando builtins/ __builtin__) y cómo hacer que ese cambio solo sea temporal (usando contextmanagers).

MSeifert
fuente
44
Sí, lo más parecido a esta pregunta que alguien debería querer hacer es redirect_stdout, por lo que es bueno tener una respuesta clara que conduzca a eso.
abarnert
6

Una forma sencilla de capturar toda la salida de una printfunción y luego procesarla es cambiar la secuencia de salida a otra cosa, por ejemplo, un archivo.

Usaré una PHPconvención de nomenclatura ( 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()

Uso:

print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))

Imprimir

Hola john adios john

Uri Goren
fuente
5

¡Combinemos esto con la introspección de cuadros!

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

Encontrarás este truco antes de cada saludo con la función o método de llamada. Esto puede ser muy útil para iniciar sesión o depurar; especialmente porque le permite "secuestrar" declaraciones impresas en código de terceros.

Rafaël Dera
fuente