The correct solution would be to use the same defaults as math.isclose()
. There is no need to hard-code them, as you can get the current defaults with the inspect.signature()
function:
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):
# ...
Quick demo:
>>> import inspect
>>> import math
>>> params = inspect.signature(math.isclose).parameters
>>> params['rel_tol'].default
1e-09
>>> params['abs_tol'].default
0.0
This works because math.isclose()
defines its arguments using the Argument Clinic tool:
[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!
Under the hood, the math.isclose()
signature is actually stored as a string:
>>> math.isclose.__text_signature__
'($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)'
This is parsed out by the inspect
signature support to give you the actual values.
Not all C-defined functions use Argument Clinic yet, the codebase is being converted on a case-by-case basis. math.isclose()
was converted for Python 3.7.0.
You could use the __doc__
string as a fallback, as in earlier versions this too contains the signature:
>>> 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'
so a slightly more generic fallback could be:
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
}
I'd see this as a stop-gap measure only needed to support older Python 3.x releases. The function produces a dictionary keyed on parameter name:
>>> 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}
Note that copying the Python defaults is very low risk; unless there is a bug, the values are not prone to change. So another option could be to hardcode the 3.5 / 3.6 known defaults as a fallback, and use the signature provided in 3.7 and newer:
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
Note however that you are at greater risk of not supporting future, additional parameters and defaults. At least the inspect.signature()
approach would let you add an assertion about the number of parameters that your code expects there to be.