我们如何在调用函数时强制命名参数?

201
在Python中,你可以有一个函数定义:
def info(obj, spacing=10, collapse=1)

可以以以下任何方式调用:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

由于Python允许任意顺序的参数,只要它们被命名,我们感谢它。
我们遇到的问题是,随着一些较大的函数增长,人们可能会在spacing和collapse之间添加参数,这意味着错误的值可能会传递给未命名的参数。此外,有时并不总是清楚需要放什么。
我们如何强制人们命名某些参数-不仅仅是编码标准,而是最好是一个标志或pydev插件?
因此,在上述4个示例中,只有最后一个将通过检查,因为所有参数都已命名。

将参数命名为“object”会覆盖内置类。我建议使用“obj”或类似的名称。 - ggorlen
11个回答

365
在Python 3中,你可以在参数列表中指定*。 来自文档

在“*”或“*标识符”之后的参数是仅限关键字参数,只能使用关键字参数传递。

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

这也可以与 **kwargs 结合使用:

def foo(pos, *, forcenamed, **kwargs):
完成示例:
def foo(pos, *, forcenamed ):
    print(pos, forcenamed)

foo(pos=10, forcenamed=20)
foo(10, forcenamed=20)
# basically you always have to give the value!
foo(10)

输出:

Traceback (most recent call last):
  File "/Users/brando/anaconda3/envs/metalearning/lib/python3.9/site-packages/IPython/core/interactiveshell.py", line 3444, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-12-ab74191b3e9e>", line 7, in <module>
    foo(10)
TypeError: foo() missing 1 required keyword-only argument: 'forcenamed'

所以你被迫始终给出这个值。如果你不调用它,你就不必做任何其他被强制的命名参数。


除了响应之外,您还可以考虑使用Python的类型提示特性来强制执行函数签名。这样,您就可以通过使用mypy检查来检测不良调用。 - rkachach
PEP 3102 - 仅限关键字参数:https://peps.python.org/pep-3102/ - Jean Monet
注意:如果函数有装饰器,这将不起作用。相反,应该将其转发到具有装饰器的第二个(私有)函数。 - kevinarpe

63

你可以通过以下方式定义函数来强制在Python3中使用关键字参数。

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

通过使第一个参数成为无名称的位置参数,您强制调用函数的所有人使用关键字参数,这正是您所询问的内容。在Python2中,唯一的方法是定义如下的函数:

def foo(**kwargs):
    pass

这样将强制调用者使用kwargs,但这并不是一个很好的解决方案,因为你还需要检查只接受需要的参数。


14

确实,大多数编程语言都将参数顺序作为函数调用约定的一部分,但这并不一定是必须的。为什么要这样做呢?我的理解是,那么Python在这方面是否与其他编程语言有所不同。除了其他关于Python 2的好答案之外,请考虑以下内容:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

如果调用者要按位置提供参数 spacingcollapse(而不会出现异常),唯一的方法是:

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

在Python中,不使用属于其他模块的私有成员已经是非常基本的规定。与Python本身一样,这种参数约定也只会半加强。

否则,调用需要采用以下形式:

info(arg1, arg2, arg3, spacing=11, collapse=2)

一个调用
info(arg1, arg2, arg3, 11, 2)

将值11分配给参数_p,并且函数的第一条指令抛出了异常。

特点:

  • _p=__named_only_start之前的参数可以按位置(或名称)提供。
  • _p=__named_only_start之后的参数必须仅通过名称提供(除非获得并使用有关特殊标记对象__named_only_start的知识)。

优点:

  • 参数在数量和含义上是明确的(当然,如果选择了良好的名称,则后者也是如此)。
  • 如果将标记对象指定为第一个参数,则所有参数都需要通过名称指定。
  • 在调用函数时,可以通过在相应位置使用标记对象__named_only_start来切换到位置模式。
  • 比其他替代方案具有更好的性能。

缺点:

  • 检查发生在运行时而不是编译时。
  • 使用额外的参数(尽管不是参数)和附加检查。与常规函数相比,性能略有降低。
  • 功能是一种语言无直接支持的技巧(请参阅下面的说明)。
  • 在调用函数时,可以通过在正确的位置使用标记对象__named_only_start来切换到位置模式。是的,这也可以看作是优点。

请记住,此答案仅适用于Python 2。 Python 3实现了类似但非常简洁的语言支持机制,如其他答案所述。

我发现当我开放思维并思考问题时,没有任何问题或其他人的决定看起来是愚蠢、傻瓜或可笑的。相反:我通常会学到很多东西。


“检查发生在运行时,而不是编译时。” - 我认为所有函数参数检查都是如此。直到您实际执行函数调用行,您才知道正在执行哪个函数。另外,+1 - 这很聪明。 - Eric
@Eric:只是我更喜欢静态检查。但你说得对:那就不是Python了。虽然这不是决定性因素,但Python 3的“*”结构也是动态检查的。感谢您的评论。 - Mario Rossi
此外,如果您将模块变量命名为_named_only_start,则无法从外部模块引用它,这既有利又有弊。(如果我没记错的话,在模块范围内使用单个前导下划线表示私有) - Eric
关于哨兵的命名,我们可以同时拥有一个__named_only_start和一个named_only_start(没有初始下划线),第二个表示推荐使用命名模式,但不到“积极宣传”的程度(因为一个是公共的,另一个则不是)。 关于下划线开头的_names的“私密性”,语言并没有严格执行:可以通过使用特定的(非*)导入或限定名称轻松规避。这就是为什么几个Python文档倾向于使用“非公共”这个术语而不是“私有”。 - Mario Rossi

10

通过创建一个“虚假的”第一个关键字参数并为其设置默认值,可以以在Python 2和Python 3中都有效的方式来实现这一点。该关键字参数可以由一个或多个不带值的参数紧随其后:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

这将允许:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

但不包括:

info(odbchelper, 12)                

如果你将函数更改为:

def info(_kw=_dummy, spacing=10, collapse=1):

如果所有的参数都必须要有关键字,那么info(odbchelper)将不再起作用。

这将使您能够在_kw之后的任何位置放置其他关键字参数,而无需将它们放在最后一个条目之后。这通常是有意义的,例如:逻辑分组或按字母顺序排列关键字可以帮助维护和开发。

因此,没有必要回到使用def(**kwargs)并在智能编辑器中丢失签名信息。你的社交契约是提供某些信息,通过强制(其中一些)需要关键字,这些呈现的顺序已经变得不相关了。


5

Python3关键字参数(*)可以在python2.x中用**kwargs模拟。

考虑以下Python3代码:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

以及它的行为:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

可以使用以下方式进行模拟,注意我已经在“必需命名参数”情况下将TypeError更改为KeyError,使其相同的异常类型也不会太麻烦。

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

行为:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

这份代码在 Python3.x 中同样有效,但如果你只使用 Python3.x,则应避免使用它。


啊,kwargs.pop('foo') 是 Python 2 的习惯用法?我需要更新我的编程风格。我仍在 Python 3 中使用这种方法。 - Neil

2

更新:

我意识到使用 **kwargs 无法解决问题。如果你的程序员随心所欲地更改函数参数,例如,他们可以将函数更改为以下内容:

def info(foo, **kwargs):

旧代码会再次出现问题(因为现在每个函数调用都必须包含第一个参数)。

真正的问题在于Bryan所说的内容。


(...) 人们可能会在spacingcollapse之间添加参数 (...)

通常情况下,在更改函数时,新参数应始终放在最后。否则它会破坏代码。这一点应该很明显。
如果有人更改了函数以使代码崩溃,则必须拒绝此更改。
(如Bryan所说,这就像是一个合同)

(...) 有时候不太清楚需要放什么。

通过查看函数的签名(即 def info(object, spacing=10, collapse=1) ),您应该立即看到每个没有默认值的参数都是强制性的。
参数的作用应该写在文档字符串中。


旧答案(仅供参考):

这可能不是一个好的解决方案:

您可以通过以下方式定义函数:

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargs是一个包含任何关键字参数的字典。您可以检查是否存在必需的参数,如果没有,则引发异常。

缺点是可能不那么明显,哪些参数是可能的,但是使用适当的文档字符串应该没问题。


3
我更喜欢你之前的回答。只需在函数中添加注释说明为什么只接受 **kwargs。毕竟,任何人都可以更改源代码--您需要文档来描述决策背后的意图和目的。 - Brandon
这个答案中并没有实际的答案! - Phil
正如@Phil所说,目前来看,即使更新旧答案,实际上在这篇文章中也没有真正的答案。是的,尽管这篇文章已经超过10年了,但让时间的遗迹继续发挥作用吧。 - Nuclear03020704

1

正如其他答案所说,更改函数签名是一个不好的主意。要么在末尾添加新参数,要么如果插入了参数,则修复每个调用者。

如果您仍然想这样做,请使用function decoratorinspect.getargspec函数。它将类似于以下用法:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

留下require_named_args的实现作为读者的练习。

我不会费心去做。每次调用该函数时速度都会很慢,而且从仔细编写代码中可以得到更好的结果。


0
你可以将函数声明为仅接收**args。这将强制使用关键字参数,但您需要额外的工作来确保只传递有效的名称。
def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.

1
你不仅需要添加关键字检查,还要考虑到一个知道必须使用签名为foo(**kwargs)的方法的消费者。那我应该传入什么?foo(killme=True, when="rightnowplease") - Dagrooms
这要看情况。考虑使用 dict - Noufal Ibrahim

-1
你可以使用 ** 运算符:
def info(**kwargs):

这样人们就被强制使用命名参数。


2
没有头绪如何在不读代码的情况下调用你的方法,这会增加消费者的认知负荷 :( - Dagrooms
因为上述原因,这是非常糟糕的做法,应该避免。 - David S.

-1
def cheeseshop(kind, *arguments, **keywords):

在Python中,如果使用*args,这意味着您可以为此参数传递n个定位参数 - 该参数将作为元组在函数内部访问。

如果使用**kw,则表示其关键字参数,可以作为字典访问 - 您可以传递任意数量的kw args,并且如果要限制用户必须按顺序输入序列和参数,则不要使用*和** - (这是提供大型架构通用解决方案的Python方式...)

如果要使用默认值限制函数,则可以在其中进行检查

def info(object, spacing, collapse)
  spacing = 10 if spacing is None else spacing
  collapse = 1 if collapse is None else collapse

如果想把间距设置为0会发生什么?答案是10。这个答案就像所有其他**kwargs答案一样,出于同样的原因都是错误的。 - Phil
@phil 是的,我明白了。为此,我认为我们应该检查它是不是None - 我已经更新了答案。 - shahjapan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接