Как указать функции использовать значения аргументов по умолчанию?

У меня есть функция 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

На самом деле способов заставить функцию использовать аргументы по умолчанию не так много... У вас есть только два варианта:

  1. Передать реальные значения по умолчанию
  2. Не передавайте аргументы вообще

Поскольку ни один из вариантов не является отличным, я собираюсь составить исчерпывающий список, чтобы вы могли сравнить их все.

  • Используйте **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

Если вы беспокоитесь о производительности:

  1. Распаковка **kwargs также повлияет на это.
  2. Я предполагаю, что вы будете использовать этот код в производственной среде.
  3. В производственной среде вы редко меняете свою версию Python, поэтому наличие значений аргументов по умолчанию, rel_tol abs_tol по умолчанию и abs_tol для math.isclose, не имеет большого значения: их значения не изменятся без изменения версии.