J'ai une méthode API qui accepte un rappel. Le rappel attend un argument.
Je voudrais que cette méthode passe un deuxième argument aux callbacks qui l'acceptent. Cependant, je dois maintenir la compatibilité avec les rappels qui n'acceptent que l'argument d'origine. (En fait, je pense que la plupart des utilisateurs ne se soucieront pas de l'argument supplémentaire, il serait donc ennuyeux de les forcer à l'ignorer explicitement.)
Je sais que cela peut être fait en utilisant inspect
. Je me demande s'il existe une solution "idiomatique" ou couramment utilisée qui n'est pas si lourde.
3 Réponses :
Utilisation d'un wrapper de fonction:
from inspect import signature def ignore_simple(function): count = len(signature(function).parameters) return lambda *args: function(*args[:count])
Cela fonctionne, mais c'est un peu de force brute.
Une version plus simple, en supposant que function code > n'a pas de mots-clés ou de paramètres variadiques:
from inspect import signature, Parameter def ignore_extra_arguments(function): positional_count = 0 var_positional = False keyword_names = set() var_keyword = False for p in signature(function).parameters.values(): if p.kind == Parameter.POSITIONAL_ONLY: positional_count += 1 elif p.kind == Parameter.POSITIONAL_OR_KEYWORD: positional_count += 1 keyword_names.add(p.name) elif p.kind == Parameter.VAR_POSITIONAL: var_positional = True elif p.kind == Parameter.KEYWORD_ONLY: keyword_names.add(p.name) elif p.kind == Parameter.VAR_KEYWORD: var_keyword = True if var_positional: new_args = lambda args: args else: new_args = lambda args: args[:positional_count] if var_keyword: new_kwargs = lambda kwargs: kwargs else: new_kwargs = lambda kwargs: { name: value for name, value in kwargs.items() if name in keyword_names } def wrapped(*args, **kwargs): return function( *new_args(args), **new_kwargs(kwargs) ) return wrapped
Une solution plus simple serait d'utiliser un bloc try
pour essayer d'appeler le rappel avec un deuxième argument d'abord, avant de revenir à un appel avec un seul argument dans le sauf
bloquer:
try: callback(first, second) except TypeError as e: if e.__traceback__.tb_frame.f_code.co_name != 'func_name': raise callback(first)
Le problème avec ceci est que si callback (first, second)
déclenche une TypeError
pour toute autre raison, alors cette erreur sera ignorée et callback
sera appelé une seconde fois.
C'est vrai, même si une fonction de rappel bien écrite ne doit jamais déclencher une TypeError en premier lieu. Mais si cela est vraiment un problème, vous pouvez déterminer si TypeError est causé par un appel avec un nombre incorrect d'arguments en vérifiant le message d'exception comme démontré dans ma réponse mise à jour). Cela rend le code moins élégant mais cela fonctionnerait, et il n'y a aucune raison de croire que le compilateur Python changera un jour ses messages d'erreur pour les exceptions intégrées existantes.
J'ai en fait un problème ouvert où un test échoue dans 3.6+ parce qu'un message d'exception intégré a changé. De plus, il est possible que callback
produise un TypeError
indiscernable avec le même message. (Il est certes peu probable que ces problèmes surviennent dans la pratique.)
Il m'est venu à l'esprit qu'un terrain d'entente entre cette solution et la solution la plus complexe dans ma réponse serait d'utiliser inspect.Signature.bind
. Vous pouvez attraper le TypeError
avant d'essayer d'exécuter le rappel.
Vrai. J'ai mis à jour ma réponse pour utiliser l'objet de trace à la place. Remplacez func_name
par le nom de la fonction que ce code est utilisé.
Je pense que vous pouvez utiliser __code__ pour voir la quantité d'arguments nécessaires au rappel.
if callback.__code__.co_argcount == 2: callback(arg1, arg2) else: callback(arg1)
Ce code n'est pas testé mais il devrait fonctionner.
Excellente solution. J'ai oublié l'attribut __code__
d'un objet fonction.
Merci @blhsing. Votre solution try..except fonctionne également, mais avec une pénalité de performances due aux exceptions fréquemment appelées.
D'accord. La solution try-except
est beaucoup moins efficace lorsque le callback n'accepte qu'un seul paramètre car cela ajouterait une surcharge dans la création des objets d'exception et de traceback.
Je ne connaissais pas __code __. Co_argcount
. Il semble que vous pourriez le simplifier / généraliser encore plus: callback (* args [: callback .__ code __. Co_argcount])
.
@ thom-smith c'est vrai si vous passez l'argument par callback. Avec le "vrai" rappel, vous ne pouvez rien passer directement. Celui qui peut passer l'argument est l'appelant.