Как указать функции использовать значения аргументов по умолчанию?
У меня есть функция foo
которая вызывает math.isclose
:
import math
def foo(..., rtol=None, atol=None):
...
if math.isclose(x, y, rel_tol=rtol, abs_tol=atol):
...
...
Вышеприведенный сбой в math.isclose
если я не rtol
и atol
в foo
:
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
- все, что я хочу, это чтобы он использовал значения по умолчанию, какими бы они ни были.
Ответы
Ответ 1
Один из способов сделать это - распаковать переменные аргументы:
def foo(..., **kwargs):
...
if math.isclose(x, y, **kwargs):
...
Это позволит вам указать atol
и rtol
качестве аргументов ключевого слова для основной функции foo
, которую затем она передаст без изменений в math.isclose
.
Тем не менее, я бы также сказал, что идиоматично, что аргументы, передаваемые kwargs
изменяют поведение функции каким-либо иным способом, чем просто передаваемые вызываемым подфункциям. Поэтому я хотел бы предложить, чтобы вместо этого параметр был назван так, чтобы было ясно, что он будет распакован и передан в подфункцию без изменений:
def foo(..., isclose_kwargs={}):
...
if math.isclose(x, y, **isclose_kwargs):
...
Вы можете увидеть эквивалентный шаблон в matplotlib
(пример: plt.subplots
- subplot_kw
и gridspec_kw
, все остальные ключевые аргументы передаются в конструктор Figure
как **fig_kw
) и seaborn
(пример: FacetGrid
- subplot_kws
, gridspec_kws
).
Это особенно очевидно, когда есть несколько подфункций, которые вы можете передать аргументам ключевых слов, но в противном случае сохраните поведение по умолчанию:
def foo(..., f1_kwargs={}, f2_kwargs={}, f3_kwargs={}):
...
f1(**f1_kwargs)
...
f2(**f2_kwargs)
...
f3(**f3_kwargs)
...
Предостережение:
Обратите внимание, что аргументы по dicts
умолчанию только один раз экземпляра, так что вы не должны изменять пустую dicts
в вашей функции. Если есть необходимость, вы должны вместо этого использовать None
в качестве аргумента по умолчанию и создавать новый пустой dict
каждом запуске функции:
def foo(..., isclose_kwargs=None):
if isclose_kwargs is None:
isclose_kwargs = {}
...
if math.isclose(x, y, **isclose_kwargs):
...
Я предпочитаю избегать этого, когда вы знаете, что делаете, поскольку это более кратко, и в целом я не люблю перепривязывать переменные. Тем не менее, это определенно верная идиома, и она может быть более безопасной.
Ответ 2
Правильное решение будет использовать те же значения по умолчанию, что и math.isclose()
. Нет необходимости жестко их кодировать, поскольку вы можете получить текущие значения по умолчанию с помощью функции inspect.signature()
:
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:
[T] Первоначальной мотивацией для Argument Clinic было предоставление интроспективных "подписей" для встроенных в CPython. Раньше функции запроса самоанализа генерировали исключение, если вы передавали встроенную функцию. С Аргумент Клиник, это в прошлом!
Под капотом подпись math.isclose()
фактически хранится в виде строки:
>>> math.isclose.__text_signature__
'($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)'
Это inspect
поддержкой inspect
подписи, чтобы дать вам фактические значения.
Не все C-определенные функции используют Argument Clinic, кодовая база конвертируется в каждом конкретном случае. math.isclose()
был преобразован для 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()
позволил бы вам добавить утверждение о количестве параметров, inspect.signature()
вашим кодом.
Ответ 3
На самом деле способов заставить функцию использовать аргументы по умолчанию не так много... У вас есть только два варианта:
- Передать реальные значения по умолчанию
- Не передавайте аргументы вообще
Поскольку ни один из вариантов не является отличным, я собираюсь составить исчерпывающий список, чтобы вы могли сравнить их все.
-
Используйте **kwargs
для передачи аргументов
Определите свой метод, используя **kwargs
и передайте его в math.isclose
:
def foo(..., **kwargs):
...
if math.isclose(x, y, **kwargs):
Минусы:
- имена параметров обеих функций должны совпадать (например,
foo(1, 2, rtol=3)
не будет работать)
-
Вручную построить **kwargs
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):
Минусы:
-
inspect.signature
может дать сбой встроенным функциям или другим функциям, определенным в C
Ответ 4
Делегируйте рекурсию вспомогательной функции, так что вам нужно будет создать 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))
Ответ 5
Если вы беспокоитесь о производительности:
- Распаковка
**kwargs
также повлияет на это. - Я предполагаю, что вы будете использовать этот код в производственной среде.
- В производственной среде вы редко меняете свою версию Python, поэтому наличие значений аргументов по умолчанию,
rel_tol
abs_tol
по умолчанию и abs_tol
для math.isclose
, не имеет большого значения: их значения не изменятся без изменения версии.