Remarque: cette question est à titre informatif uniquement. Je suis intéressé de voir à quel point il est possible d'approfondir les composants internes de Python.
Il n'y a pas très longtemps, une discussion a commencé à l'intérieur d'une certaine question concernant la question de savoir si les chaînes passées aux instructions print pouvaient être modifiées après / pendant l'appel à print
. Par exemple, considérons la fonction:
def print_something():
print('This cat was scared.')
Maintenant, quand print
est exécuté, alors la sortie vers le terminal devrait afficher:
This dog was scared.
Notez que le mot «chat» a été remplacé par le mot «chien». Quelque chose quelque part a pu modifier ces tampons internes pour changer ce qui était imprimé. Supposons que cela soit fait sans l'autorisation explicite de l'auteur du code d'origine (par conséquent, piratage / détournement).
Ce commentaire du sage @abarnert, en particulier, m'a fait réfléchir:
Il y a plusieurs façons de le faire, mais elles sont toutes très laides et ne devraient jamais être faites. Le moyen le moins laid est probablement de remplacer l'
code
objet à l'intérieur de la fonction par un objet avec uneco_consts
liste différente . La prochaine étape consiste probablement à accéder à l'API C pour accéder au tampon interne de str. [...]
Donc, il semble que cela soit réellement possible.
Voici ma façon naïve d'aborder ce problème:
>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.
Bien sûr, exec
c'est mauvais, mais cela ne répond pas vraiment à la question, car cela ne modifie en fait rien pendant le moment ou après l' print
appel.
Comment cela se passerait-il comme @abarnert l'a expliqué?
42
pour23
que pourquoi il est une mauvaise idée de changer la valeur"My name is Y"
à"My name is X"
.Réponses:
Premièrement, il existe en fait une manière beaucoup moins piratée. Tout ce que nous voulons faire, c'est changer les
print
impressions, non?Ou, de même, vous pouvez monkeypatch
sys.stdout
au lieu deprint
.De plus, rien de mal avec l'
exec … getsource …
idée. Eh bien, bien sûr, il y a beaucoup de mal à cela, mais moins que ce qui suit ici ...Mais si vous souhaitez modifier les constantes de code de l'objet fonction, nous pouvons le faire.
Si vous voulez vraiment jouer avec des objets de code pour de vrai, vous devriez utiliser une bibliothèque comme
bytecode
(quand c'est fini) oubyteplay
(jusque-là, 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 choses comme réparerlnotab
, seul un fou le ferait manuellement.De plus, il va sans dire que toutes les implémentations Python n'utilisent pas des objets de code de style CPython. Ce code fonctionnera dans CPython 3.7, et probablement toutes les versions remontant à au moins 2.2 avec quelques modifications mineures (et non les trucs de piratage de code, mais des choses comme les expressions de générateur), mais il ne fonctionnera avec aucune version d'IronPython.
Qu'est-ce qui pourrait mal tourner avec le piratage des objets de code? Surtout juste des segfaults, des
RuntimeError
s qui consomment toute la pile, desRuntimeError
s plus normales qui peuvent être gérées, ou des valeurs de garbage qui ne feront probablement que lever unTypeError
ouAttributeError
lorsque vous essayez de les utiliser. Pour des exemples, essayez de créer un objet de code avec juste unRETURN_VALUE
sans rien sur la pile (bytecodeb'S\0'
pour 3.6+,b'S'
avant), ou avec un tuple vide pourco_consts
quand il y a unLOAD_CONST 0
dans le bytecode, ou avecvarnames
décrémenté de 1 pour que le plus élevéLOAD_FAST
charge réellement une freevar / cellvar cellule. Pour vous amuser vraiment, si vous vouslnotab
trompez suffisamment, votre code ne fera que segfault lorsqu'il sera exécuté dans le débogueur.Utiliser
bytecode
oubyteplay
ne vous protégera pas de tous ces problèmes, mais ils ont quelques vérifications de base de la cohérence et de bons assistants qui vous permettent de faire des choses comme insérer un morceau de code et le laisser s'inquiéter de la mise à jour de tous les décalages et étiquettes afin que vous puissiez ' t se tromper, et ainsi de suite. (De plus, ils vous évitent d'avoir à taper dans ce ridicule constructeur de 6 lignes et de déboguer les fautes de frappe qui en découlent.)Passons maintenant au n ° 2.
J'ai mentionné que les objets de code sont immuables. Et bien sûr, les consts sont un tuple, donc nous ne pouvons pas changer cela directement. Et la chose dans le tuple const est une chaîne, que nous ne pouvons pas non plus changer directement. C'est pourquoi j'ai dû créer une nouvelle chaîne pour créer un nouveau tuple afin de créer un nouvel objet de code.
Mais que se passerait-il si vous pouviez changer une chaîne directement?
Eh bien, assez profondément sous les couvertures, tout n'est qu'un pointeur vers des données C, non? Si vous utilisez CPython, il existe une API C pour accéder aux objets , et vous pouvez l'utiliser
ctypes
pour accéder à cette API à partir de Python lui-même, ce qui est une idée tellement terrible qu'ils ont mis unpythonapi
droit là dans lectypes
module de stdlib . :) L'astuce la plus importante que vous devez savoir est queid(x)
c'est le pointeur versx
en mémoire (en tant queint
).Malheureusement, l'API C pour les chaînes ne nous permettra pas d'accéder en toute sécurité au stockage interne d'une chaîne déjà figée. Alors vissez en toute sécurité, lisons simplement les fichiers d'en-tête et trouvons ce stockage nous-mêmes.
Si vous utilisez CPython 3.4 - 3.7 (c'est différent pour les anciennes versions, et qui sait pour le futur), une chaîne littérale d'un module en ASCII pur sera stockée en utilisant le format ASCII compact, ce qui signifie que la structure se termine tôt et le tampon d'octets ASCII suit immédiatement en mémoire. Cela cassera (comme probablement dans segfault) si vous mettez un caractère non-ASCII dans la chaîne, ou certains types de chaînes non littérales, mais vous pouvez lire les 4 autres façons d'accéder au tampon pour différents types de chaînes.
Pour rendre les choses un peu plus faciles, j'utilise le
superhackyinternals
projet depuis mon GitHub. (Ce n'est intentionnellement pas installable par pip car vous ne devriez vraiment pas l'utiliser sauf pour expérimenter votre version locale de l'interpréteur et autres.)Si vous voulez jouer avec ce truc,
int
c'est beaucoup plus simple sous les couvertures questr
. Et il est beaucoup plus facile de deviner ce que vous pouvez briser en changeant la valeur de2
à1
, non? En fait, oubliez d'imaginer, faisons-le simplement (en utilisant àsuperhackyinternals
nouveau les types de ):… Prétendez que la zone de code a une barre de défilement infinie.
J'ai essayé la même chose dans IPython, et la première fois que j'ai essayé d'évaluer
2
à l'invite, cela s'est passé dans une sorte de boucle infinie ininterrompue. Il utilise probablement le nombre2
pour quelque chose dans sa boucle REPL, contrairement à l'interpréteur de stock?la source
PyUnicodeObject
, d'autre part, c'est probablement vraiment seulement Python dans le sens où un interpréteur Python l'exécutera…NameError: name 'arg' is not defined
. Vouliez-vous direargs = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]
:? Un sans doute meilleure façon d'écrire ce serait:args = [str(arg).replace('cat', 'dog') for arg in args]
. Une autre, encore plus courte, l' option:args = map(lambda a: str(a).replace('cat', 'dog'), args)
. Ceci a l'avantage supplémentaire quiargs
est paresseux (qui pourrait également être accompli en remplaçant la compréhension de liste ci-dessus par une compréhension génératrice -*args
fonctionne dans les deux sens).PyUnicodeObject
définition de structure, mais en copiant cela dans la réponse, je pense que cela me gênerait , et je pense que le readme et / ou les commentaires de la sourcesuperhackyinternals
expliquent réellement comment accéder au tampon (au moins assez bien pour me le rappeler la prochaine fois que je m'en soucie; je ne sais pas si ce sera suffisant pour quelqu'un d'autre…), ce dont je ne voulais pas parler ici. La partie pertinente est de savoir comment passer d'un objet Python en direct à sonPyObject *
viactypes
. (Et peut-être simuler l'arithmétique des pointeurs, éviter leschar_p
conversions automatiques , etc.)print
à un nom. Vous pouvez également lier le nomprint
pour eux:import yourmodule; yourmodule.print = badprint
.Patch de singe
print
print
est une fonction intégrée donc elle utilisera laprint
fonction définie dans lebuiltins
module (ou__builtin__
en Python 2). Ainsi, chaque fois que vous souhaitez modifier ou changer le comportement d'une fonction intégrée, vous pouvez simplement réaffecter le nom dans ce module.Ce processus est appelé
monkey-patching
.Après cela, chaque
print
appel passeracustom_print
, même si leprint
est dans un module externe.Cependant, vous ne souhaitez pas vraiment imprimer de texte supplémentaire, vous souhaitez modifier le texte imprimé. Une façon de procéder consiste à le remplacer dans la chaîne qui serait imprimée:
Et en effet si vous exécutez:
Ou si vous écrivez cela dans un fichier:
test_file.py
et importez-le:
Cela fonctionne donc vraiment comme prévu.
Cependant, au cas où vous ne voudriez que temporairement imprimer un patch, vous pouvez envelopper ceci dans un gestionnaire de contexte:
Donc, lorsque vous exécutez cela, cela dépend du contexte ce qui est imprimé:
C'est ainsi que vous pouvez "pirater"
print
par le patching de singe.Modifiez la cible au lieu du
print
Si vous regardez la signature de,
print
vous remarquerez unfile
argument qui estsys.stdout
par défaut. Notez qu'il s'agit d'un argument par défaut dynamique (il recherche vraimentsys.stdout
chaque fois que vous appelezprint
) et non comme des arguments par défaut normaux en Python. Donc, si vous changezsys.stdout
print
, l'impression sur la cible différente sera d'autant plus pratique que Python fournit également uneredirect_stdout
fonction (à partir de Python 3.4, mais il est facile de créer une fonction équivalente pour les versions antérieures de Python).L'inconvénient est que cela ne fonctionnera pas pour les
print
instructions qui ne s'impriment passys.stdout
et que créer les vôtresstdout
n'est pas vraiment simple.Cependant, cela fonctionne également:
Résumé
Certains de ces points ont déjà été mentionnés par @abarnet mais je voulais explorer ces options plus en détail. Surtout comment le modifier à travers les modules (en utilisant
builtins
/__builtin__
) et comment rendre ce changement uniquement temporaire (en utilisant des contextmanagers).la source
redirect_stdout
, donc c'est bien d'avoir une réponse claire qui mène à cela.Un moyen simple de capturer toute la sortie d'une
print
fonction puis de la traiter est de changer le flux de sortie en quelque chose d'autre, par exemple un fichier.Je vais utiliser une
PHP
convention de nommage ( ob_start , ob_get_contents , ...)Usage:
Serait imprimer
la source
Combinons cela avec l'introspection du cadre!
Vous trouverez cette astuce précédant chaque message d'accueil avec la fonction ou la méthode d'appel. Cela peut être très utile pour la journalisation ou le débogage; d'autant plus qu'il vous permet de "détourner" des instructions d'impression dans du code tiers.
la source