Nota: Esta pregunta es solo para fines informativos. Estoy interesado en ver qué tan profundo en las partes internas de Python es posible llegar con esto.
No hace mucho tiempo, comenzó una discusión dentro de cierta pregunta sobre si las cadenas pasadas para imprimir declaraciones 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 a la terminal debería mostrar:
This dog was scared.
Observe que la palabra "gato" ha sido reemplazada por la palabra "perro". Algo en algún lugar de alguna manera 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 desagradables y nunca deben hacerse. La forma menos fea es probablemente reemplazar el objeto de
code
dentro de la función con uno con una lista diferente deco_consts
. Lo siguiente probablemente sea acceder a la API de C para acceder al búfer interno de str. [...]
Entonces, parece que esto es realmente posible.
Aquí está mi forma 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, porque en realidad no modifica nada durante cuando/después de que se llama a print
.
¿Cómo se haría como lo ha explicado @abarnert?
Primero, en realidad hay una manera mucho menos hacky. Todo lo que queremos hacer es cambiar lo que print
, ¿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 parchear sys.stdout
en lugar de print
.
Además, no hay nada malo con la idea exec … getsource …
Bueno, por supuesto que hay mucho de malo en ello, 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 una molestia; si realmente necesita hacer cosas como arreglar lnotab
, solo un lunático 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 a por lo menos 2.2 con algunos cambios menores (y no las cosas de pirateo de código, sino cosas como expresiones de generador), 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 con la piratería de objetos de código? En su mayoría, solo segfaults, RuntimeError
s que consumen toda la pila, RuntimeError
más normales que se pueden manejar, o valores basura que probablemente generarán un TypeError
o AttributeError
cuando intente usarlos. Por ejemplo, intente crear un objeto de código con solo 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 LOAD_CONST 0
en el bytecode, o con varnames
disminuido en 1, por lo que el LOAD_FAST
más alto en realidad carga una celda freevar/cellvar. Para divertirse de verdad, si se equivoca lo suficiente en lnotab
, su código solo generará un error de segmento cuando se ejecute en el depurador.
El uso de bytecode
o byteplay
no lo protegerá de todos esos problemas, pero tienen algunas comprobaciones de cordura básicas 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 puede equivocarse, y así sucesivamente. (Además, le evitan tener que escribir ese ridículo constructor de 6 líneas y tener que depurar los errores tipográficos tontos que surgen al hacerlo).
Ahora vamos al #2.
Mencioné que los objetos de código son inmutables. Y, por supuesto, las constantes son una tupla, por lo que no podemos cambiar eso directamente. Y la cosa en la tupla const 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 sábanas, todo es solo un puntero a algunos datos C, ¿verdad? Si está usando CPython, hay una API C para acceder a los objetos , y puede usar ctypes
para acceder a esa API desde dentro de Python, lo cual es una idea tan terrible que pusieron un pythonapi
allí mismo en el módulo ctypes de ctypes
. :) El truco más importante que debes saber es que id(x)
es el puntero real a x
en la memoria (como un int
).
Desafortunadamente, la API de C para cadenas no nos permitirá acceder con seguridad al almacenamiento interno de una cadena ya congelada. Así que jodan 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á usando el formato ASCII compacto, lo que significa que la estructura termina antes y el búfer de bytes ASCII sigue inmediatamente en la memoria. Esto se romperá (como en probablemente un error de segmento) si coloca un carácter que no sea ASCII en la cadena, o ciertos tipos de cadenas no literales, pero puede leer las otras 4 formas de acceder al búfer para diferentes tipos de cadenas.
Para facilitar un poco las cosas, estoy usando el proyecto superhackyinternals
de mi GitHub. (Intencionalmente, no se puede instalar con pip porque realmente no debería usar esto, excepto para experimentar con su 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 debajo de las sábanas que str
. Y es mucho más fácil adivinar lo que puedes romper cambiando el valor de 2
a 1
, ¿verdad? En realidad, olvídate de imaginar, 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
… pretender 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 bucle REPL, mientras que el intérprete de stock no lo está.
print
print
es una función incorporada, por lo que utilizará la función de print
definida en el módulo __builtin__
builtins
Python 2). Entonces, siempre 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, cada llamada 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 ejecutas:
>>> def print_something(): ... print('This cat was scared.') >>> print_something() This dog was scared.
O si escribes eso en un archivo:
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.
Así que realmente funciona según lo previsto.
Sin embargo, en caso de que solo desee imprimir parches de mono temporalmente, 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 podrías "hackear" print
con parches de mono.
print
Si observa la firma de print
, notará un argumento de file
que es sys.stdout
de forma predeterminada. Tenga en cuenta que este es un argumento predeterminado dinámico ( realmente busca sys.stdout
cada vez que llama a print
) y no como los argumentos predeterminados normales en Python. Entonces, si cambia sys.stdout
, print
se imprimirá en el destino diferente, lo que es aún más conveniente porque 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 declaraciones de print
que no se imprimen en sys.stdout
y que crear su propia stdout
estándar 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.
@abarnet ya mencionó algunos de estos puntos, pero quería explorar estas opciones con más detalle. Especialmente cómo modificarlo entre módulos (usando builtins
/ __builtin__
) y cómo hacer que ese cambio sea solo temporal (usando contextmanagers).
Una forma sencilla de capturar toda la salida de una función de print
y luego procesarla es cambiar el flujo de salida a otra cosa, por ejemplo, un archivo.
Usaré las convenciones de nomenclatura de 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ía
hola juan adios juan