关于python:如何告诉函数使用默认参数值?

How to tell a function to use the default argument values?

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

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

如果我不把rtolatol传给foo的话,那么在math.isclose中上述问题就失败了:

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

这看起来又长又傻,在每次调用foo(它以递归方式调用自己,所以这是一件大事)。

那么,告诉math.isclose使用默认值的最佳方法是什么?公差?

还有几个相关的问题。注意,我不想知道math.isclose的实际默认参数——我只想告诉它使用默认值,不管它们是什么。


正确的解决方案是使用与math.isclose()相同的默认值。不需要对它们进行硬编码,因为您可以使用inspect.signature()函数获得当前的默认值:

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

这是因为math.isclose()使用参数临床工具定义其参数:

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

在引擎盖下,math.isclose()签名实际上存储为字符串:

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

这是由inspect签名支持解析出来的,为您提供实际值。

并不是所有的C定义函数都使用参数clinic,代码库是根据具体情况进行转换的。math.isclose()被转换为python 3.7.0。

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

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

但是请注意,您面临着更大的风险,不支持未来、附加参数和默认值。至少,inspect.signature()方法可以让您添加一个断言,说明您的代码期望有多少参数。


一种方法是使用变量参数解包:

1
2
3
4
def foo(..., **kwargs):
    ...
    if math.isclose(x, y, **kwargs):
        ...

这将允许您指定atolrtol作为主函数foo的关键字参数,然后它将原封不动地传递给math.isclose

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

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

matplotlibgridspec_kw中可以看到一个等价的模式,所有其他关键字参数都作为**fig_kwseaborn传递给Figure构造函数(例如:FacetGridsubplot_kwsgridspec_kws

当存在多个子函数时,这一点尤其明显,您可能希望传递关键字参数,但保留默认行为,否则:

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:

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

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
      3
      def foo(..., **kwargs):
          ...
          if math.isclose(x, y, **kwargs):

      欺骗:

      • 两个函数的参数名必须匹配(如foo(1, 2, rtol=3)不起作用)
    • 手工构造一个**kwargsdict
      1
      2
      3
      4
      5
      6
      7
      8
      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):

      欺骗:

      • 丑陋,难以编码,而且速度不快
    • 硬编码默认值
      1
      2
      3
      def 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
      9
      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):

      欺骗:

      • inspect.signature可能在内置函数或C中定义的其他函数上失败。


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

    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)

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

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