150 votes

Est-il possible de "pirater" la fonction d'impression de Python?

Remarque: Cette question est à titre informatif seulement. Je suis intéressé de voir comment profondément dans Python-même, il est possible d'aller avec cela.

Pas très longtemps, une discussion a commencé à l'intérieur d'une certaine question quant à savoir si la chaîne passée en pour imprimer les relevés pourraient être modifiées après le/lors de l'appel à print a été faite. Par exemple, considérons la fonction:

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

Maintenant, quand print est exécuté, puis la sortie vers le terminal doit s'afficher:

This dog was scared.

Remarquez le mot "chat" a été remplacé par le mot "chien". Quelque chose, quelque part, en quelque sorte, a été en mesure de modifier ces tampons internes de changer ce qui a été imprimé. Suppose que c'est fait sans le code d'origine de l'auteur la permission explicite (et donc, de piratage ou de détournement).

Ce commentaire de la sage @abarnert, en particulier, m'a fait penser:

Il ya un couple de façons de le faire, mais ils sont tous très laids, et ne doit jamais être fait. Le moins laid façon est probablement remplacer le code objet à l'intérieur de la fonction avec un avec un autre co_consts liste. La prochaine est probablement atteint dans l'API C pour accéder à la dod tampon interne. [...]

Ainsi, il ressemble à ce qui est réellement possible.

Voici mon naïf façon 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 ce n'est pas vraiment répondre à la question, car il ne fait pas modifier quoi que ce soit lors de la/après print est appelé.

Comment serait-il fait comme @abarnert a expliqué?

247voto

abarnert Points 94246

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, RuntimeErrors qui mangent jusqu'à l'ensemble de la pile, de plus normal, RuntimeErrors 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?

38voto

MSeifert Points 6307

Monkey patch- print

print est un builtin fonction de sorte qu'il va utiliser l' print fonction définie dans l' builtins module (ou __builtin__ en Python 2). Donc, chaque fois que vous voulez modifier ou de changer le comportement d'une fonction builtin vous pouvez assignez simplement le nom de ce module.

Ce processus est appelé 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 que chaque print appel sera fait par custom_print, même si l' print est un module externe.

Cependant, vous ne voulez pas vraiment l'impression de texte supplémentaire, vous voulez modifier le texte qui est imprimé. Une des façons de faire qui consiste à remplacer dans la chaîne de caractères qui sera 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 en effet, si vous exécutez:

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

Ou si vous écrivez que pour un fichier:

test_file.py

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.

De sorte qu'il fonctionne vraiment comme prévu.

Toutefois, dans le cas où vous ne souhaitez temporairement singe-patch d'impression vous pouvez rassembler tout cela dans un contexte-manager:

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

Ainsi, lorsque vous exécutez que cela dépend du contexte de 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 comment vous pouvez "hack" print par singe de correction.

Modifier la cible au lieu de l' print

Si vous regardez la signature de l' print vous remarquerez file argument qui est - sys.stdout par défaut. Notez que c'est une dynamique d'argument par défaut (il vraiment regarde sys.stdout chaque fois que vous appelez print) et non pas comme normal par défaut arguments en Python. Donc, si vous changez sys.stdout print vont en fait imprimer à la cible différente même plus pratique que Python fournit également un redirect_stdout de la fonction (à partir de Python 3.4 sur, mais il est facile de créer une fonction équivalente pour les précédentes versions de Python).

L'inconvénient est qu'il ne fonctionne pas pour print des déclarations qui n'ont pas l'impression d' sys.stdout et que la création de 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'))

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

Résumé

Certains de ces points ont déjà mentionné par @abarnet mais j'ai voulu explorer ces options plus en détail. Surtout comment le modifier à travers des modules (à l'aide d' builtins/__builtin__) et comment faire pour que le changement temporaire (à l'aide de contextmanagers).

5voto

Uri Goren Points 445

Un moyen simple de capturer toutes les données de sortie à partir d'un print de la fonction et de les traiter ensuite, est de changer le flux de sortie à quelque chose d'autre, par exemple un fichier.

Je vais utiliser un PHP des conventions de nommage (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()

Utilisation:

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

Imprimez

Salut Jean Bye John

5voto

Rafaël Dera Points 282

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X