[Python3] Réécrire l'objet code de la fonction

introduction

En Python, il est relativement facile de remplacer une méthode ou une fonction dans un module. Qiita a également quelques articles.

Ces techniques modifient simplement l'accès aux attributs comme suit: Je ne joue pas avec une fonction comme objet.

class SomeClass:
    def original_method(self):
        print('call original_method')

def new_method(self):
    print('call new_method')

some_instance1 = SomeClass()
some_instance2 = SomeClass()

# some_Réécrire la méthode de instance1
some_instance1.original_method = type(some_instance1.original_method)(new_method, some_instance1)  
some_instance1.original_method()  # new_method()Est appelé
some_instance2.original_method()  # original_method()Est appelé

#Réécrire les méthodes pour toutes les instances
SomeClass.original_method = new_method
some_instance1.original_method()  # new_method()Est appelé
some_instance2.original_method()  # new_method()Est appelé
import some_module

def new_func():
    print('call new_func')

##Réécrire la fonction dans le module
some_module.original_func = new_func
some_module.original_func()  # new_func()Est appelé

La plupart du temps, ces techniques sont plutôt bonnes. Cependant, il existe certains cas où vous ne pouvez pas écraser même si vous jouez avec l'accès aux attributs.

#S'il a été récupéré avant d'écraser l'attribut,
original_method = some_instance1.original_method

#Même si vous écrasez l'attribut
type(some_instance1).original_method = new_method

#Il n'y a aucun effet sur celui qui a été retiré en premier
original_method()  #Original original_method()Est appelé
import some_module
from some_module import original_func

#Idem pour les fonctions dans les modules
some_module.original_func = new_func
original_func()  # original_func()Est appelé

Ce que vous voulez faire (comment utiliser)

Je veux écraser tout le programme, même si les attributs ont été récupérés en premier.

import some_module
from some_module import original_func  #Même s'il a été retiré en premier

def new_func():
    print('call new_func')

overwrite_func(some_module.original_func, new_func)  #Écraser plus tard
original_func()  #Ici nouveau_func()Je veux que tu sois appelé

Fabriqué (prototype)

def overwrite_func(orig, new):
    from uuid import uuid4
    kw = 'kw' + str(uuid4()).replace('-', '')
    exec("def outer():\n " + '='.join(list(orig.__code__.co_freevars) + ['None']) 
         + "\n def inner(*args, " + kw + "=new, **kwargs):\n  " 
         + ','.join(orig.__code__.co_freevars)
         + "\n  return " + kw + "(*args, **kwargs)\n return inner",
         locals(), globals())
    inner = outer()
    orig.__code__ = inner.__code__
    orig.__defaults__ = inner.__defaults__
    orig.__kwdefaults__ = inner.__kwdefaults__

Au début, je pensais que je remplacerais simplement __code__, mais le nombre de __code __. Co_freevars (le nombre de variables de la fonction externe que la fonction définie à l'intérieur de la fonction utilise en interne?) En est-il un. Il semblait que je ne pouvais pas le remplacer si je ne le faisais pas, alors j'ai ajusté le nombre de freevars avec ʻexec`.

Fabriqué (version terminée)

S'il s'agit d'une version prototype, la signature sera perdue, je l'ai donc laissée autant que possible. Cependant, __overwrite_func est ajouté à l'argument mot-clé pour ajuster __code __. Co_freevars.

def overwrite_func(orig, new, signature=None):
    import inspect
    from types import FunctionType
    from textwrap import dedent
    assert isinstance(orig, FunctionType), (orig, type(orig))
    assert isinstance(new, FunctionType), (new, type(new))
    if signature is None:
        signature = inspect.signature(orig)
    params = [
        (str(p).split(':')[0].split('=')[0], p)
        for k, p in signature.parameters.items()
        if k != '__overwrite_func'
    ]
    default = {p.name: p.default for _, p in params}
    anno = {p.name: p.annotation for _, p in params}
    args_kwargs = [
        k if k[0] == '*' or p.kind == p.POSITIONAL_ONLY else k + '=' + k 
        for k, p in params
    ]
    signature_ = [
        (k + (':anno["' + k + '"]' if p.annotation != p.empty else '')
         + ('=default["' + k + '"]' if p.default != p.empty else ''),
         not (p.kind == p.VAR_KEYWORD or p.kind == p.KEYWORD_ONLY))
        for k, p in params
    ]
    signature__ = [s for s, positional in signature_ if positional]
    signature__.append('__overwrite_func=new')
    signature__.extend(s for s, positional in signature_ if not positional)
    signature__ = '(' + ', '.join(signature__) + ')'
    if signature.return_annotation is not inspect.Signature.empty:
        anno['return'] = signature.return_annotation
        signature__ += ' -> anno["return"]'
    source = dedent("""
    def outer():
        """ + '='.join(list(orig.__code__.co_freevars) + ['None']) + """
        def inner""" + signature__ + """:
            """ + ', '.join(orig.__code__.co_freevars) + """
            return __overwrite_func(""" + ', '.join(args_kwargs) + """)
        return inner
    """)
    globals_ = {}
    exec(source, dict(new=new, default=default, anno=anno), globals_)
    inner = globals_['outer']()
    globals_.clear()
    orig.__code__ = inner.__code__
    orig.__defaults__ = inner.__defaults__
    orig.__kwdefaults__ = inner.__kwdefaults__
    orig.__annotations__ = inner.__annotations__

Mise en garde

La fonction créée cette fois n'est pas universelle. Il est impuissant pour les fonctions spéciales qui n'ont pas de __code__ ou d'objets appelables qui implémentent __call__. Veuillez l'utiliser selon l'autre partie.

overwrite_func(print, new_func)  #assert est désactivé
# → AttributeError: 'builtin_function_or_method' object has no attribute '__code__'

Faites attention aux fuites de mémoire, car les références aux fonctions d'écrasement s'accumuleront dans __overwrite_func.

Exemple d'application

def copy_func(f):
    """https://stackoverflow.com/questions/13503079"""
    import functools
    import types
    assert isinstance(f, types.FunctionType), (f, type(f))
    g = types.FunctionType(
        f.__code__,
        f.__globals__, 
        name=f.__name__,
        argdefs=f.__defaults__,
        closure=f.__closure__,
    )
    g.__kwdefaults__ = f.__kwdefaults__
    functools.update_wrapper(g, f)
    return g

def add_hook(func, pre_call=None, post_call=None, except_=None, finally_=None):
    import inspect
    func_sig = inspect.signature(func)
    func_copy = copy_func(func)

    def hook(*args, **kwargs):
        bound_args = func_sig.bind(*args, **kwargs)
        if pre_call is not None:
            pre_call(func_copy, bound_args)
        try:
            return_ = func_copy(*args, **kwargs)
        except BaseException as e:
            if except_ is not None:
                except_(func_copy, bound_args, e)
            raise
        else:
            if post_call is not None:
                post_call(func_copy, bound_args, return_)
        finally:
            if finally_ is not None:
                finally_(func_copy, bound_args)
        return return_

    overwrite_func(func, hook)

Vous pouvez ajouter une fonction de rappel ultérieurement.

def callback(f, args, result):
    print(result)

add_hook(original_func, post_call=callback)
original_func()  # original_func()Appelé avant le rappel()Est appelé.

en conclusion

J'ai pu le faire, mais si je n'ai pas à le faire, je ne devrais pas le faire. Les cas de test ne sont probablement pas suffisants car je ne comprends pas complètement les spécifications autour de __code__. S'il vous plaît laissez-moi savoir s'il y a un cas qui ne fonctionne pas.

Recommended Posts

[Python3] Réécrire l'objet code de la fonction
[python] Valeur de l'objet fonction (?)
[Python] Récupère le code de caractère du fichier
Réécrire le code Python2 en Python3 (2to3)
le zen de Python
[Python] Lire le code source de Bottle Part 2
Récupérer l'appelant d'une fonction en Python
[Python] Lire le code source de Bottle Part 1
Code pour vérifier le fonctionnement de Python Matplot lib
Convertir le code de caractère du fichier avec Python3
Vers la retraite de Python2
[Python] Calculez la valeur moyenne de la valeur de pixel RVB de l'objet
[Python] L'origine du nom de la fonction python
Décomposons les bases du code Python de TensorFlow
Expliquez le code de Tensorflow_in_ROS
À propos de la fonction enumerate (python)
Récupérer le code retour d'un script Python depuis bat
#Une fonction qui renvoie le code de caractère d'une chaîne de caractères
2.x, 3.x code de caractères des séries python
À propos des fonctionnalités de Python
Le pouvoir des pandas: Python
[Python] Modifier le contrôle du cache des objets téléchargés sur Cloud Storage
Essayez d'obtenir la liste des fonctions du paquet Python> os
Avoir le graphique d'équation de la fonction linéaire dessiné en Python
Réécrivez le nœud d'ajout d'enregistrement de SPSS Modeler avec Python.
Le processus de création et d'amélioration du code Python orienté objet
2015-11-26 python> Afficher la liste des fonctions du module> import math> dir (math)
L'histoire de Python et l'histoire de NaN
[Python] La pierre d'achoppement de l'importation
First Python 3 ~ Le début de la répétition ~
Existence du point de vue de Python
pyenv-changer la version python de virtualenv
Obtenir les attributs d'un objet
[Python] Faire de la fonction une fonction lambda
[Python] Comprendre le potentiel_field_planning de Python Robotics
Revue des bases de Python (FizzBuzz)
Touchez l'objet du réseau neuronal
[Python] Lire le code source de Flask
À propos de la liste de base des bases de Python
[OpenCV; Python] Résumé de la fonction findcontours
Apprenez les bases de Python ① Débutants élémentaires
fonction python ①
Comment connaître la structure interne d'un objet en Python
Mesurez la couverture de test du code python poussé sur GitHub.
Essayez de transcrire la fonction de masse stochastique de la distribution binomiale en Python
[Python] fonction
Premier python ② Essayez d'écrire du code tout en examinant les fonctionnalités de python
J'ai écrit le code pour écrire le code Brainf * ck en python
Une fonction qui mesure le temps de traitement d'une méthode en python
[Python3] Définition d'un décorateur qui mesure le temps d'exécution d'une fonction
Résumons le degré de couplage entre les modules avec du code Python
Réécrire le nœud d'échantillonnage de SPSS Modeler avec Python (2): échantillonnage en couches, échantillonnage en grappes
[Python] Une fonction simple pour trouver les coordonnées du centre d'un cercle
fonction python ②
Installation du code Visual Studio et installation de python