在 Python 中避免默认参数中的代码重复

Avoiding code repetition in default arguments in 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 中以这种方式做所有事情,就会丢弃该语言的大部分默认参数支持。

还有比这两个更好的方法吗?这样做的标准 pythonic 方法是什么?

定义全局常量:

ACCURACY = 1e-3
NSTEP = 10

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

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

如果 fg 定义在不同的模块中,那么您也可以创建一个 constants.py 模块:

ACCURACY = 1e-3
NSTEP = 10

然后定义 f 为:

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

g 也是如此。

我的最爱,kwargs!

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

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

当然,您可以随意使用上述常量。

与@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__.pymyconstants.py 这样做。

我认为程序范式缩小了您对该问题的视野。以下是我使用其他 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__ 成员更新默认值:

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 装饰器背后的逻辑。

您也可以使用包装器在运行时添加参数,但这可能会降低性能。