web-dev-qa-db-fra.com

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

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 liste co_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?

148
cs95

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, RuntimeErrors qui dévorent toute la pile, de plus normaux RuntimeErrors 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?

237
abarnert

Monkey-patch 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:

test_fichier.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.
_

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.

Modifier la cible au lieu de 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.
_

Sommaire

@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).

34
MSeifert

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

6
Uri Goren

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.

4
Rafaël Dera