Ist es möglich, die Druckfunktion von Python zu "hacken"?


151

Hinweis: Diese Frage dient nur zu Informationszwecken. Ich bin gespannt, wie tief es in Pythons Interna geht.

Vor nicht allzu langer Zeit begann eine Diskussion innerhalb einer bestimmten Frage, ob die an print-Anweisungen übergebenen Zeichenfolgen nach / während des Aufrufs von geändert werden könnten print. Betrachten Sie zum Beispiel die Funktion:

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

Wenn nun ausgeführt printwird, sollte die Ausgabe an das Terminal Folgendes anzeigen:

This dog was scared.

Beachten Sie, dass das Wort "Katze" durch das Wort "Hund" ersetzt wurde. Irgendwo konnte diese internen Puffer irgendwie geändert werden, um das zu ändern, was gedruckt wurde. Angenommen, dies erfolgt ohne die ausdrückliche Erlaubnis des ursprünglichen Code-Autors (daher Hacking / Hijacking).

Insbesondere dieser Kommentar des weisen @abarnert brachte mich zum Nachdenken:

Es gibt verschiedene Möglichkeiten, dies zu tun, aber sie sind alle sehr hässlich und sollten niemals durchgeführt werden. Der am wenigsten hässliche Weg besteht darin, das codeObjekt innerhalb der Funktion wahrscheinlich durch ein Objekt mit einer anderen co_consts Liste zu ersetzen . Next greift wahrscheinlich in die C-API, um auf den internen Puffer des Str zuzugreifen. [...]

Es sieht also so aus, als wäre dies tatsächlich möglich.

Hier ist meine naive Herangehensweise an dieses Problem:

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

Natürlich execist es schlecht, aber das beantwortet die Frage nicht wirklich, da es während des Aufrufs von / nach nichts ändert print.

Wie würde es gemacht werden, wie @abarnert es erklärt hat?


3
Übrigens ist der interne Speicher für Ints viel einfacher als Strings und schwebt noch mehr. Und als Bonus ist es viel offensichtlicher, warum es eine schlechte Idee ist, den Wert von 42in zu ändern , 23als warum es eine schlechte Idee ist, den Wert von "My name is Y"in zu ändern "My name is X".
abarnert

Antworten:


244

Erstens gibt es tatsächlich einen viel weniger hackigen Weg. Wir wollen nur ändern, welche printDrucke gedruckt werden, oder?

_print = print
def print(*args, **kw):
    args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
            for arg in args)
    _print(*args, **kw)

Oder Sie können auch Monkeypatch sys.stdoutanstelle von print.


Auch nichts falsch mit der exec … getsource …Idee. Natürlich ist daran viel auszusetzen, aber weniger als das, was hier folgt ...


Wenn Sie jedoch die Codekonstanten des Funktionsobjekts ändern möchten, können wir dies tun.

Wenn Sie wirklich mit Codeobjekten herumspielen möchten, sollten Sie eine Bibliothek wie bytecode(wenn sie fertig ist) oder byteplay(bis dahin oder für ältere Python-Versionen) verwenden, anstatt sie manuell auszuführen. Selbst für etwas so Triviales ist der CodeTypeInitialisierer ein Schmerz; Wenn Sie tatsächlich Dinge wie das Reparieren lnotaberledigen müssen, würde dies nur ein Verrückter manuell tun.

Es versteht sich auch von selbst, dass nicht alle Python-Implementierungen Codeobjekte im CPython-Stil verwenden. Dieser Code funktioniert in CPython 3.7 und wahrscheinlich alle Versionen auf mindestens 2.2 mit ein paar geringfügigen Änderungen (und nicht das Code-Hacking-Zeug, sondern Dinge wie Generatorausdrücke), aber er funktioniert mit keiner Version von 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()

Was könnte beim Hacken von Codeobjekten schief gehen? Meistens nur Segfaults, RuntimeErrors, die den gesamten Stapel verschlingen, normalere RuntimeErrors, die gehandhabt werden können, oder Müllwerte, die wahrscheinlich nur ein TypeErroroder auslösen, AttributeErrorwenn Sie versuchen, sie zu verwenden. Versuchen Sie beispielsweise, ein Codeobjekt mit nur einem RETURN_VALUEmit nichts auf dem Stapel (Bytecode b'S\0'für 3.6+, b'S'vorher) oder mit einem leeren Tupel zu erstellen, co_constswenn sich ein LOAD_CONST 0Bytecode im Bytecode befindet, oder mit einem varnamesDekrement von 1, damit der höchste LOAD_FASTtatsächlich eine Freevar lädt / cellvar cell. Für echten Spaß, wenn Sie das lnotabFalsche genug bekommen, wird Ihr Code nur dann fehlerhaft, wenn er im Debugger ausgeführt wird.

Verwenden bytecodeoder byteplayschützen Sie nicht vor all diesen Problemen, aber es gibt einige grundlegende Überprüfungen der Integrität und nette Helfer, mit denen Sie beispielsweise einen Teil des Codes einfügen und sich Gedanken über die Aktualisierung aller Offsets und Beschriftungen machen können, damit Sie dies tun können. ' Versteh es nicht falsch und so weiter. (Außerdem verhindern sie, dass Sie diesen lächerlichen 6-Zeilen-Konstruktor eingeben und die dummen Tippfehler, die daraus entstehen, debuggen müssen.)


Nun zu # 2.

Ich erwähnte, dass Codeobjekte unveränderlich sind. Und natürlich sind die Konstanten ein Tupel, also können wir das nicht direkt ändern. Und das Ding im const-Tupel ist ein String, den wir auch nicht direkt ändern können. Deshalb musste ich eine neue Zeichenfolge erstellen, um ein neues Tupel zu erstellen, um ein neues Codeobjekt zu erstellen.

Aber was wäre, wenn Sie eine Zeichenfolge direkt ändern könnten?

Nun, tief genug unter der Decke ist alles nur ein Zeiger auf einige C-Daten, oder? Wenn Sie CPython verwenden, gibt es eine C-API für den Zugriff auf die Objekte , und Sie können über ctypesPython selbst auf diese API zugreifen. pythonapiDies ist eine so schreckliche Idee, dass sie genau dort im ctypesModul der stdlib abgelegt werden . :) Der wichtigste Trick, den Sie wissen müssen, id(x)ist der tatsächliche Zeiger auf xim Speicher (als int).

Leider können wir mit der C-API für Zeichenfolgen nicht sicher in den internen Speicher einer bereits eingefrorenen Zeichenfolge gelangen. Also sicher schrauben, lasst uns einfach die Header-Dateien lesen und diesen Speicher selbst finden.

Wenn Sie CPython 3.4 - 3.7 verwenden (es ist anders für ältere Versionen und wer weiß für die Zukunft), wird ein Zeichenfolgenliteral aus einem Modul, das aus reinem ASCII besteht, im kompakten ASCII-Format gespeichert, dh der Struktur endet früh und der Puffer von ASCII-Bytes folgt sofort im Speicher. Dies wird (wie in wahrscheinlich segfault) unterbrochen, wenn Sie ein Nicht-ASCII-Zeichen in die Zeichenfolge oder bestimmte Arten von nicht-wörtlichen Zeichenfolgen einfügen. Sie können jedoch die anderen vier Möglichkeiten für den Zugriff auf den Puffer für verschiedene Arten von Zeichenfolgen nachlesen.

Um die Sache etwas einfacher zu machen, verwende ich das superhackyinternalsProjekt von meinem GitHub. (Es ist absichtlich nicht pip-installierbar, da Sie dies wirklich nicht verwenden sollten, außer um mit Ihrem lokalen Build des Interpreters und dergleichen zu experimentieren.)

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

Wenn Sie mit diesem Zeug spielen wollen, intist es unter der Decke viel einfacher als str. Und es ist viel einfacher zu erraten, was Sie durch Ändern des Werts von 2auf brechen können 1, oder? Vergiss die Vorstellung, lass es uns einfach tun (mit den Typen von superhackyinternalswieder):

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

… Stellen Sie sich vor, dass das Codefeld eine Bildlaufleiste mit unendlicher Länge hat.

Ich habe dasselbe in IPython versucht, und als ich das erste Mal versuchte, 2an der Eingabeaufforderung auszuwerten , ging es in eine Art unterbrechungsfreie Endlosschleife. Vermutlich verwendet es die Nummer 2für etwas in seiner REPL-Schleife, während der Aktieninterpreter dies nicht tut?


11
@ cᴏʟᴅsᴘᴇᴇᴅ Das Code-Munging ist wohl vernünftiges Python, obwohl Sie Code-Objekte im Allgemeinen nur aus viel besseren Gründen berühren möchten (z. B. indem Sie den Bytecode über einen benutzerdefinierten Optimierer ausführen). Zugriff auf den internen Speicher von a PyUnicodeObject, das ist wahrscheinlich wirklich nur Python in dem Sinne, dass ein Python-Interpreter es ausführen wird…
abarnert

4
Ihr erstes Code-Snippet wird ausgelöst NameError: name 'arg' is not defined. Meinten Sie : args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? Ein wohl besserer Weg, dies zu schreiben, wäre : args = [str(arg).replace('cat', 'dog') for arg in args]. Eine weitere, noch kürzere Option : args = map(lambda a: str(a).replace('cat', 'dog'), args). Dies hat den zusätzlichen Vorteil, dass argses faul ist (was auch erreicht werden könnte, indem das obige Listenverständnis durch ein Generatorverständnis ersetzt wird - *argsfunktioniert so oder so).
Konstantin

1
@ cᴏʟᴅsᴘᴇᴇᴅ Ja, IIRC Ich verwende nur die PyUnicodeObjectStrukturdefinition, aber das Kopieren in die Antwort würde meiner Meinung nach nur stören , und ich denke, dass die Readme- und / oder Quellkommentare superhackyinternalstatsächlich erklären, wie man auf den Puffer zugreift (zumindest) gut genug, um mich das nächste Mal daran zu erinnern; ich bin mir nicht sicher, ob es für irgendjemanden anderen ausreicht…), auf den ich hier nicht eingehen wollte. Der relevante Teil ist, wie man von einem Live-Python-Objekt zu seiner PyObject *Via gelangt ctypes. (Und vielleicht Zeigerarithmetik simulieren, automatische char_pKonvertierungen vermeiden usw.)
abarnert

1
@ jpmc26 Ich glaube nicht, dass Sie dies vor dem Importieren von Modulen tun müssen , solange Sie dies tun, bevor sie gedruckt werden. Module führen jedes Mal die Namenssuche durch, es sei denn, sie sind explizit printan einen Namen gebunden . Sie können auch den Namen printfür sie binden : import yourmodule; yourmodule.print = badprint.
Leez

1
@abarnert: Ich habe bemerkt, dass Sie oft davor gewarnt haben (z. B. "Sie möchten dies nie wirklich tun" , "warum es eine schlechte Idee ist, den Wert zu ändern" usw.). Es ist nicht genau klar, was möglicherweise schief gehen könnte (Sarkasmus). Würden Sie gerne etwas näher darauf eingehen? Es könnte möglicherweise für diejenigen helfen, die versucht sind, es blind zu versuchen.
l'L'l

37

Affenpflaster print

printist eine eingebaute Funktion, die printdie im builtinsModul (oder __builtin__in Python 2) definierte Funktion verwendet . Wenn Sie also das Verhalten einer integrierten Funktion ändern oder ändern möchten, können Sie den Namen in diesem Modul einfach neu zuweisen.

Dieser Vorgang wird aufgerufen 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

Danach wird jeder printAnruf durchlaufen custom_print, auch wenn sich der printin einem externen Modul befindet.

Sie möchten jedoch nicht wirklich zusätzlichen Text drucken, sondern den gedruckten Text ändern. Eine Möglichkeit, dies zu erreichen, besteht darin, es in der Zeichenfolge zu ersetzen, die gedruckt werden soll:

_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

Und in der Tat, wenn Sie laufen:

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

Oder wenn Sie das in eine Datei schreiben:

test_file.py

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

print_something()

und importiere es:

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

Es funktioniert also wirklich wie beabsichtigt.

Wenn Sie jedoch nur vorübergehend einen Affen-Patch drucken möchten, können Sie dies in einen Kontext-Manager einbinden:

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

Wenn Sie also ausführen, hängt es vom Kontext ab, was gedruckt wird:

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

So könnte man also printdurch Affen-Patches "hacken" .

Ändern Sie das Ziel anstelle des print

Wenn Sie sich die Signatur von ansehen, werden printSie ein fileArgument bemerken , das sys.stdoutstandardmäßig verwendet wird. Beachten Sie, dass dies ein dynamisches Standardargument ist (es wird bei jedem Aufruf wirklich nachgeschlagen ) und nicht wie normale Standardargumente in Python. Wenn Sie also ändern, wird das Drucken auf dem anderen Ziel noch praktischer, da Python auch eine Funktion bereitstellt (ab Python 3.4 ist es jedoch einfach, eine äquivalente Funktion für frühere Python-Versionen zu erstellen).sys.stdoutprintsys.stdout printredirect_stdout

Der Nachteil ist, dass es bei printAnweisungen, die nicht gedruckt werden, nicht funktioniert sys.stdoutund dass das Erstellen eigener Anweisungen stdoutnicht wirklich einfach ist.

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

Dies funktioniert jedoch auch:

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

Zusammenfassung

Einige dieser Punkte wurden bereits von @abarnet erwähnt, aber ich wollte diese Optionen genauer untersuchen. Insbesondere, wie man es modulübergreifend ändert (mit builtins/ __builtin__) und wie man diese Änderung nur vorübergehend vornimmt (mit Kontextmanagern).


4
Ja, das, was dieser Frage am nächsten kommt redirect_stdout, ist , dass es eine schöne Antwort ist, die dazu führt.
abarnert

6

Eine einfache Möglichkeit, alle Ausgaben von a zu erfassen print Funktion und anschließend zu verarbeiten, besteht darin, den Ausgabestream in eine andere Funktion zu ändern, z. B. eine Datei.

Ich werde eine PHPNamenskonvention verwenden ( 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()

Verwendung:

print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))

Würde drucken

Hallo John Bye John


5

Kombinieren wir dies mit Frame-Introspektion!

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

Sie werden feststellen, dass dieser Trick jeder Begrüßung die aufrufende Funktion oder Methode voranstellt. Dies kann sehr nützlich für die Protokollierung oder das Debuggen sein. vor allem, weil Sie damit Druckanweisungen im Code von Drittanbietern "entführen" können.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.