在Python中强制命名参数


111

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

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

可以通过以下任何一种方式调用:

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

多亏了Python允许任意顺序的参数(只要它们被命名)。

我们遇到的问题是,随着一些更大的函数的增长,人们可能会在spacing和之间添加参数collapse,这意味着错误的值可能会传递给未命名的参数。此外,有时不清楚需要输入什么。我们正在寻找一种方法来强迫人们命名某些参数-不仅是编码标准,还是理想的标志或pydev插件?

因此,在上述4个示例中,由于所有参数均已命名,因此只有最后一个示例可以通过检查。

奇怪的是,我们只会为某些功能打开它,但是有关如何实现此功能的任何建议-甚至可能的话,我们将不胜感激。

Answers:


214

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

文档

“ *”或“ * identifier”之后的参数仅是关键字参数,并且只能使用关键字参数传递。

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

32

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

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

通过将第一个参数设置为不带名称的位置参数,您可以强制每个调用该函数的人都使用关键字参数,这正是我想问的。在Python2中,唯一的方法是定义一个这样的函数

def foo(**kwargs):
    pass

这将迫使调用者使用kwargs,但这并不是一个很好的解决方案,因为您随后必须进行检查以仅接受所需的参数。


11

的确,大多数编程语言都将参数顺序作为函数调用协定的一部分,但这不是必须的。为什么会这样?我对这个问题的理解是,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)

调用方能够提供参数spacing并按collapse位置(无例外)提供的唯一方法是:

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有效。Python3实现了类似的,但非常优雅的,语言支持的机制,在其他答案中也有描述。

我发现,当我打开思路思考时,没有问题或其他人的决定看起来是愚蠢,愚蠢或愚蠢的。恰恰相反:我通常会学到很多东西。


“检查发生在运行时,而不是编译时。” -我认为所有函数参数检查都是如此。在您实际执行函数调用的那一行之前,您并不总是知道正在执行哪个函数。另外,+ 1-这很聪明。
埃里克

@Eric:只是我偏爱进行静态检查。但是您是对的:根本不是Python。尽管不是决定性的要点,但是Python 3的“ *”构造也会被动态检查。谢谢你的评论。
Mario Rossi

另外,如果您将模块变量命名为_named_only_start,那么就不可能从外部模块中引用它,因为外部模块会带来一个优点和一个缺点。(模块范围内的单个下划线是私有的,IIRC)
Eric

关于前哨的命名,我们也可以同时使用a __named_only_start和a named_only_start(没有下划线),第二个表示“推荐”命名模式,但不能达到“积极推广”的水平(因为一个是公开的,其他不是)。关于_names以下划线开头的“私密性”,该语言并没有对它进行强制性的强制规定:可以通过使用特定的(非*)输入或限定名称来轻松地规避它。这就是为什么几个Python文档更喜欢使用术语“非公开”而不是“私有”的原因。
Mario Rossi

6

您可以通过使“伪造的”第一个关键字参数具有默认值而不会“自然地”出现,从而以在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)在智能编辑器中使用和丢失签名信息。您的社会契约是通过强迫(其中一些)要求关键字(它们的显示顺序)变得不相关来提供某些信息。


2

更新:

我意识到使用**kwargs并不能解决问题。如果您的程序员根据需要更改函数参数,则可以例如将函数更改为:

def info(foo, **kwargs):

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

确实归结为布莱恩所说的话。


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

通常,在更改函数时,新参数应始终结尾。否则,它将破坏代码。应该很明显。
如果有人更改了功能使代码中断,则必须拒绝此更改。
(正如布莱恩所说,这就像是一份合同)

(...)有时不清楚需要输入什么。

通过查看函数的签名(即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即可。毕竟,任何人都可以更改源代码中的任何内容-您需要文档来描述决策背后的意图和目的。
布兰登

这个答案没有实际答案!
Phil

2

python3-only关键字参数(*)可以在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'

可以使用以下方法对此进行模拟,请注意TypeErrorKeyError在“必需的命名参数”情况下,我可以自由切换到,同样要使相同的异常类型也不会花费太多工作

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

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
努法尔·易卜拉欣

0

正如其他答案所说,更改功能签名是一个坏主意。在末尾添加新参数,或者在插入参数的情况下修复每个调用方。

如果仍要执行此操作,请使用函数装饰器inspect.getargspec函数。它将使用如下形式:

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

的实现require_named_args留给读者练习。

我不会打扰。每次调用该函数的速度都会很慢,通过更仔细地编写代码,您将获得更好的结果。


-1

您可以使用**运算符:

def info(**kwargs):

这样,人们被迫使用命名参数。


2
而且不知道如何在不阅读代码的情况下调用方法,从而增加了消费者的认知负担:(
Dagrooms

由于上述原因,这实际上是一种不好的做法,应该避免。
David S.

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

在python中,如果使用* args,则意味着您可以为该参数传递n个参数-这将成为函数内部的列表以访问

如果使用** kw表示其关键字参数,则可以按dict的方式进行访问-您可以传递n个数量的kw args,并且如果要限制该用户必须按顺序输入序列和参数,则不要使用*和**-(它为大型架构提供通用解决方案的pythonic方法...)

如果要使用默认值限制功能,则可以在其中检查

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

如果希望间隔为0会怎样?(答案是10)。出于相同的原因,这个答案与所有​​其他** kwargs答案一样错误。
Phil

-2

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

如果您希望函数参数与名称一起使用(例如, info(spacing=15, object=odbchelper)),则定义它们的顺序无关紧要,因此您最好将新参数放在最后。

如果您确实希望订单很重要,那么更改后就别指望了!


2
这不能回答问题。不管这是一个好主意都无关紧要-仍然有人可以这样做。
Graeme Perrow,2015年

1
正如Graeme所说,总有人会这样做。另外,如果您要编写供他人使用的库,则在必须重构API时,强制(仅适用于python 3)(仅python 3)传递仅关键字参数可提供额外的灵活性。
s0undt3ch '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.