3
votes

Passer des arguments optionnels supplémentaires aux rappels sans interrompre les rappels existants

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.


0 commentaires

3 Réponses :


-1
votes

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


0 commentaires

1
votes

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)


5 commentaires

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



4
votes

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.


5 commentaires

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.