如何告诉函数使用默认参数值?

时间:2019-04-03 14:39:30

标签: python python-3.x

我有一个调用math.isclose的函数foo

import math
def foo(..., rtol=None, atol=None):
    ...
    if math.isclose(x, y, rel_tol=rtol, abs_tol=atol):
        ...
    ...

如果我没有将rtolatol传递给foo,上述操作将在math.isclose中失败:

TypeError: must be real number, not NoneType

我不想将系统默认参数值放入我的代码中( 如果他们改变了方向?)

这是我到目前为止的想法:

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

这看起来很长很傻,并且在每次调用时分配一个dict foo(它以递归方式调用,所以这 很重要)。

因此,告诉math.isclose使用默认设置的最佳方法是什么 容忍度?

PS。有几个相关的问题。请注意,我不是想要了解math.isclose的实际默认参数-我只想告诉它使用默认值,无论它们是什么。

4 个答案:

答案 0 :(得分:11)

一种解决方法是可变参数分解:

def foo(..., **kwargs):
    ...
    if math.isclose(x, y, **kwargs):
        ...

这将允许您将atolrtol指定为主要函数foo的关键字参数,然后将其不变地传递给math.isclose

但是,我也要说,传递给kwargs的参数以某种方式修改函数的行为是很习惯的,而不是仅仅传递给被调用的子函数。因此,我建议改用一个参数命名,这样很明显它将被解压缩并原样传递给子函数:

def foo(..., isclose_kwargs={}):
    ...
    if math.isclose(x, y, **isclose_kwargs):
        ...

您可以在matplotlib中看到一个等效的模式(例如:plt.subplots-subplot_kwgridspec_kw,所有其他关键字参数都传递给Figure构造函数分别为**fig_kwseaborn(例如:FacetGrid-subplot_kwsgridspec_kws)。

当存在可能需要传递关键字参数的多个子功能时,这一点尤其明显。否则,保留默认行为:

def foo(..., f1_kwargs={}, f2_kwargs={}, f3_kwargs={}):
    ...
    f1(**f1_kwargs)
    ...
    f2(**f2_kwargs)
    ...
    f3(**f3_kwargs)
    ...

注意:

请注意default arguments are only instantiated once,因此您不应在函数中修改空的dicts。如果需要,您应该改为使用None作为默认参数,并在每次运行函数时实例化一个新的空dict

def foo(..., isclose_kwargs=None):
    if isclose_kwargs is None:
        isclose_kwargs = {}
    ...
    if math.isclose(x, y, **isclose_kwargs):
        ...

我的偏好是避免这种情况,因为它比较简短,因此您不知道自己在做什么,并且一般而言,我不喜欢重新绑定变量。但是,这绝对是一个有效的习惯用法,它可以更安全。

答案 1 :(得分:11)

正确的解决方案是使用与math.isclose()相同的默认值。无需对其进行硬编码,因为您可以使用inspect.signature() function获取 current 默认值:

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

快速演示:

>>> import inspect
>>> import math
>>> params = inspect.signature(math.isclose).parameters
>>> params['rel_tol'].default
1e-09
>>> params['abs_tol'].default
0.0

之所以可行,是因为math.isclose()使用Argument Clinic tool定义了其自变量:

  

[争论]诊所的最初动机是为CPython内置函数提供内省的“签名”。过去,如果您传入内置函数,自省查询功能将引发异常。有了Argument Clinic,这已经成为过去!

在幕后,math.isclose()签名实际上存储为字符串:

>>> math.isclose.__text_signature__
'($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)'

inspect签名支持对此进行了解析,以为您提供实际值。

并非所有C定义的函数都使用Argument Clinic ,代码库是根据具体情况进行转换的。 math.isclose()converted for Python 3.7.0

您可以使用__doc__字符串作为后备,因为在早期版本中,它也包含签名:

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

所以通用的后备方式可能是:

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发行版。该函数将生成一个以参数名称为关键字的字典:

>>> 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默认值是非常低的风险;除非有错误,否则值不容易更改。因此,另一种选择是将3.5 / 3.6的默认默认值硬编码为后备,并使用3.7及更高版本中提供的签名:

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

但是请注意,您不支持未来,其他参数和默认值的风险更大。至少inspect.signature()方法可以让您添加有关代码期望的参数数量的断言。

答案 2 :(得分:7)

使用函数的默认参数确实没有很多方法...您只有两个选择:

  1. 传递真实的默认值
  2. 完全不要传递参数

由于所有选项都不是很好,因此我将列出详尽的清单,以便您可以将它们全部进行比较。

  • 使用**kwargs传递参数

    使用**kwargs定义您的方法,并将其传递给math.isclose

    def foo(..., **kwargs):
        ...
        if math.isclose(x, y, **kwargs):
    

    缺点:

    • 两个函数的参数名称必须匹配(例如foo(1, 2, rtol=3)不起作用)
  • 手动构建一个**kwargs字典

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

    缺点:

    • 丑陋,难以编码,而且速度不快
  • 对默认值进行硬编码

    def foo(..., rtol=1e-09, atol=0.0):
        ...
        if math.isclose(x, y, rel_tol=rtol, abs_tol=atol):
    

    缺点:

    • 硬编码值
  • 使用自省功能查找默认值

    您可以使用inspect模块在​​运行时确定默认值:

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

    缺点:

答案 3 :(得分:3)

将递归委托给一个辅助函数,这样您只需构建一次dict

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)

或者,不要将公差传递给foo,而传递已经包含所需公差的函数。

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