tutorial python3 org official modules documentacion docs commands python python-3.x printing python-internals

python3 - python libraries



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

Nota: Esta pregunta es solo para fines informativos. Estoy interesado en ver qué tan profundo en el interior de Python es posible ir con esto.

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

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

Ahora, cuando se ejecuta la print , 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 fue capaz de 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 comment 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 objeto de code dentro de la función con uno con una lista diferente de co_consts . 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, exec es malo, pero eso realmente no responde a la pregunta, ya que en realidad no modifica nada durante o cuando se llama a print .

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


Estampado de mono

print es una función incorporada, por lo que utilizará la función print definida en el módulo builtins (o __builtin__ en Python 2). Entonces, cada vez que desee modificar o cambiar el comportamiento de una función incorporada, 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, cada llamada de print pasará por custom_print , incluso si la print está 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 se podría "piratear" la print con parches de mono.

Modificar el objetivo en lugar de la print

Si observa la firma de print , notará un argumento de file que es sys.stdout por defecto. Tenga en cuenta que este es un argumento predeterminado dinámico ( realmente busca sys.stdout cada vez que llama a print ) y no como argumentos predeterminados normales en Python. Entonces, si cambia sys.stdout print se imprimirá en el destino diferente, incluso más conveniente que Python también proporciona una función redirect_stdout (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 print declaraciones de print que no se imprimen en sys.stdout y que crear su propia stdout no 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 en los módulos (usando builtins / __builtin__ ) y cómo hacer que ese cambio solo sea temporal (usando contextmanagers).


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


Primero, en realidad hay una forma mucho menos hacky. Todo lo que queremos hacer es cambiar lo que imprime, ¿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, puede monkeypatch sys.stdout lugar de print .

Además, no hay nada malo con el 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é terminado) o byteplay (hasta entonces, o para versiones anteriores de Python) en lugar de hacerlo manualmente. Incluso para algo tan trivial, el inicializador CodeType es un fastidio; 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ódigo, 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, RuntimeError s que se comen toda la pila, más RuntimeError s normales que se pueden manejar o valores basura que probablemente solo generarán un TypeError o AttributeError cuando intentes usarlos. Por ejemplo, intente crear un objeto de código con solo un RETURN_VALUE sin nada en la pila (bytecode b''S/0'' para 3.6+, b''S'' antes), o con una tupla vacía para co_consts cuando hay un LOAD_CONST 0 en el bytecode, o con varnames decrementado en 1, por lo que LOAD_FAST más LOAD_FAST realmente carga una celda freevar / cellvar. Para divertirse realmente, si obtiene el lnotab lo suficientemente incorrecto, su código solo se segfault cuando se ejecuta en el depurador.

El uso de bytecode o byteplay no 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 puedo equivocarme, y así sucesivamente. (Además, evitan que tengas que escribir ese ridículo constructor de 6 líneas y tener que depurar los errores tipográficos que surgen de 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 usar ctypes para acceder a esa API desde Python, lo cual es una idea tan terrible que ponen un pythonapi allí mismo en el módulo ctypes de ctypes . :) El truco más importante que necesita saber es que id(x) es el puntero real a x en la memoria (como 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 proyecto superhackyinternals de mi GitHub. (Intencionalmente no es pip-installable porque realmente no deberías usar esto excepto para experimentar con tu compilació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, int es mucho más simple bajo las sábanas que str . Y es mucho más fácil adivinar qué puede romper cambiando el valor de 2 a 1 , ¿verdad? En realidad, olvida imaginarte, hagámoslo (usando los tipos de superhackyinternals nuevamente):

>>> 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 2 en el indicador, entró en una especie de bucle infinito ininterrumpido. Presumiblemente está usando el número 2 para algo en su ciclo REPL, mientras que el intérprete de valores no lo está.


Una forma sencilla de capturar todos los resultados de una función de print y luego procesarlos es cambiar el flujo de salida a otra cosa, por ejemplo, un archivo.

ob_start convenciones de nomenclatura 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()

Uso:

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

Imprimir

Hola john adios john