Erstens gibt es tatsächlich einen viel weniger hackigen Weg. Wir wollen nur ändern, welche print
Drucke 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.stdout
anstelle 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 CodeType
Initialisierer ein Schmerz; Wenn Sie tatsächlich Dinge wie das Reparieren lnotab
erledigen 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, RuntimeError
s, die den gesamten Stapel verschlingen, normalere RuntimeError
s, die gehandhabt werden können, oder Müllwerte, die wahrscheinlich nur ein TypeError
oder auslösen, AttributeError
wenn Sie versuchen, sie zu verwenden. Versuchen Sie beispielsweise, ein Codeobjekt mit nur einem RETURN_VALUE
mit nichts auf dem Stapel (Bytecode b'S\0'
für 3.6+, b'S'
vorher) oder mit einem leeren Tupel zu erstellen, co_consts
wenn sich ein LOAD_CONST 0
Bytecode im Bytecode befindet, oder mit einem varnames
Dekrement von 1, damit der höchste LOAD_FAST
tatsächlich eine Freevar lädt / cellvar cell. Für echten Spaß, wenn Sie das lnotab
Falsche genug bekommen, wird Ihr Code nur dann fehlerhaft, wenn er im Debugger ausgeführt wird.
Verwenden bytecode
oder byteplay
schü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 ctypes
Python selbst auf diese API zugreifen. pythonapi
Dies ist eine so schreckliche Idee, dass sie genau dort im ctypes
Modul der stdlib abgelegt werden . :) Der wichtigste Trick, den Sie wissen müssen, id(x)
ist der tatsächliche Zeiger auf x
im 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 superhackyinternals
Projekt 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, int
ist es unter der Decke viel einfacher als str
. Und es ist viel einfacher zu erraten, was Sie durch Ändern des Werts von 2
auf brechen können 1
, oder? Vergiss die Vorstellung, lass es uns einfach tun (mit den Typen von superhackyinternals
wieder):
>>> 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, 2
an der Eingabeaufforderung auszuwerten , ging es in eine Art unterbrechungsfreie Endlosschleife. Vermutlich verwendet es die Nummer 2
für etwas in seiner REPL-Schleife, während der Aktieninterpreter dies nicht tut?
42
in zu ändern ,23
als warum es eine schlechte Idee ist, den Wert von"My name is Y"
in zu ändern"My name is X"
.