È possibile "hackerare" la funzione di stampa di Python?


151

Nota: questa domanda è solo a scopo informativo. Sono interessato a vedere quanto in profondità negli interni di Python è possibile andare con questo.

Non molto tempo fa, è iniziata una discussione all'interno di una certa domanda relativa alla possibilità di modificare le stringhe passate alle istruzioni di stampa dopo / durante la chiamata a print. Ad esempio, considera la funzione:

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

Ora, quando printviene eseguito, l'output al terminale dovrebbe visualizzare:

This dog was scared.

Notare che la parola "gatto" è stata sostituita dalla parola "cane". Qualcosa da qualche parte è stato in qualche modo in grado di modificare quei buffer interni per cambiare ciò che è stato stampato. Supponiamo che ciò avvenga senza il permesso esplicito dell'autore del codice originale (quindi, hacking / hijacking).

Questo commento del saggio @abarnert, in particolare, mi ha fatto pensare:

Ci sono un paio di modi per farlo, ma sono tutti molto brutti e non dovrebbero mai essere fatti. Il modo meno brutto è probabilmente sostituire l' codeoggetto all'interno della funzione con uno con un co_consts elenco diverso . Il prossimo è probabilmente raggiungere l'API C per accedere al buffer interno dello str. [...]

Quindi, sembra che questo sia effettivamente possibile.

Ecco il mio modo ingenuo di affrontare questo problema:

>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.

Certo, execè un male, ma questo non risponde davvero alla domanda, perché in realtà non modifica nulla durante il quando / dopo print viene chiamato.

Come sarebbe stato spiegato da @abarnert?


3
A proposito, la memoria interna per gli ints è molto più semplice delle stringhe e galleggia ancora di più. E, come bonus, è molto più evidente perché è una cattiva idea di modificare il valore di 42per 23rispetto perché è una cattiva idea di modificare il valore di "My name is Y"a "My name is X".
abarnert

Risposte:


244

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 è?


11
@ cᴏʟᴅsᴘᴇᴇᴅ Il munging del codice è probabilmente ragionevole in Python, anche se in genere si desidera toccare gli oggetti del codice solo per motivi molto migliori (ad esempio, eseguire il bytecode tramite un ottimizzatore personalizzato). Accedere alla memoria interna di un PyUnicodeObject, d'altra parte, è probabilmente solo Python, nel senso che un interprete Python lo eseguirà ...
abarnert

4
Il tuo primo frammento di codice aumenta NameError: name 'arg' is not defined. Forse cercavi: args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? Un modo forse meglio scrivere questo sarebbe: args = [str(arg).replace('cat', 'dog') for arg in args]. Un'altra opzione, ancora più breve,: args = map(lambda a: str(a).replace('cat', 'dog'), args). Questo ha l'ulteriore vantaggio che argsè pigro (che potrebbe anche essere ottenuto sostituendo la comprensione della lista sopra con una generatore - *argsfunziona in entrambi i modi).
Konstantin,

1
@ cᴏʟᴅsᴘᴇᴇᴅ Sì, IIRC sto solo usando la PyUnicodeObjectdefinizione di struttura, ma copiandolo nella risposta penserei solo di intralciarmi, e penso che il readme e / o i commenti dei sorgenti superhackyinternalsspieghino effettivamente come accedere al buffer (almeno abbastanza bene da ricordarmi la prossima volta che me ne importa; non sono sicuro che sarà abbastanza per qualcun altro ...), che non volevo entrare qui. La parte rilevante è come passare da un oggetto Python live alla sua PyObject *via ctypes. (E forse simulando l'aritmetica del puntatore, evitando char_pconversioni automatiche , ecc.)
abarnert

1
@ jpmc26 Non penso che sia necessario farlo prima di importare i moduli, purché lo si faccia prima della stampa. I moduli eseguiranno la ricerca del nome ogni volta, a meno che non si associno esplicitamente printa un nome. È inoltre possibile associare il nome printper loro: import yourmodule; yourmodule.print = badprint.
leewz,

1
@abarnert: ho notato che hai avvertito spesso di farlo (ad es. "non vuoi mai farlo" , "perché è una cattiva idea cambiare il valore" , ecc.). Non è esattamente chiaro cosa potrebbe andare storto (sarcasmo), saresti disposto ad approfondire un po '? Potrebbe essere d'aiuto per coloro che sono tentati di provarlo alla cieca.
L'L

37

Scimmia-patch print

printè una funzione incorporata, quindi utilizzerà la printfunzione definita nel builtinsmodulo (o __builtin__in Python 2). Quindi, ogni volta che si desidera modificare o cambiare il comportamento di una funzione integrata, è possibile semplicemente riassegnare il nome in quel modulo.

Questo processo è chiamato 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

Successivamente ogni printchiamata passerà custom_print, anche se si printtrova in un modulo esterno.

Tuttavia, in realtà non si desidera stampare testo aggiuntivo, si desidera modificare il testo stampato. Un modo per procedere è quello di sostituirlo nella stringa che verrebbe stampata:

_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

E infatti se corri:

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

O se lo scrivi in ​​un file:

test_file.py

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

print_something()

e importalo:

>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.

Quindi funziona davvero come previsto.

Tuttavia, nel caso in cui desideri stampare temporaneamente solo patch scimmia, puoi avvolgerlo in un gestore di contesto:

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

Quindi, quando lo esegui, dipende dal contesto che viene stampato:

>>> with ChangePrint() as x:
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Quindi è così che puoi "hackerare" printcon il patching delle scimmie.

Modifica il target anziché il print

Se guardi la firma di printnoterai un fileargomento che è sys.stdoutdi default. Nota che questo è un argomento predefinito dinamico ( cerca davverosys.stdout ogni volta che chiami print) e non come i normali argomenti predefiniti in Python. Quindi, se si cambia sys.stdout print, in realtà si stamperà sul diverso target ancora più conveniente che Python fornisca anche una redirect_stdoutfunzione (da Python 3.4 in poi, ma è facile creare una funzione equivalente per le versioni precedenti di Python).

Il rovescio della medaglia è che non funzionerà per le printdichiarazioni che non vengono stampate sys.stdoute che la creazione della propria stdoutnon è molto semplice.

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'))

Tuttavia, questo funziona anche:

>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Sommario

Alcuni di questi punti sono già stati menzionati da @abarnet ma volevo esplorare queste opzioni in modo più dettagliato. Soprattutto come modificarlo attraverso i moduli (usando builtins/ __builtin__) e come rendere quel cambiamento solo temporaneo (usando i gestori di contesto).


4
Sì, la cosa più vicina a questa domanda che qualcuno dovrebbe mai davvero voler fare è redirect_stdout, quindi è bello avere una risposta chiara che porti a questo.
abarnert,

6

Un modo semplice per acquisire tutto l'output da una printfunzione e quindi elaborarlo è quello di modificare il flusso di output in qualcos'altro, ad esempio un file.

Userò una PHPconvenzione di denominazione ( 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"))

Stampa

Ciao John Ciao John


5

Uniamo questo con l'introspezione del frame!

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}!")

Troverai questo trucco che precede ogni saluto con la funzione o il metodo di chiamata. Questo potrebbe essere molto utile per la registrazione o il debug; soprattutto perché ti consente di "dirottare" le istruzioni di stampa nel codice di terze parti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.