获取关键字参数实际上传递给Python方法
题
我梦想着一个带有显式关键字args的Python方法:
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)
我想得到一个字典,只有调用者实际传入方法的那些参数,就像 ** kwargs
一样,但我不希望调用者能够传递任何旧的随机args,与 ** kwargs
不同。
>>> func(b=2)
b: 2
>>> func(a=3, c=5)
a: 3
c: 5
那么:有这样的咒语吗?在我的情况下,我碰巧能够将每个参数与其默认值进行比较以找到不同的参数,但是当你有九个参数时,这有点不雅并且变得单调乏味。对于奖励积分,提供一个咒语,即使调用者传递了一个分配了默认值的关键字参数,也可以告诉我:
>>> func(a=None)
a: None
调皮!
编辑:(词汇)函数签名必须保持不变。它是公共API的一部分,显式关键字args的主要价值在于它们的文档值。只是为了让事情变得有趣。 :)
解决方案
我受到了失去理论的装饰师善良的启发,并且在玩了一段时间之后想出了这个:
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
打印出来:
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'
我对此很满意。更灵活的方法是将要使用的属性的名称传递给装饰器,而不是将其硬编码为“actual_kwargs”,但这是说明解决方案的最简单方法。
嗯,Python很好吃。其他提示
这是最简单最简单的方法:
def func(a=None, b=None, c=None):
args = locals().copy()
print args
func(2, "egg")
这给出了输出: {'a':2,'c':无,'b':'egg'}
。
args
的原因应该是 locals
字典的副本是字典是可变的,所以如果你在这个函数中创建了任何局部变量 args
包含所有局部变量及其值,而不仅仅是参数。
有关内置 locals
函数的更多文档此处
一种可能性:
def f(**kw):
acceptable_names = set('a', 'b', 'c')
if not (set(kw) <= acceptable_names):
raise WhateverYouWantException(whatever)
...proceed...
IOW,很容易检查传入的名称是否在可接受的集合内,否则会引发你想要Python引发的任何内容(TypeError,我猜;-)。很容易变成装饰,顺便说一句。
另一种可能性:
_sentinel = object():
def f(a=_sentinel, b=_sentinel, c=_sentinel):
...proceed with checks `is _sentinel`...
通过创建唯一对象 _sentinel
,可以消除调用者可能意外传递 None
(或调用者可能传递的其他非唯一默认值)的风险。这是所有 object()
都适用于btw:一个非常轻量级的独特标记,它不可能与任何其他对象意外混淆(当你用检查时
操作者)。
对于略有不同的问题,这两种解决方案都是优选的。
如何使用装饰器来验证传入的kwargs?
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')
给出这个输出:
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')
使用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
这种方法的好处在于,因为我们使用的是“is”。运算符,调用者可以传递一个空的dict作为参数值,我们仍然会发现它们并不意味着传递它。我们也以这种方式避免讨厌的装饰,并保持我们的代码更清洁。
这可能是更好的方法,但这是我的看法:
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)
及其输出:
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
这可能会被转换为装饰者,但我的装饰者需要工作。作为练习留给读者:P
如果他们通过任何* 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()))
将它考虑为使用一个varnames列表或一个通用的解析函数是很简单的。将它变成装饰器(python 3.1)也不会太难:
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)
注意:我不确定这对于已经包装过的 * args
或 ** kwargs
的函数或函数有多好处。
魔术不是答案:
def funky(a=None, b=None, c=None):
for name, value in [('a', a), ('b', b), ('c', c)]:
print name, value