Obtenir les arguments de mots clés réellement passés à une méthode Python

Je rêve d'une méthode Python avec des args de mots clés explicites:

def func(a=None, b=None, c=None):
    for arg, val in magic_arg_dict.items():   # Where do I get the magic?
        print '%s: %s' % (arg, val)

Je veux obtenir un dictionnaire des seuls arguments que l'appelant a réellement passés dans la méthode, tout comme **kwargs, mais je ne veux pas que l'appelant puisse passer d'anciens arguments aléatoires, contrairement à **kwargs.

>>> func(b=2)
b: 2
>>> func(a=3, c=5)
a: 3
c: 5

Alors: y a-t-il une telle incantation? Dans mon cas, je suis capable de comparer chaque argument par rapport à sa valeur par défaut pour trouver ceux qui sont différents, mais c'est un peu inélégant et devient fastidieux quand vous avez neuf arguments. Pour les points bonus, fournissez une incantation qui peut me dire même lorsque l'appelant passe dans un argument de mot-clé attribué sa valeur par défaut:

>>> func(a=None)
a: None

Tricksy!

Edit: la signature de la fonction (lexicale) doit rester intacte. Cela fait partie d'une API publique, et la valeur principale des args de mots clés explicites réside dans leur valeur documentaire. Juste pour rendre les choses intéressantes. :)

21
demandé sur Cœur 2009-09-11 07:21:12

8 réponses

J'ai été inspiré par la bonté du décorateur de Lost-theory, et après avoir joué un peu avec elle, j'ai trouvé ceci:

def actual_kwargs():
    """
    Decorator that provides the wrapped function with an attribute 'actual_kwargs'
    containing just those keyword arguments actually passed in to the function.
    """
    def decorator(function):
        def inner(*args, **kwargs):
            inner.actual_kwargs = kwargs
            return function(*args, **kwargs)
        return inner
    return decorator


if __name__ == "__main__":

    @actual_kwargs()
    def func(msg, a=None, b=False, c='', d=0):
        print msg
        for arg, val in sorted(func.actual_kwargs.iteritems()):
            print '  %s: %s' % (arg, val)

    func("I'm only passing a", a='a')
    func("Here's b and c", b=True, c='c')
    func("All defaults", a=None, b=False, c='', d=0)
    func("Nothin'")
    try:
        func("Invalid kwarg", e="bogon")
    except TypeError, err:
        print 'Invalid kwarg\n  %s' % err

Qui imprime ceci:

I'm only passing a
  a: a
Here's b and c
  b: True
  c: c
All defaults
  a: None
  b: False
  c: 
  d: 0
Nothin'
Invalid kwarg
  func() got an unexpected keyword argument 'e'

Je suis content de ça. Une approche plus flexible consiste à passer le nom de l'attribut que vous souhaitez utiliser au décorateur, au lieu de le coder en dur à 'actual_kwargs', mais c'est l'approche la plus simple qui illustre la solution.

Mmm, Python est savoureux.

24
répondu Doctor J 2009-09-11 06:35:23

Voici le moyen le plus simple et le plus simple:

def func(a=None, b=None, c=None):
    args = locals().copy()
    print args

func(2, "egg")

Cela donne la sortie: {'a': 2, 'c': None, 'b': 'egg'}. La raison pour laquelle args devrait être une copie du dictionnaire locals est que les dictionnaires sont mutables, donc si vous avez créé des variables locales dans cette fonction args contiendrait toutes les variables locales et leurs valeurs, pas seulement les arguments.

Plus de documentation sur la fonction locals intégrée ici .

12
répondu Alvin Row 2009-09-11 03:48:38

Une possibilité:

def f(**kw):
  acceptable_names = set('a', 'b', 'c')
  if not (set(kw) <= acceptable_names):
    raise WhateverYouWantException(whatever)
  ...proceed...

IOW, il est très facile de vérifier que les noms passés sont dans l'ensemble acceptable et sinon soulever tout ce que vous voudriez que Python soulève (TypeError, je suppose; -). Assez facile à transformer en décorateur, btw.

Une Autre possibilité:

_sentinel = object():
def f(a=_sentinel, b=_sentinel, c=_sentinel):
   ...proceed with checks `is _sentinel`...

En créant un objet unique _sentinel, vous supprimez le risque que l'appelant passe accidentellement None (ou d'autres valeurs par défaut non uniques que l'appelant pourrait éventuellement transmettre). C'est tout object() est bon pour, btw: une sentinelle unique et extrêmement légère qui ne peut pas être accidentellement confondue avec un autre objet (lorsque vous vérifiez avec l'opérateur is).

L'une ou l'autre solution est préférable pour des problèmes légèrement différents.

5
répondu Alex Martelli 2009-09-11 03:31:08

Que diriez-vous d'utiliser un décorateur pour valider les kwargs entrants?

def validate_kwargs(*keys):
    def entangle(f):
        def inner(*args, **kwargs):
            for key in kwargs:
                if not key in keys:
                    raise ValueError("Received bad kwarg: '%s', expected: %s" % (key, keys))
            return f(*args, **kwargs)
        return inner
    return entangle

###

@validate_kwargs('a', 'b', 'c')
def func(**kwargs):
   for arg,val in kwargs.items():
       print arg, "->", val

func(b=2)
print '----'
func(a=3, c=5)
print '----'
func(d='not gonna work')

Donne cette sortie:

b -> 2
----
a -> 3
c -> 5
----
Traceback (most recent call last):
  File "kwargs.py", line 20, in <module>
    func(d='not gonna work')
  File "kwargs.py", line 6, in inner
    raise ValueError("Received bad kwarg: '%s', expected: %s" % (key, keys))
ValueError: Received bad kwarg: 'd', expected: ('a', 'b', 'c')
2
répondu Steven Kryskalla 2009-09-11 03:34:33

Ceci est plus facile à réaliser avec une seule instance d'un objet sentry:

# Top of module, does not need to be exposed in __all__
missing = {}

# Function prototype
def myFunc(a = missing, b = missing, c = missing):
    if a is not missing:
        # User specified argument a
    if b is missing:
        # User did not specify argument b

La bonne chose à propos de cette approche est que, puisque nous utilisons l'opérateur "is", l'appelant peut passer un dict vide comme valeur d'argument, et nous allons toujours comprendre qu'ils ne voulaient pas le passer. Nous évitons également les décorateurs méchants de cette façon, et gardons notre code un peu plus propre.

1
répondu Walt W 2012-05-08 18:46:38

Il y a probablement de meilleures façons de le faire, Mais voici ma prise:

def CompareArgs(argdict, **kwargs):
    if not set(argdict.keys()) <= set(kwargs.keys()):
        # not <= may seem weird, but comparing sets sometimes gives weird results.
        # set1 <= set2 means that all items in set 1 are present in set 2
        raise ValueError("invalid args")

def foo(**kwargs):
    # we declare foo's "standard" args to be a, b, c
    CompareArgs(kwargs, a=None, b=None, c=None)
    print "Inside foo"


if __name__ == "__main__":
    foo(a=1)
    foo(a=1, b=3)
    foo(a=1, b=3, c=5)
    foo(c=10)
    foo(bar=6)

Et sa sortie:

Inside foo
Inside foo
Inside foo
Inside foo
Traceback (most recent call last):
  File "a.py", line 18, in 
    foo(bar=6)
  File "a.py", line 9, in foo
    CompareArgs(kwargs, a=None, b=None, c=None)
  File "a.py", line 5, in CompareArgs
    raise ValueError("invalid args")
ValueError: invalid args

Cela pourrait probablement être converti en décorateur, mais mes décorateurs ont besoin de travail. Laissé comme un exercice au lecteur: P

0
répondu Mark Rushakoff 2009-09-11 03:36:13

Peut-être soulever une erreur s'ils passent des *args?

def func(*args, **kwargs):
  if args:
    raise TypeError("no positional args allowed")
  arg1 = kwargs.pop("arg1", "default")
  if kwargs:
    raise TypeError("unknown args " + str(kwargs.keys()))

Il serait simple de le factoriser en prenant une liste de varnames ou une fonction d'analyse générique à utiliser. Ce ne serait pas trop difficile d'en faire un décorateur (Python 3.1), aussi:

def OnlyKwargs(func):
  allowed = func.__code__.co_varnames
  def wrap(*args, **kwargs):
    assert not args
    # or whatever logic you need wrt required args
    assert sorted(allowed) == sorted(kwargs)
    return func(**kwargs)

Note: Je ne suis pas sûr à quel point cela fonctionnerait autour des fonctions déjà encapsulées ou des fonctions qui ont déjà *args ou **kwargs.

0
répondu Richard Levasseur 2009-09-11 03:37:09

La Magie n'est pas la réponse:

def funky(a=None, b=None, c=None):
    for name, value in [('a', a), ('b', b), ('c', c)]:
        print name, value
0
répondu Lennart Regebro 2015-06-11 06:52:14