Innanzitutto, c'è in realtà un modo molto meno confuso. Tutto quello che vogliamo fare è cambiare ciò che printstampa, giusto?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Oppure, allo stesso modo, puoi monkeypatch sys.stdoutinvece di print.
Inoltre, nulla di sbagliato exec … getsource …nell'idea. Beh, ovviamente c'è molto di sbagliato in questo, ma meno di quello che segue qui ...
Ma se si desidera modificare le costanti di codice dell'oggetto funzione, possiamo farlo.
Se vuoi davvero giocare con oggetti di codice per davvero, dovresti usare una libreria come bytecode(quando è finita) o byteplay(fino ad allora, o per le versioni precedenti di Python) invece di farlo manualmente. Anche per qualcosa di così banale, l' CodeTypeinizializzatore è un dolore; se hai davvero bisogno di fare cose come sistemare lnotab, solo un pazzo lo farebbe manualmente.
Inoltre, è ovvio che non tutte le implementazioni di Python utilizzano oggetti di codice in stile CPython. Questo codice funzionerà in CPython 3.7 e probabilmente tutte le versioni tornano almeno alla 2.2 con alcune piccole modifiche (e non le cose di hacking del codice, ma cose come le espressioni del generatore), ma non funzioneranno con nessuna versione di 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()
Cosa potrebbe andare storto con l'hacking di oggetti in codice? Principalmente solo segfault, RuntimeErrorche consumano l'intero stack, più normali RuntimeErrorche possono essere gestiti, o valori di immondizia che probabilmente aumenteranno solo TypeErroro AttributeErrorquando si tenta di usarli. Ad esempio, prova a creare un oggetto codice con solo un RETURN_VALUEsenza nulla nello stack (bytecode b'S\0'per 3.6+, b'S'prima), o con una tupla vuota per co_constsquando c'è un LOAD_CONST 0nel bytecode o con varnamesdecrementato di 1 in modo che il più alto LOAD_FASTcarichi effettivamente un freevar / cellvar cell. Per un vero divertimento, se lnotabsbagli abbastanza, il tuo codice segfault solo quando eseguito nel debugger.
Utilizzando bytecodeo byteplaynon ti proteggeranno da tutti questi problemi, ma hanno alcuni controlli di integrità di base e simpatici aiutanti che ti consentono di fare cose come inserire un pezzo di codice e lasciarti preoccupare di aggiornare tutti gli offset e le etichette in modo da poter ' non sbagliare, e così via. (Inoltre, ti impediscono di dover digitare quel ridicolo costruttore di 6 righe e di dover eseguire il debug degli errori di battitura che derivano dal farlo.)
Passiamo ora al n. 2.
Ho detto che gli oggetti di codice sono immutabili. E ovviamente i contro sono una tupla, quindi non possiamo cambiarlo direttamente. E la cosa nella tupla const è una stringa, che anche noi non possiamo cambiare direttamente. Ecco perché ho dovuto creare una nuova stringa per creare una nuova tupla per creare un nuovo oggetto codice.
E se potessi cambiare direttamente una stringa?
Bene, abbastanza in profondità sotto le coperte, tutto è solo un puntatore ad alcuni dati C, giusto? Se stai usando CPython, c'è un'API C per accedere agli oggetti e puoi usare ctypesper accedere pythonapia quell'API dall'interno di Python stesso, il che è un'idea così terribile che si inseriscono nel ctypesmodulo di stdlib . :) Il trucco più importante che devi sapere è che id(x)è il puntatore reale xin memoria (come int).
Sfortunatamente, l'API C per le stringhe non ci consente di accedere in modo sicuro alla memoria interna di una stringa già bloccata. Quindi avvitatelo in sicurezza, leggiamo solo i file di intestazione e troviamo da soli l'archiviazione.
Se stai usando CPython 3.4 - 3.7 (è diverso per le versioni precedenti e chissà per il futuro), una stringa letterale da un modulo fatto di puro ASCII verrà memorizzata usando il formato compatto ASCII, il che significa che la struttura termina presto e il buffer di byte ASCII segue immediatamente in memoria. Questo si interromperà (come nel caso probabilmente del segfault) se si inserisce un carattere non ASCII nella stringa o determinati tipi di stringhe non letterali, ma è possibile leggere gli altri 4 modi per accedere al buffer per diversi tipi di stringhe.
Per semplificare leggermente le cose, sto usando il superhackyinternalsprogetto su GitHub. (Non è intenzionalmente installabile tramite pip perché in realtà non dovresti usarlo se non per sperimentare la tua build locale dell'interprete e simili.)
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()
Se vuoi giocare con queste cose, intè molto più semplice sotto le coperte di str. Ed è molto più facile indovinare cosa puoi rompere cambiando il valore di 2a 1, giusto? In realtà, dimentica di immaginare, facciamolo (usando di superhackyinternalsnuovo i tipi ):
>>> 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
... fingi che la casella del codice abbia una barra di scorrimento di lunghezza infinita.
Ho provato la stessa cosa in IPython e la prima volta che ho provato a valutare 2al prompt, è andato in una specie di loop infinito ininterrotto. Presumibilmente sta usando il numero 2per qualcosa nel suo ciclo REPL, mentre l'interprete di borsa non lo è?
42per23rispetto perché è una cattiva idea di modificare il valore di"My name is Y"a"My name is X".