Избегание повторения кода в аргументах по умолчанию в Python

Рассмотрим типичную функцию с аргументами по умолчанию:

def f(accuracy=1e-3, nstep=10):
    ...

Это компактно и легко понять. Но что, если у нас есть другая функция g, которая вызовет f, и мы хотим передать некоторые аргументы от g до f? Естественный способ сделать это:

def g(accuracy=1e-3, nstep=10):
    f(accuracy, nstep)
    ...

Проблема с этим способом заключается в том, что значения по умолчанию дополнительных аргументов повторяются. Обычно при распространении аргументов по умолчанию, подобных этому, в верхней функции (g) требуется одинаковое значение по умолчанию, как в нижней функции (f), и, следовательно, в любое время, когда по умолчанию изменяется значение f, нужно пройти все функции, которые его вызывают и обновляют значения по умолчанию любого из своих аргументов, они будут распространяться на f.

Другой способ сделать это - использовать аргумент-заполнитель и заполнить его значение внутри функции:

def f(accuracy=None, nstep=None):
    if accuracy is None: accuracy = 1e-3
    if nstep is None: nstep=10
    ...
def g(accuracy=None, nstep=None):
    f(accuracy, nstep)
    ...

Теперь вызывающей функции не нужно знать, что f значения по умолчанию. Но интерфейс f теперь немного более громоздкий и менее ясный. Это типичный подход в языках без явной поддержки аргументов по умолчанию, например fortran или javascript. Но если все это делается в python, вы удаляете большую часть поддержки аргументов по умолчанию для языка.

Есть ли лучший подход, чем эти два? Каков стандартный, питонический способ сделать это?

Ответы

Ответ 1

Я думаю, что процедурная парадигма сужает ваше видение этой проблемы. Вот некоторые решения, которые я нашел, используя другие функции Python.

Объектно-ориентированное программирование

Вы вызываете f() и g() с тем же набором параметров - это хороший намек на то, что эти параметры представляют один и тот же объект. Почему бы не сделать его объектом?

class FG:
    def __init__(self, accuracy=1e-3, nstep=10):
        self.accuracy = accuracy
        self.nstep = nstep

    def f(self):
        print ('f', self.accuracy, self.nstep)

    def g(self):
        self.f()
        print ('g', self.accuracy, self.nstep)

FG().f()
FG(1e-5).g()
FG(nstep=20).g()

Функциональное программирование

Вы можете преобразовать f() в функцию более высокого порядка - например, что-то вроде этого:

from functools import partial

def g(accuracy, nstep):
    print ('g', accuracy, nstep)

def f(accuracy=1e-3, nstep=10):
    g(accuracy, nstep)
    print ('f', accuracy, nstep)

def fg(func, accuracy=1e-3, nstep=10):
    return partial(func, accuracy=accuracy, nstep=nstep)

fg(g)()
fg(f, 2e-5)()
fg(f, nstep=32)()

Но это также сложный подход - здесь были обменены вызовы f() и g(). Вероятно, есть более эффективные подходы к этому - т.е. Конвейеры с обратными вызовами, я не так хорош с FP: (

Динамичность и интроспекция

Это гораздо более сложный подход, и он требует копания в внутренности CPython, но поскольку CPython позволяет это, почему бы не использовать его?

Вот декоратор для обновления значений по умолчанию через __defaults__ member:

class use_defaults:
    def __init__(self, deflt_func):
        self.deflt_func = deflt_func

    def __call__(self, func):
        defltargs = dict(zip(getargspec(self.deflt_func).args, 
                            getargspec(self.deflt_func).defaults))

        defaults = (list(func.__defaults__) 
                    if func.__defaults__ is not None 
                    else [])

        func_args = reversed(getargspec(func).args[:-len(defaults)])

        for func_arg in func_args:
            if func_arg not in defltargs:
                # Default arguments doesn't allow gaps, ignore rest
                break
            defaults.insert(0, defltargs[func_arg])

        # Update list of default arguments
        func.__defaults__ = tuple(defaults)

        return func

def f(accuracy=1e-3, nstep=10, b = 'bbb'):
    print ('f', accuracy, nstep, b)

@use_defaults(f)
def g(first, accuracy, nstep, a = 'aaa'):
    f(accuracy, nstep)
    print ('g', first, accuracy, nstep, a)

g(True)
g(False, 2e-5)
g(True, nstep=32)

Это, однако, исключает аргументы только для ключевого слова, у которых есть отдельный __kwdefaults__, и, вероятно, взорвать логику позади use_defaults decorator.

Вы также можете добавить аргументы во время выполнения, используя оболочку, но это, вероятно, снизит производительность.

Ответ 2

Определите глобальные константы:

ACCURACY = 1e-3
NSTEP = 10

def f(accuracy=ACCURACY, nstep=NSTEP):
    ...

def g(accuracy=ACCURACY, nstep=NSTEP):
    f(accuracy, nstep)

Если f и g определены в разных модулях, вы также можете сделать модуль constants.py:

ACCURACY = 1e-3
NSTEP = 10

а затем определите f с помощью:

from constants import ACCURACY, NSTEP
def f(accuracy=ACCURACY, nstep=NSTEP):
    ...

и аналогично для g.

Ответ 3

Согласование с @unutbu:

Если вы используете структуру пакета:

mypackage
|
+- __init__.py
|
+- fmod.py
|
+- gmod.py
|
...

а затем в __init__.py поместите ваши константы в @unutbu:

ACCURACY = 1e-3
NSTEP = 10
__all__ = ['ACCURACY', 'NSTEP']

то в fmod.py

from mypackage import *
def f(accuracy=ACCURACY, nstep=NSTEP):
    ...

и gmod.py, а любые другие модули импортируют ваши константы.

from mypackage import *
def g(accuracy=ACCURACY, nstep=NSTEP):
    f(accuracy, nstep)
    ...

Или, если вы не используете пакеты, просто создайте модуль с именем myconstants.py и выполните то же самое, что и с __init__.py, за исключением того, что вместо импорта из mypackage вы должны импортировать из myconstants.

Одно из преимуществ этого стиля заключается в том, что если позже вы захотите прочитать свои константы из файла (или как аргументы функции), если он существует, вы можете поместить код в __init__.py или myconstants.py для этого.

Ответ 4

Мой любимый, kwargs!

def f(**kwargs):
    kwargs.get('accuracy', 1e-3)
    ..

def g(**kwargs):
    f(**kwargs)

Конечно, не стесняйтесь использовать константы, как описано выше.