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. :)
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.
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 .
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.
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')
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.
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
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
.
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