Primero, en realidad hay una forma mucho menos hacky. Todo lo que queremos hacer es cambiar lo que print
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, puedes usar monkeypatch en sys.stdout
lugar 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 CodeType
inicializador 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, RuntimeError
s que se comen toda la pila, RuntimeError
s más normales que se pueden manejar o valores de basura que probablemente solo aumentarán una 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 por 1 para que el más alto LOAD_FAST
realmente cargue un freevar / cellvar cell. Para divertirse realmente, si se lnotab
equivoca lo suficiente, su código solo será predeterminado cuando se ejecute en el depurador.
Usar 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 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 ctypes
para acceder a esa API desde Python, lo cual es una idea tan terrible que ponen un pythonapi
derecho allí en el ctypes
módulo de stdlib . :) El truco más importante que necesita saber es que id(x)
es el puntero real x
en 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 superhackyinternals
proyecto 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, int
es 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 2
a 1
, ¿verdad? En realidad, olvida imaginarte, hagámoslo (usando los tipos de superhackyinternals
nuevo):
>>> 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á.
42
a23
que por qué es una mala idea cambiar el valor de"My name is Y"
a"My name is X"
.