27

我梦想着一个带有显式关键字 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,但我不希望调用者能够传递任何旧的随机参数,不像**kwargs.

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

那么:有这样的咒语吗?就我而言,我碰巧能够将每个参数与其默认值进行比较,以找到不同的参数,但这有点不雅,当你有九个参数时会变得乏味。对于加分,请提供一个咒语,即使调用者传入分配其默认值的关键字参数,它也可以告诉我:

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

调皮!

编辑:(词法)函数签名必须保持不变。它是公共 API 的一部分,显式关键字 args 的主要价值在于它们的文档价值。只是为了让事情变得有趣。:)

4

8 回答 8

31

我受到了失落理论的装饰师的启发,在玩了一会儿之后想出了这个:

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

打印这个:

我只是通过一个
  一个:一个
这是b和c
  乙:真
  丙:丙
所有默认值
  答:无
  b:错误
  C:
  d: 0
没什么
无效的夸格
  func() 得到了一个意外的关键字参数 'e'

我对此很满意。更灵活的方法是将要使用的属性名称传递给装饰器,而不是将其硬编码为“actual_kwargs”,但这是说明解决方案的最简单方法。

嗯,Python很好吃。

于 2009-09-11T06:25:43.097 回答
20

这是最简单和最简单的方法:

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

func(2, "egg")

这给出了输出:{'a': 2, 'c': None, 'b': 'egg'}. 原因args应该是字典的副本locals是字典是可变的,因此如果您在此函数中创建任何局部变量,args将包含所有局部变量及其值,而不仅仅是参数。

更多关于内置locals函数的文档在这里

于 2009-09-11T03:37:44.530 回答
7

一种可能:

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()都有好处:一个极其轻量级、独特的哨兵,不可能意外地与任何其他对象混淆(当您与is操作员核对时)。

对于略有不同的问题,任何一种解决方案都更可取。

于 2009-09-11T03:31:08.570 回答
2

使用装饰器来验证传入的 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')
于 2009-09-11T03:34:33.657 回答
1

使用哨兵对象的单个实例最容易做到这一点:

# 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 作为参数值,我们仍然会发现他们并不打算传递它。我们也可以通过这种方式避免讨厌的装饰器,并使我们的代码更简洁。

于 2012-05-08T18:46:38.660 回答
0

魔术不是答案:

def funky(a=None, b=None, c=None):
    for name, value in [('a', a), ('b', b), ('c', c)]:
        print name, value
于 2009-09-11T06:53:28.173 回答
0

可能有更好的方法来做到这一点,但这是我的看法:

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)

及其输出:

里面 foo
里面 foo
里面 foo
里面 foo
回溯(最近一次通话最后):
  文件“a.py”,第 18 行,在
    富(酒吧=6)
  文件“a.py”,第 9 行,在 foo 中
    CompareArgs(kwargs, a=None, b=None, c=None)
  文件“a.py”,第 5 行,在 CompareArgs 中
    引发ValueError(“无效参数”)
ValueError:无效的参数

这可能会转换为装饰器,但我的装饰器需要工作。留给读者作为练习:P

于 2009-09-11T03:36:13.690 回答
0

如果他们通过任何 *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已经存在的函数的工作情况如何。

于 2009-09-11T03:37:09.643 回答