Tout d'abord, il y a en fait beaucoup moins hacky façon. Tout ce que nous voulons faire est de changer ce que l' print
d'estampes, de droit?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Ou, de la même façon, vous pouvez monkeypatch sys.stdout
au lieu de print
.
Aussi, rien de mal avec l' exec … getsource …
idée. Bien sûr, il y a beaucoup de mal à cela, mais moins que ce qui suit ici...
Mais si vous ne voulez modifier la fonction de l'objet du code des constantes, on peut le faire.
Si vous voulez vraiment jouer avec le code d'objets pour de vrai, vous devez utiliser une bibliothèque comme bytecode
(quand il est terminé) ou byteplay
(jusqu'alors, ou pour les anciennes versions de Python) au lieu de le faire manuellement. Même pour quelque chose d'aussi trivial, l' CodeType
initialiseur est une douleur; si vous avez réellement besoin de faire des trucs comme la fixation d' lnotab
, seul un fou serait de le faire manuellement.
Aussi, il va sans dire que toutes les implémentations de Python utilisation Disponible-code de style objets. Ce code fonctionnera dans Disponible 3.7, et probablement toutes les versions de retour pour au moins 2.2 avec quelques modifications mineures (et non pas le code du piratage des choses, mais des choses comme générateur d'expressions), mais il ne fonctionne pas avec toutes les versions 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()
Ce qui pourrait aller mal avec le piratage de code de des objets? La plupart du temps simplement de segmentation, RuntimeError
s qui mangent jusqu'à l'ensemble de la pile, de plus normal, RuntimeError
s qui peuvent être traitées, ou de déchets de valeurs qui sera probablement juste de soulever un TypeError
ou AttributeError
lorsque vous essayez de les utiliser. Pour des exemples, essayez de créer un code objet avec juste un RETURN_VALUE
avec rien sur la pile (bytecode b'S\0'
3,6+, b'S'
avant), ou avec un vide tuple d' co_consts
quand il y a un LOAD_CONST 0
dans le bytecode, ou avec varnames
décrémenté de 1, le plus haut LOAD_FAST
charge réellement un freevar/cellvar cellule. Pour certains réel plaisir, si vous obtenez l' lnotab
mauvais assez, votre code ne erreur de segmentation lors de l'exécution dans le débogueur.
À l'aide de bytecode
ou byteplay
ne sera pas vous protéger de tous ces problèmes, mais ils n'ont pas certains de base des contrôles d'intégrité, et de nice aides qui vous permettent de faire des choses comme insérer un bout de code et de le laisser vous soucier de la mise à jour de tous les décalages et les étiquettes de sorte que vous ne pouvez pas se tromper, et ainsi de suite. (Et en Plus, ils vous éviter d'avoir à taper dans cette ridicule de 6 constructeur de lignes, et d'avoir à déboguer le ridicule de fautes qui viennent de le faire.)
Maintenant, sur le n ° 2 de.
Je l'ai mentionné que le code des objets immuables. Et bien sûr, le consts sont un n-uplet, on ne peut pas modifier directement. Et la chose dans la const n-uplet est une chaîne de caractères, que nous ne pouvons pas changer directement. C'est pourquoi j'ai dû construire une nouvelle chaîne à construire un nouveau tuple de construire un nouveau code objet.
Mais si vous pouviez changer une chaîne directement?
Bien, assez profond sous les couvertures, tout est simplement un pointeur vers certains de données C, à droite? Si vous êtes à l'aide de Disponible, il y a une API C pour accéder aux objets, et vous pouvez utiliser ctypes
pour accéder à l'API de l'intérieur Python lui-même, ce qui est une très mauvaise idée de mettre un pythonapi
dans la stdlib de l' ctypes
module. :) Le plus important astuce que vous devez savoir, c'est qu' id(x)
est le pointeur à l' x
en mémoire (comme un int
).
Malheureusement, l'API C pour les chaînes ne nous laisse pas nous amener à la mémoire de stockage interne de déjà congelés chaîne. Donc vis en toute sécurité, nous allons juste de lire les fichiers d'en-tête et de trouver que le stockage de nous-mêmes.
Si vous utilisez Disponible 3.4 - 3.7 (c'est différent pour les versions plus anciennes, et qui sait pour l'avenir), un littéral de chaîne à partir d'un module, qui est fait de pur ASCII va être stocké à l'aide de la compacte au format ASCII, ce qui signifie que la structure se termine tôt et le tampon d'octets ASCII suit immédiatement dans la mémoire. Ce sera la rupture (comme dans probablement erreur de segmentation) si vous mettez un caractère non-ASCII dans la chaîne, ou certains types de non-chaînes littérales, mais vous pouvez lire sur les 4 autres façons d'accéder à la mémoire tampon pour les différents types de chaînes.
Pour rendre les choses un peu plus facile, je suis à l'aide de l' superhackyinternals
projet sur mon GitHub. (C'est intentionnellement pas pip-installable parce que vous ne devriez vraiment pas être à l'aide de cette exception à expérimenter avec votre bureau local de la construction de l'interprète et la comme.)
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 vous voulez jouer avec ce genre de choses, int
est beaucoup plus simple sous les couvertures qu' str
. Et il est beaucoup plus facile de deviner ce que vous pouvez briser en cas de modification de la valeur de 2
de 1
, droite? En fait, oubliez l'imaginer, nous allons le faire (en utilisant les types de superhackyinternals
encore une fois):
>>> 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
... de prétendre que la case code a une longueur infinie barre de défilement.
J'ai essayé la même chose dans IPython, et la première fois que j'ai essayé d'évaluer 2
à l'invite de commandes, il est allé dans une sorte de sans coupure de boucle infinie. On peut supposer que c'est à l'aide de le nombre 2
pour quelque chose dans son REPL la boucle, tandis que le stock interprète n'est-ce pas?