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

25

考虑一个具有默认参数的典型函数:

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的做法吗?


我已经问过自己这个问题很多次了。但这可能是一个针对程序员的问题?我记得有一个计算机科学的交流平台,但现在找不到了。我迫不及待地想看看你们会得到什么样的答案。 - Mark Mikofski
3
你在想的是http://cs.stackexchange.com/,但那个网站更多地涉及需要希腊字母和类似于Big O之类的问题。这个问题,我不确定它是否最好在Stack Overflow或Programmers.SE上提问。既然它在这里,并且似乎没有收集到离题投票,那么可能可以在这里。请注意,如果它要迁移到P.SE,答案将不符合该网站所需的答案风格。 - user289086
3
另一方面,如果问题在Stack Overflow上主要是基于观点或者过于宽泛的话,那么在Programmers.SE上也可能表现得差不多。为了达到这个目的,你可能希望阅读《什么属于 Programmers.SE?Stack Overflow 用户指南》(http://meta.programmers.stackexchange.com/q/7182/40980),该指南试图帮助 Stack Overflow 用户了解 Programmers.SE 的范围,并避免低质量的迁移建议。 - user289086
感谢@MichaelT,对于其他人,请参阅在Stack Overflow和Programmers Stack Exchange之间选择,我特别喜欢这个“经验法则:如果您坐在IDE前,请在Stack Overflow上提问。如果您站在白板前,请在Programmers上提问”。 - Mark Mikofski
4个回答

10

定义全局常量:

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 也是一样的。


我正要把这个作为评论添加。是的,这是我经常使用的技术,它有许多优点,比如只需要更新一次默认值,但它也有一个(可能好或坏的)外观,并且肯定意味着更多的按键操作,尽管如果你所有的“CONSTANTS”都只有4个字母或更短,则不会超过OP最初建议的“None”选项。 - Mark Mikofski
1
如果fg被定义在不同的模块中(如果您有另一个函数h调用g),那么这将看起来像是def g(accuracy=fmod.ACCURACY, nstep=fmod.NSTEP,而且h(accyracy=gmod.fmod.ACCURACY, nstep=gmod.fmod.NSTEP),对吧?是否有一种方便的方法来传播这些默认值到各个模块中? - amaurea
@amaurea,在所有其他模块中使用from fmod import ACCURACY, NSTEP导入fmod.py中的常量,然后您就不必在函数协议中使用完整的命名空间来使用常量,您可以直接使用@unutbu的答案,def g(accuracy=ACCURACY, nstep=NSTEP)h(accuracy=ACCURACY, nstep=NSTEP) - Mark Mikofski
通常,如果我有很多这些常量,并且它们适用于包中的每个模块,那么我会将它们放在__init__.py或名为constants.py的模块中。然后,您还可以设置类似于__all__ = ['ACCURACY', 'NSTEP']的内容,然后在每个模块中只需使用from mypackage import *导入所有常量即可。 - Mark Mikofski
@unutbu,我没有看到你的更新,不确定我的答案是否与你的实质性不同,我们可以将它们合并,然后我可以删除我的答案。 - Mark Mikofski
@MarkMikofski:我认为你的答案很好,足够不同,可以作为一个独立的答案存在;让人们看到解决问题的不同方式是很好的。 - unutbu

5
我认为过程化编程范式会限制你对问题的视角。以下是我使用其他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装饰器背后的逻辑。

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


1
你的第三个建议非常有趣。我从未考虑过可能会有这样的事情。将装饰器应用于函数是简单而描述性的。不错! - amaurea
我不理解你的第二个建议有什么不同。它与简单地def f(a,b): pass; def g(a=1,b=2): f(a,b)有何区别?如果只在顶部指定默认值,则很容易只指定一次。问题是,我希望f本身作为一个独立的函数具有合理的默认值。f的典型用例不是由g调用。那只是其中一种可能的用法。 - amaurea
1
我认为你的第一个解决方案可以工作,但我不太喜欢它。我认为它将 fg 耦合得太紧密了。使用 g 的用户基本上会传递一个“f-parameters”对象,这会泄漏实现细节,即 f 用于实现 g 给用户。 - amaurea
@amaurea,感谢您接受我的答案!我改变了函数式编程的示例,因此现在f()g()之间的紧密依赖已被打破。此外,我的答案是概念性的,选择应该选择什么样的路径取决于fg的性质。 - myaut

3

我最喜欢的是kwargs参数!

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

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

当然,可以像上面描述的那样自由使用常量。

1
这种方法与 None 方法类似,实际默认值在 f 函数体内定义。但是使用 kwargs 时,g 无法重命名参数。因此,如果 g 调用了 f1f2,且这些函数具有冲突的参数名称,则会产生问题。 kwargs 方法还使得函数实际接收哪些参数变得难以理解。所以,我不确定是否总体上喜欢这种方法胜过 None 方法。 - amaurea
你可以在gpop和添加到kwargs。kwargs的目的是当在代码中多次调用函数(即有a,b,c,...z都调用f)时,在两个地方进行更改,而不是所有地方。如果选择,仍然可以明确定义f的签名为f(accuracy = None ...) - C.B.
如果一个人构建多层函数相互调用,使用kwargs,那么在一个函数中修改kwargs是很危险的,因为它可能会重命名一个本来是为兄弟函数准备的选项。所以在修改之前必须先复制kwargs。不过,kwargs方法的好处是,即使对于f的参数名称和数量,g现在也可以是不可知的,而不仅仅是它们的默认值。如果向f添加一个新参数,则g自动支持它(除非有冲突)。 - amaurea

3
与@unutbu紧密结合:
如果您正在使用包结构:
mypackage
|
+- __init__.py
|
+- fmod.py
|
+- gmod.py
|
...

然后按照 @unutbu 建议,在 __init__.py 中放置您的常量:

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相同的操作,不同之处在于,您将从myconstants导入而不是从mypackage导入。
这种风格的一个优点是,如果以后您想要从文件中读取常量(或作为函数参数),假设该文件存在,您可以在__init__.pymyconstants.py中编写代码来实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接