我们如何在调用函数时强制命名参数?
- 2025-03-18 08:54:00
- admin 原创
- 49
问题描述:
在 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 个例子中,只有最后一个会通过检查,因为所有参数都已命名。
解决方案 1:
在 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):
完成示例:
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'
因此,您被迫始终给出值。如果您不调用它,则不必执行任何其他名为参数强制的操作。
解决方案 2:
您可以通过以下方式定义函数强制人们在 Python3 中使用关键字参数。
def foo(*, arg0="default0", arg1="default1", arg2="default2"):
pass
通过将第一个参数设为没有名称的位置参数,您可以强制调用该函数的每个人都使用关键字参数,我认为这正是您问的。在 Python2 中,唯一的方法是定义一个这样的函数
def foo(**kwargs):
pass
这会强制调用者使用 kwargs,但这不是一个很好的解决方案,因为你必须检查是否只接受你需要的参数。
解决方案 3:
确实,大多数编程语言都将参数顺序作为函数调用契约的一部分,但这并不是必须的。为什么会这样?那么,我对这个问题的理解是,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
在正确位置使用 sentinel 对象切换到位置模式。是的,这也可以看作是一个优点。
请记住,这个答案仅对 Python 2 有效。Python 3 实现了其他答案中描述的类似但非常优雅的语言支持机制。
我发现,当我敞开心扉思考时,任何问题或他人的决定都不会显得愚蠢、无知或可笑。恰恰相反:我通常能学到很多东西。
解决方案 4:
你可以通过一种在 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,则应避免使用
解决方案 6:
更新:
我意识到使用**kwargs
并不能解决问题。如果你的程序员随意更改函数参数,那么你可以将函数更改为:
def info(foo, **kwargs):
旧代码会再次中断(因为现在每个函数调用都必须包含第一个参数)。
这实际上取决于布莱恩说了什么。
(...)人们可能会在
spacing
和之间添加参数collapse
(...)
一般来说,当改变函数时,新的参数应该总是放在最后。否则会破坏代码。这应该是显而易见的。
如果有人改变函数导致代码破坏,那么这种改变必须被拒绝。
(正如 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
是一个包含任意关键字参数的字典。您可以检查是否存在强制参数,如果不存在,则引发异常。
缺点是,哪些参数是可能的可能不再那么明显,但有了适当的文档字符串,就应该没问题。
解决方案 7:
正如其他答案所说,更改函数签名不是一个好主意。要么在末尾添加新参数,要么在插入参数时修复每个调用者。
如果您仍想这样做,请使用函数装饰器和inspect.getargspec函数。它的用法如下:
@require_named_args
def info(object, spacing=10, collapse=1):
....
的实现require_named_args
留给读者作为练习。
我不会费心。每次调用该函数时都会很慢,如果能更仔细地编写代码,您将获得更好的结果。
解决方案 8:
您可以将函数声明为**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.
解决方案 9:
def cheeseshop(kind, *arguments, **keywords):
在 python 中如果使用 *args 意味着您可以为此参数传递 n 个位置参数 - 这些参数将作为函数内部的元组进行访问。
如果使用 kw ,那意味着它的关键字参数,可以作为字典访问 - 您可以传递 n 个 kw 参数,如果您想限制用户必须按顺序输入序列和参数,那么不要使用 * 和 - (这是为大型架构提供通用解决方案的 Python 方式......)
如果你想用默认值来限制你的函数,那么你可以在它里面检查
def info(object, spacing, collapse)
spacing = 10 if spacing is None else spacing
collapse = 1 if collapse is None else collapse
解决方案 10:
您可以使用**
运算符:
def info(**kwargs):
这样人们就被迫使用命名参数。
解决方案 11:
我不明白为什么程序员首先要在两个参数之间添加一个参数。
如果您希望函数参数与名称一起使用(例如info(spacing=15, object=odbchelper)
),那么它们的定义顺序就无关紧要了,因此您不妨将新参数放在最后。
如果您确实希望顺序重要,那么改变它就别指望任何事情都能起作用!
扫码咨询,免费领取项目管理大礼包!