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é
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é
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`.
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__
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
.
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é.
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