在 Python 中强制命名参数

在 Python 中,你可能有一个函数定义:

def info(object, spacing=10, collapse=1)

可以用以下任何一种方式来称呼:

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

因为 Python 允许任意顺序的参数,只要它们被命名。

我们遇到的问题是,随着一些较大的函数的增长,人们可能会在 spacingcollapse之间添加参数,这意味着错误的值可能会指向未命名的参数。此外,有时候我们并不总是清楚需要放入什么。我们正在寻找一种强制人们命名某些参数的方法——不仅仅是一个编码标准,最好是一个标志或者 pydev 插件?

因此,在上面的4个示例中,只有最后一个将通过检查,因为所有的参数都被命名。

我们很有可能只在某些功能上打开它,但是任何关于如何实现这个功能的建议——或者它是否有可能实现的建议,我们都会感激不尽。

60561 次浏览

你可以使用 **操作符:

def info(**kwargs):

这样人们就不得不使用命名参数。

可以将函数声明为只接收 **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.

更新:

我意识到使用 **kwargs并不能解决这个问题。如果您的程序员按照自己的意愿更改函数参数,例如,可以将函数更改为:

def info(foo, **kwargs):

旧的代码将再次中断(因为现在每个函数调用都必须包含第一个参数)。

这真的取决于布莱恩怎么说。


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

一般来说,当更改函数时,新参数应该一直到最后。否则就会破坏密码。应该很明显。
如果有人更改函数以致代码中断,则必须拒绝此更改。
(正如布莱恩所说,这就像一份合同。)

(...)有时候并不总是很清楚,什么需要进去。

通过查看函数的签名(即 def info(object, spacing=10, collapse=1)) ,应该可以立即看到每个具有 没有默认值的参数都是必需的。
参数 的值应该放到 docstring 中。


旧答案(保留完整性) :

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

您可以这样定义函数:

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是一个包含任何关键字参数的字典。您可以检查是否存在强制参数,如果没有,则引发异常。

缺点是,它可能不再那么明显,可以使用哪些参数,但使用适当的 docstring,应该没问题。

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

在 python 中,如果使用 * args,这意味着可以传递 n 个位置参数,这些参数将作为元组在函数中访问。

如果使用 * * kw 意味着它的关键字参数,这可以作为 dict- 你可以传递 n 个 kw 参数,如果你想限制用户必须输入序列和参数,那么不要使用 * 和 * *-(它的 pythonic 方式为大型架构提供通用的解决方案...)

如果你想用默认值限制你的函数,那么你可以检查它的内部

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

正如其他答案所说,更改函数签名是一个糟糕的主意。要么在结尾添加新参数,要么在插入参数时修复每个调用方。

如果您仍然希望这样做,请使用 功能装饰器功能装饰器检查 getargspec函数。它的用途是这样的:

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

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

我不会麻烦你的。每次调用这个函数时,它都会变慢,而且通过更仔细地编写代码,您会得到更好的结果。

我不明白为什么一个程序员首先要在两个参数之间添加一个参数。

如果您希望函数参数与名称一起使用(例如 info(spacing=15, object=odbchelper)) ,那么它们的定义顺序应该无关紧要,所以您不妨将新参数放在最后。

如果你确实希望订单重要,那么不能指望任何工作,如果你改变它!

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

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

通过使第一个参数成为一个没有名称的位置参数,你可以强迫每个调用函数的人使用关键字参数,我想这就是你要问的。在 Python 2中,实现这一点的唯一方法是定义这样的函数

def foo(**kwargs):
pass

这会迫使调用者使用 kwargs,但这不是一个很好的解决方案,因为你必须放一张支票,只接受你需要的参数。

在 Python 3-Yes 中,您可以在参数列表中指定 *

来自 医生:

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

>>> 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 是否与其他编程语言有任何不同。除了 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切换到位置模式。是的,这也可以看作是一个专业的。

请记住,这个答案只对 Python2有效。Python 3实现了其他答案中描述的类似但非常优雅的语言支持机制。

我发现,当我敞开心扉思考时,没有任何问题或他人的决定看起来愚蠢、愚蠢或者只是愚蠢。恰恰相反: 我通常能学到很多东西。

您可以用 可以同时在 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)并在智能编辑器中丢失签名信息。你的社会契约是提供某些信息,通过强迫(其中一些)要求关键字,这些关键字呈现的顺序,已经变得无关紧要。

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

考虑下面的 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,则应该避免使用该配方