在Python中强制命名参数

问题:在Python中强制命名参数

在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个示例中,由于所有参数均已命名,因此只有最后一个示例可以通过检查。

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

In Python you may have a function definition:

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

which could be called in any of the following ways:

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

thanks to Python’s allowing of any-order arguments, so long as they’re named.

The problem we’re having is as some of our larger functions grow, people might be adding parameters between spacing and collapse, meaning that the wrong values may be going to parameters that aren’t named. In addition sometimes it’s not always clear as to what needs to go in. We’re after a way to force people to name certain parameters – not just a coding standard, but ideally a flag or pydev plugin?

so that in the above 4 examples, only the last would pass the check as all the parameters are named.

Odds are we’ll only turn it on for certain functions, but any suggestions as to how to implement this – or if it’s even possible would be appreciated.


回答 0

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

In Python 3 – Yes, you can specify * in the argument list.

From docs:

Parameters after “*” or “*identifier” are keyword-only parameters and may only be passed used keyword arguments.

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

This can also be combined with **kwargs:

def foo(pos, *, forcenamed, **kwargs):

回答 1

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

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

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

def foo(**kwargs):
    pass

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

You can force people to use keyword arguments in Python3 by defining a function in the following way.

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

By making the first argument a positional argument with no name you force everyone who calls the function to use the keyword arguments which is what I think you were asking about. In Python2 the only way to do this is to define a function like this

def foo(**kwargs):
    pass

That’ll force the caller to use kwargs but this isn’t that great of a solution as you’d then have to put a check to only accept the argument that you need.


回答 2

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

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

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

True, most programming languages make parameter order part of the function call contract, but this doesn’t need to be so. Why would it? My understanding of the question is, then, if Python is any different to other programming languages in this respect. In addition to other good answers for Python 2, please consider the following:

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

The only way a caller would be able to provide arguments spacing and collapse positionally (without an exception) would be:

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

The convention of not using private elements belonging to other modules already is very basic in Python. As with Python itself, this convention for parameters would only be semi-enforced.

Otherwise, calls would need to be of the form:

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

A call

info(arg1, arg2, arg3, 11, 2)

would assign value 11 to parameter _p and an exception risen by the function’s first instruction.

Characteristics:

  • Parameters before _p=__named_only_start are admitted positionally (or by name).
  • Parameters after _p=__named_only_start must be provided by name only (unless knowledge about the special sentinel object __named_only_start is obtained and used).

Pros:

  • Parameters are explicit in number and meaning (the later if good names are also chosen, of course).
  • If the sentinel is specified as first parameter, then all arguments need to be specified by name.
  • When calling the function, it’s possible to switch to positional mode by using the sentinel object __named_only_start in the corresponding position.
  • A better performance than other alternatives can be anticipated.

Cons:

  • Checking occurs during run-time, not compile-time.
  • Use of an extra parameter (though not argument) and an additional check. Small performance degradation respect to regular functions.
  • Functionality is a hack without direct support by the language (see note below).
  • When calling the function, it’s possible to switch to positional mode by using the sentinel object __named_only_start in the right position. Yes, this can also be seen as a pro.

Please do keep in mind that this answer is only valid for Python 2. Python 3 implements the similar, but very elegant, language-supported mechanism described in other answers.

I’ve found that when I open my mind and think about it, no question or other’s decision seems stupid, dumb, or just silly. Quite on the contrary: I typically learn a lot.


回答 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)在智能编辑器中使用和丢失签名信息。您的社会契约是通过强迫(其中一些)要求关键字(它们的显示顺序)变得不相关来提供某些信息。

You can do that in a way that works in both Python 2 and Python 3, by making a “bogus” first keyword argument with a default value that will not occur “naturally”. That keyword argument can be preceded by one or more arguments without value:

_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")

This will allow:

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

but not:

info(odbchelper, 12)                

If you change the function to:

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

then all arguments must have keywords and info(odbchelper) will no longer work.

This will allow you to position additional keyword arguments any place after _kw, without forcing you to put them after the last entry. This often makes sense, e.g. grouping thing logically or arranging keywords alphabetically can help with maintenance and development.

So there is no need to revert to using def(**kwargs) and losing the signature information in your smart editor. Your social contract is to provide certain information, by forcing (some of them) to require keywords, the order these are presented in, has become irrelevant.


回答 4

更新:

我意识到使用**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是包含任何关键字参数的字典。您可以检查是否存在强制性参数,如果不存在,则引发异常。

不利的一面是,可能不再是显而易见的,哪些参数是可能的,但是使用适当的文档字符串,应该没问题。

Update:

I realized that using **kwargs would not solve the problem. If your programmers change function arguments as they wish, one could, for example, change the function to this:

def info(foo, **kwargs):

and the old code would break again (because now every function call has to include the first argument).

It really comes down to what Bryan says.


(…) people might be adding parameters between spacing and collapse (…)

In general, when changing functions, new arguments should always go to the end. Otherwise it breaks the code. Should be obvious.
If someone changes the function so that the code breaks, this change has to be rejected.
(As Bryan says, it is like a contract)

(…) sometimes it’s not always clear as to what needs to go in.

By looking at the signature of the function (i.e def info(object, spacing=10, collapse=1) ) one should immediately see that every argument that has not a default value, is mandatory.
What the argument is for, should go into the docstring.


Old answer (kept for completeness):

This is probably not a good solution:

You can define functions this way:

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 is a dictionary that contains any keyword argument. You can check whether a mandatory argument is present and if not, raise an exception.

The downside is, that it might not be that obvious anymore, which arguments are possible, but with a proper docstring, it should be fine.


回答 5

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中应避免使用

The python3 keyword-only arguments (*) can be simulated in python2.x with **kwargs

Consider the following python3 code:

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

and its behaviour:

>>> 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'

This can be simulated using the following, note I’ve taken the liberty of switching TypeError to KeyError in the “required named argument” case, it wouldn’t be too much work to make that the same exception type as well

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)

And behaviour:

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

The recipe works equally as well in python3.x, but should be avoided if you are python3.x only


回答 6

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

You could declare your functions as receiving **args only. That would mandate keyword arguments but you’d have some extra work to make sure only valid names are passed in.

def foo(**args):
   print args

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

回答 7

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

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

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

的实现require_named_args留给读者练习。

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

As other answers say, changing function signatures is a bad idea. Either add new parameters to the end, or fix every caller if arguments are inserted.

If you still want to do it, use a function decorator and the inspect.getargspec function. It would be used something like this:

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

Implementation of require_named_args is left as an exercise for the reader.

I would not bother. It will be slow every time the function is called, and you will get better results from writing code more carefully.


回答 8

您可以使用**运算符:

def info(**kwargs):

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

You could use the ** operator:

def info(**kwargs):

this way people are forced to use named parameters.


回答 9

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
def cheeseshop(kind, *arguments, **keywords):

in python if use *args that means you can pass n-number of positional arguments for this parameter – which will be accessed as a tuple inside the function.

And if use **kw that means its keyword arguments, that can be access as dict – you can pass n-number of kw args, and if you want to restrict that user must enter the sequence and arguments in order then don’t use * and ** – (its pythonic way to provide generic solutions for big architectures…)

if you want to restrict your function with default values then you can check inside it

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

回答 10

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

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

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

I don’t get why a programmer will add a parameter in between two others in the first place.

If you want the function parameters to be used with names (e.g. info(spacing=15, object=odbchelper) ) then it shouldn’t matter what order they are defined in, so you might as well put the new parameters at the end.

If you do want the order to matter then can’t expect anything to work if you change it!