Remarque: cette question est à titre informatif seulement. Je suis intéressé de voir à quel point il est possible d'aller dans ce sens avec les composants internes de Python.
Il n'y a pas si longtemps, une discussion a commencé à l'intérieur d'une question concernant le fait de savoir si les chaînes passées aux instructions d'impression 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 lancé, 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 part, quelque part, a été capable de modifier ces tampons internes pour changer ce qui était imprimé. Supposons que cela se fasse sans la permission explicite de l'auteur du code original (d'où le piratage/piratage).
Ce commentaire du sage @abarnert, en particulier, m'a fait réfléchir:
Il y a plusieurs façons de le faire, mais ils sont tous très laids et ne devraient jamais être faits. La façon la moins laide est de remplacer probablement l'objet
code
de la fonction par un autre avec une listeco_consts
différente. Next va probablement dans l'API C pour accéder au tampon interne de la 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
est mauvais, mais cela ne répond pas vraiment à la question, car cela ne modifie en réalité rien pendant quand/après print
est appelé.
Comment cela se passerait-il comme l'explique @abarnert?
Premièrement, il existe en réalité une méthode beaucoup moins astucieuse. Tout ce que nous voulons, c'est changer ce que print
imprime, n'est-ce pas?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
De même, vous pouvez effectuer un monkeypatch sys.stdout
au lieu de print
.
En outre, rien de mal à l'idée exec … getsource …
. Eh bien, bien sûr, il y a beaucoup de faux, 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 les objets de code pour de vrai, vous devriez utiliser une bibliothèque comme bytecode
(quand c'est fini) ou byteplay
(jusque-là, ou pour les anciens Python versions) au lieu de le faire manuellement. Même pour quelque chose d'aussi trivial, l'initialiseur CodeType
est pénible; si vous devez réellement faire des choses comme réparer lnotab
, seul un fou le fera manuellement.
En outre, il va sans dire que toutes les implémentations Python n'utilisent pas d'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 pas le truc de piratage de code, mais des choses comme les expressions de générateur), mais cela ne fonctionnera avec aucune version d'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()
Qu'est-ce qui pourrait mal tourner avec le piratage d'objets de code? La plupart du temps, il s'agit uniquement de sefaults, RuntimeError
s qui dévorent toute la pile, de plus normaux RuntimeError
s qui peuvent être gérés, ou de valeurs erronées qui ne feront probablement que déclencher un TypeError
ou AttributeError
lorsque vous essayez de les utiliser. Par exemple, essayez de créer un objet code avec juste un RETURN_VALUE
sans rien sur la pile (bytecode b'S\0'
pour 3.6+, b'S'
avant), ou avec un tuple vide pour co_consts
quand il y a LOAD_CONST 0
dans le bytecode, ou avec varnames
décrémenté de 1, le LOAD_FAST
le plus élevé charge en fait une cellule freevar/cellvar. Pour vous amuser vraiment, si vous vous trompez suffisamment avec lnotab
, votre code sera seulement segfault lorsqu’il sera exécuté dans le débogueur.
Utiliser bytecode
ou byteplay
ne vous protégera pas de tous ces problèmes, mais ils disposent de quelques contrôles de base, et de jolis assistants qui vous permettent de faire des choses comme insérer un morceau de code et de vous laisser inquiéter de la mise à jour de tous les décalages et étiquettes. ne peut pas se tromper, et ainsi de suite. (De plus, ils vous évitent de taper ce constructeur ridicule de 6 lignes et de déboguer les fautes de frappe stupides qui en résultent.)
Passons maintenant à # 2.
J'ai mentionné que les objets de code sont immuables. Et bien sûr, les constants sont un tuple, nous ne pouvons donc pas changer cela directement. Et la chose dans le const Tuple est une chaîne, que nous ne pouvons pas non plus modifier 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 si vous pouviez changer une chaîne directement?
Eh bien, assez profondément sous les couvertures, tout n’est qu’un pointeur sur des données C, non? Si vous utilisez CPython, il y a ne API C pour accéder aux objets , et vous pouvez utiliser ctypes
pour accéder à cette API à partir de Python lui-même, ce qui est le cas. une idée terrible qu'ils ont mis un pythonapi
dans le module ctypes
de stdlib . :) L'astuce la plus importante que vous devez savoir est que id(x)
est le pointeur actuel sur x
en mémoire (en tant que int
).
Malheureusement, l'API C pour les chaînes ne nous permet pas d'accéder en toute sécurité au stockage interne d'une chaîne déjà gelée. Alors vissez bien, allons juste lisez 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), un littéral de chaîne d'un module composé de pure ASCII va être stocké à l'aide du compact ASCII format, ce qui signifie que la structure se termine tôt et que le tampon de ASCII octets suit immédiatement en mémoire. Cela cassera (comme dans probablement 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 sur les 4 autres manières d'accéder au tampon pour différents types de chaînes.
Pour rendre les choses un peu plus faciles, j'utilise le projet superhackyinternals
de mon GitHub. (Il est intentionnellement non-installable car vous ne devriez vraiment pas l'utiliser, sauf pour expérimenter votre version locale de l'interpréteur, etc.).
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 que str
. Et il est beaucoup plus facile de deviner ce que vous pouvez briser en modifiant la valeur de 2
en 1
, n'est-ce pas? En fait, oubliez d’imaginer, faisons-le simplement (en utilisant à nouveau les types de superhackyinternals
):
>>> 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
… Prétend que cette boîte de code a une barre de défilement de longueur infinie.
J'ai essayé la même chose avec 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 sans interruption. Vraisemblablement, il utilise le nombre 2
pour quelque chose dans sa boucle REPL, alors que l'interpréteur de stock ne le fait pas?
print
print
est une fonction intégrée, elle utilisera donc la fonction print
définie dans le module builtins
(ou ___builtin__
_ dans Python 2). Ainsi, chaque fois que vous souhaitez modifier ou modifier le comportement d’une fonction intégrée, vous pouvez simplement réaffecter le nom dans ce module.
Ce processus s'appelle _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
_
Après cela, chaque appel print
passe par _custom_print
_, même si print
se trouve dans un module externe.
Cependant, vous ne voulez pas vraiment imprimer de texte supplémentaire, vous voulez changer le texte imprimé. Une façon de s'y prendre est de le remplacer dans la chaîne qui serait imprimée:
__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
_
Et effectivement si vous courez:
_>>> def print_something():
... print('This cat was scared.')
>>> print_something()
This dog was scared.
_
Ou si vous écrivez cela dans un fichier:
_def print_something():
print('This cat was scared.')
print_something()
_
et l'importer:
_>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.
_
Donc, cela fonctionne vraiment comme prévu.
Cependant, si vous ne souhaitez que temporairement imprimer monkey-patch, vous pouvez le placer dans un gestionnaire de contexte:
_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
_
Donc, lorsque vous exécutez cela dépend du contexte ce qui est imprimé:
_>>> with ChangePrint() as x:
... test_file.print_something()
...
This dog was scared.
>>> test_file.print_something()
This cat was scared.
_
Donc, c'est comme ça que vous pourriez "pirater" print
en appliquant la technique du singe.
print
Si vous regardez la signature de print
, vous remarquerez un argument file
qui est _sys.stdout
_ par défaut. Notez qu'il s'agit d'un argument dynamique par défaut (il vraiment recherche _sys.stdout
_ à chaque fois que vous appelez print
) et non comme des arguments par défaut normaux dans Python. . Donc, si vous changez _sys.stdout
_ print
sera réellement imprimé sur la cible différente encore plus pratique que Python fournit également une fonction redirect_stdout
(de Python 3.4 sur, mais il est facile de créer une fonction équivalente pour les versions antérieures Python).
L'inconvénient est que cela ne fonctionnera pas pour les instructions print
qui n'impriment pas sur _sys.stdout
_ et que créer votre propre stdout
n'est pas vraiment simple.
_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'))
_
Cependant cela fonctionne aussi:
_>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
... test_file.print_something()
...
This dog was scared.
>>> test_file.print_something()
This cat was scared.
_
@Abarnet a déjà mentionné certains de ces points, mais je souhaitais explorer ces options plus en détail. En particulier, comment le modifier dans les modules (en utilisant builtins
/___builtin__
_) et comment rendre cette modification uniquement temporaire (en utilisant des gestionnaires de contexte).
Un moyen simple de capturer toutes les sorties d'une fonction print
puis de les traiter consiste à changer le flux de sortie en un autre, par ex. un fichier.
Je vais utiliser un PHP
conventions de dénomination ( 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()
Usage:
print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))
Serait imprimer
Bonjour John Bye John
Combinons cela avec l'introspection des cadres!
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}!")
Vous constaterez que cette astuce préfigure 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 que cela vous permet de "détourner" des instructions d'impression en code tiers.