Question

Je suis en train d’écrire un décorateur qui doit appeler d’autres fonctions avant d’appeler la fonction qu’il décore. La fonction décorée peut avoir des arguments de position, mais les fonctions appelées par le décorateur ne peuvent accepter que des arguments de mots clés. Quelqu'un at-il un moyen pratique de convertir des arguments de position en arguments de mots clés?

Je sais que je peux obtenir une liste des noms de variables de la fonction décorée:

>>> def a(one, two=2):
...    pass

>>> a.func_code.co_varnames
('one', 'two')

Mais je ne sais pas comment dire ce qui a été passé et ce qui était en tant que mot clé.

Mon décorateur ressemble à ceci:

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        hozer(**kwargs)
        self.f(*args, **kwargs)

Existe-t-il un moyen autre que de simplement comparer kwargs et co_varnames, d’ajouter à kwargs quelque chose qui n’y figure pas et d’espérer le meilleur?

Était-ce utile?

La solution

Remarque - co_varnames inclura des variables locales ainsi que des mots-clés. Cela n’aura probablement pas d’importance, car zip tronque la séquence la plus courte, mais peut donner lieu à des messages d’erreur déroutants si vous transmettez un nombre incorrect de arguments.

Vous pouvez éviter cela avec func_code.co_varnames [: func_code.co_argcount] , mais il vaut mieux utiliser inspecter le module. c'est-à-dire:

import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

Vous pouvez également vouloir gérer le cas où la fonction définit ** kwargs ou * args (même si vous souhaitez simplement déclencher une exception lorsqu'il est utilisé avec le décorateur). Si ceux-ci sont définis, les deuxième et troisième résultats de getargspec renverront leur nom de variable, sinon ils seront Aucun.

Autres conseils

Tout argument qui a été adopté positionnellement sera passé à * args. Et tout argument passé en tant que mot clé sera transmis à ** kwargs. Si vous avez des noms et des valeurs d'argument de position, vous pouvez faire:

kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))

pour les convertir tous en arguments de mots clés.

Si vous utilisez Python, > = 2,7 inspect.getcallargs () le fait pour vous tout de suite. Vous devez simplement lui passer la fonction décorée comme premier argument, puis le reste des arguments exactement comme vous envisagez de l'appeler. Exemple:

>>> def f(p1, p2, k1=None, k2=None, **kwargs):
...     pass
>>> from inspect import getcallargs

Je prévois de faire f ('p1', 'p2', 'p3', k2 = 'k2', extra = 'kx1') (notez que k1 est transmis positionnellement comme p3), alors ...

>>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}

Si vous savez que la fonction décorée n'utilisera pas ** kwargs , cette clé n'apparaîtra pas dans le dict, et vous avez terminé (et je suppose qu'il n'y a pas de * args , car cela casserait l'exigence selon laquelle tout doit avoir un nom). Si vous avez le ** kwargs , comme je l’ai dans cet exemple, et souhaitez les inclure avec le reste des arguments nommés, il faut encore une ligne:

>>> call_args.update(call_args.pop('kwargs'))
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}

Mise à jour: pour Python > = 3.3, voir < code> inspect.Signature.bind () et le inspect.signature pour une fonctionnalité similaire à (mais plus robuste que) inspect.getcallargs () .

Eh bien, c'est peut-être exagéré. Je l'ai écrit pour le paquet dectools (sur PyPi), afin que vous puissiez obtenir les mises à jour. Il renvoie le dictionnaire en tenant compte des arguments de position, mot clé et par défaut. Il existe une suite de tests dans le package (test_dict_as_called.py):

 def _dict_as_called(function, args, kwargs):
""" return a dict of all the args and kwargs as the keywords they would
be received in a real function call.  It does not call function.
"""

names, args_name, kwargs_name, defaults = inspect.getargspec(function)

# assign basic args
params = {}
if args_name:
    basic_arg_count = len(names)
    params.update(zip(names[:], args))  # zip stops at shorter sequence
    params[args_name] = args[basic_arg_count:]
else:
    params.update(zip(names, args))    

# assign kwargs given
if kwargs_name:
    params[kwargs_name] = {}
    for kw, value in kwargs.iteritems():
        if kw in names:
            params[kw] = value
        else:
            params[kwargs_name][kw] = value
else:
    params.update(kwargs)

# assign defaults
if defaults:
    for pos, value in enumerate(defaults):
        if names[-len(defaults) + pos] not in params:
            params[names[-len(defaults) + pos]] = value

# check we did it correctly.  Each param and only params are set
assert set(params.iterkeys()) == (set(names)|set([args_name])|set([kwargs_name])
                                  )-set([None])

return params

Voici une méthode plus récente pour résoudre ce problème en utilisant inspect.signature (pour Python 3.3+). Je vais vous donner un exemple que vous pouvez exécuter / tester d’abord, puis vous montrer comment modifier le code original.

Voici une fonction de test qui résume les arguments / kwargs qui lui sont donnés; au moins un argument est requis ( a ) et il existe un argument composé uniquement de mots clés avec une valeur par défaut ( b ), uniquement pour tester différents aspects des signatures de fonction.

def silly_sum(a, *args, b=1, **kwargs):
    return a + b + sum(args) + sum(kwargs.values())

Faisons maintenant un wrapper pour silly_sum qui peut être appelé de la même manière que silly_sum (avec une exception à laquelle nous aboutirons) mais qui ne fait que passer kwargs au silly_sum encapsulé.

def wrapper(f):
    sig = inspect.signature(f)
    def wrapped(*args, **kwargs):
        bound_args = sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        print(bound_args) # just for testing

        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args")) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        return f(**all_kwargs)
    return wrapped

sig.bind renvoie un objet BoundArguments , mais cela ne prend pas les valeurs par défaut en compte, sauf si vous appelez explicitement apply_defaults . Cela générera également un tuple vide pour args et un dict vide pour kwargs si aucun * args / ** kwargs n'a été donné.

sum_wrapped = wrapper(silly_sum)
sum_wrapped(1, c=9, d=11)
# prints <BoundArguments (a=1, args=(), b=1, kwargs={'c': 9, 'd': 11})>
# returns 22

Ensuite, nous obtenons simplement le dictionnaire des arguments et ajoutons le ** kwargs dans. L'exception à l'utilisation de ce wrapper est que * args ne peut pas être passé à la une fonction. C'est parce qu'il n'y a pas de noms pour ceux-ci, nous ne pouvons donc pas les convertir en kwargs. Si les utiliser comme arguments kwarg nommés est acceptable, vous pouvez le faire à la place.

Voici comment cela peut être appliqué au code d'origine:

import inspect


class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f
        self._f_sig = inspect.signature(f)

    def __call__(self, *args, **kwargs):
        bound_args = self._f_sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args")) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        hozer(**all_kwargs)
        self.f(*args, **kwargs)
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top