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