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

时间:2015-04-10 16:31:40

标签: python default argument-passing

考虑使用默认参数的典型函数:

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方式是什么?

4 个答案:

答案 0 :(得分:9)

定义全局常量:

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

答案 1 :(得分:4)

我认为程序范式缩小了你对这个问题的看法。以下是我使用其他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装饰器后面的逻辑。

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

答案 2 :(得分: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导入1}}。

这种风格的一个优点是,如果以后你想从文件中读取常量(或作为函数的参数),假设它存在,你可以将代码放在__init__.pymyconstants.py中那样做。

答案 3 :(得分:2)

我最喜欢的,kwargs!

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

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

当然,请随意使用上述常量。