How to tell a function to use the default argument values?
我有一个函数
1 2 3 4 5 6 | import math def foo(..., rtol=None, atol=None): ... if math.isclose(x, y, rel_tol=rtol, abs_tol=atol): ... ... |
如果我不把
1 | TypeError: must be real number, not NoneType |
我不想在我的代码中放入系统默认参数值(什么如果他们改变了方向?)
以下是我到目前为止的想法:
1 2 3 4 5 6 7 8 9 10 11 | import math def foo(..., rtol=None, atol=None): ... tols = {} if rtol is not None: tols["rel_tol"] = rtol if atol is not None: tols["abs_tol"] = atol if math.isclose(x, y, **tols): ... ... |
这看起来又长又傻,在每次调用
那么,告诉
还有几个相关的问题。注意,我不想知道
正确的解决方案是使用与
1 2 3 4 5 6 7 | import inspect import math _isclose_params = inspect.signature(math.isclose).parameters def foo(..., rtol=_isclose_params['rel_tol'].default, atol=_isclose_params['abs_tol'].default): # ... |
快速演示:
1 2 3 4 5 6 7 | >>> import inspect >>> import math >>> params = inspect.signature(math.isclose).parameters >>> params['rel_tol'].default 1e-09 >>> params['abs_tol'].default 0.0 |
这是因为
[T]he original motivation for Argument Clinic was to provide introspection"signatures" for CPython builtins. It used to be, the introspection query functions would throw an exception if you passed in a builtin. With Argument Clinic, that’s a thing of the past!
在引擎盖下,
1 2 | >>> math.isclose.__text_signature__ '($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)' |
这是由
并不是所有的C定义函数都使用参数clinic,代码库是根据具体情况进行转换的。
您可以使用
1 2 3 4 5 6 | >>> import math >>> import sys >>> sys.version_info sys.version_info(major=3, minor=6, micro=8, releaselevel='final', serial=0) >>> math.isclose.__doc__.splitlines()[0] 'isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0) -> bool' |
因此,稍微更一般的回退可能是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import inspect def func_defaults(f): try: params = inspect.signature(f).parameters except ValueError: # parse out the signature from the docstring doc = f.__doc__ first = doc and doc.splitlines()[0] if first is None or f.__name__ not in first or '(' not in first: return {} sig = inspect._signature_fromstr(inspect.Signature, math.isclose, first) params = sig.parameters return { name: p.default for name, p in params.items() if p.default is not inspect.Parameter.empty } |
我认为这只是支持旧的python 3.x版本所需要的一种权宜之计。函数生成一个键入参数名的字典:
1 2 3 4 5 6 | >>> import sys >>> import math >>> sys.version_info sys.version_info(major=3, minor=6, micro=8, releaselevel='final', serial=0) >>> func_defaults(math.isclose) {'rel_tol': 1e-09, 'abs_tol': 0.0} |
请注意,复制Python默认值的风险非常低;除非存在bug,否则值不容易更改。因此,另一种选择是将3.5/3.6的默认值硬编码为回退,并使用3.7和更新版本中提供的签名:
1 2 3 4 5 6 7 8 9 | try: # Get defaults through introspection in newer releases _isclose_params = inspect.signature(math.isclose).parameters _isclose_rel_tol = _isclose_params['rel_tol'].default _isclose_abs_tol = _isclose_params['abs_tol'].default except ValueError: # Python 3.5 / 3.6 known defaults _isclose_rel_tol = 1e-09 _isclose_abs_tol = 0.0 |
但是请注意,您面临着更大的风险,不支持未来、附加参数和默认值。至少,
一种方法是使用变量参数解包:
1 2 3 4 | def foo(..., **kwargs): ... if math.isclose(x, y, **kwargs): ... |
这将允许您指定
但是,我也会说,传递给
1 2 3 4 | def foo(..., isclose_kwargs={}): ... if math.isclose(x, y, **isclose_kwargs): ... |
在
当存在多个子函数时,这一点尤其明显,您可能希望传递关键字参数,但保留默认行为,否则:
1 2 3 4 5 6 7 8 | def foo(..., f1_kwargs={}, f2_kwargs={}, f3_kwargs={}): ... f1(**f1_kwargs) ... f2(**f2_kwargs) ... f3(**f3_kwargs) ... |
Caveat:
请注意,默认参数只实例化一次,因此不应在函数中修改空的
1 2 3 4 5 6 | def foo(..., isclose_kwargs=None): if isclose_kwargs is None: isclose_kwargs = {} ... if math.isclose(x, y, **isclose_kwargs): ... |
我的偏好是避免在你知道你在做什么的地方这样做,因为它更简短,一般来说,我不喜欢重新绑定变量。但是,它绝对是一个有效的习语,而且更安全。
实际上没有很多方法可以让函数使用其默认参数…您只有两个选项:
由于没有一个选择是伟大的,我将做一个详尽的列表,以便你可以比较它们全部。
- 使用
**kwargs 传递参数使用
**kwargs 定义方法,并将其传递给math.isclose :1
2
3def foo(..., **kwargs):
...
if math.isclose(x, y, **kwargs):欺骗:
- 两个函数的参数名必须匹配(如
foo(1, 2, rtol=3) 不起作用)
- 两个函数的参数名必须匹配(如
- 手工构造一个
**kwargs dict1
2
3
4
5
6
7
8def foo(..., rtol=None, atol=None):
...
kwargs = {}
if rtol is not None:
kwargs["rel_tol"] = rtol
if atol is not None:
kwargs["abs_tol"] = atol
if math.isclose(x, y, **kwargs):欺骗:
- 丑陋,难以编码,而且速度不快
- 硬编码默认值
1
2
3def foo(..., rtol=1e-09, atol=0.0):
...
if math.isclose(x, y, rel_tol=rtol, abs_tol=atol):欺骗:
- 硬编码值
- 使用自省查找默认值
您可以使用
inspect 模块确定运行时的默认值:1
2
3
4
5
6
7
8
9import inspect, math
signature = inspect.signature(math.isclose)
DEFAULT_RTOL = signature.parameters['rel_tol'].default
DEFAULT_ATOL = signature.parameters['abs_tol'].default
def foo(..., rtol=DEFAULT_RTOL, atol=DEFAULT_ATOL):
...
if math.isclose(x, y, rel_tol=rtol, abs_tol=atol):欺骗:
inspect.signature 可能在内置函数或C中定义的其他函数上失败。
将递归委托给一个助手函数,这样您只需要构建一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import math def foo_helper(..., **kwargs): ... if math.isclose(x, y, **kwargs): ... ... def foo(..., rtol=None, atol=None): tols = {} if rtol is not None: tols["rel_tol"] = rtol if atol is not None: tols["abs_tol"] = atol return foo_helper(x, y, **tols) |
或者,不是将公差传递给
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from functools import partial # f can take a default value if there is a common set of tolerances # to use. def foo(x, y, f): ... if f(x,y): ... ... # Use the defaults foo(x, y, f=math.isclose) # Use some other tolerances foo(x, y, f=partial(math.isclose, rtol=my_rtel)) foo(x, y, f=partial(math.isclose, atol=my_atol)) foo(x, y, f=partial(math.isclose, rtol=my_rtel, atol=my_atol)) |