【问题标题】:Avoiding code repetition in default arguments in Python避免 Python 中默认参数中的代码重复
【发布时间】:2015-06-16 11:10:48
【问题描述】:

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

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

【问题讨论】:

  • 这个问题我已经问过自己很多次了。但这可能是[程序员[(programmers.stackexchange.com)的问题?我以为有一个 comp-sci 堆栈交换,但我现在找不到它。我迫不及待地想看看你会得到什么样的答案。
  • @MarkMikofski 您正在考虑 cs.stackexchange.com 虽然该网站更多地处理需要时髦希腊字母的问题,并且喜欢谈论 Big O。不过,这个问题,如果最好在 Stack Overflow 或 Programmers.SE 上询问。因为它在这里并且似乎没有收集主题选票,所以这里可能还可以。请注意,如果要迁移到 P.SE,答案将与该网站所需的答案样式不匹配。
  • @MarkMikofski 另一方面,如果问题主要是关于 Stack Overflow 的意见或过于宽泛的问题,那么它在 Programmers.SE 上可能同样适用。为此,您不妨阅读What goes on Programmers.SE? A guide for Stack Overflow,它试图帮助 Stack Overflow 用户了解 Programmers.SE 的范围并避免糟糕的迁移建议。
  • 感谢 @MichaelT 让其他人看到 Choosing between Stack Overflow and Programmers Stack Exchange,我特别喜欢 > “经验法则:如果您坐在 IDE 前,请在 Stack Overflow 上询问。如果您是站着的在白板前,向程序员提问”

标签: python default argument-passing


【解决方案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__成员更新默认值的装饰器:

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

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

【讨论】:

  • 您的第三个建议非常有趣。我没有考虑过这样的事情是可能的。将装饰器应用于函数是简单且具有描述性的。不错!
  • 我不明白您的第二个建议添加了什么。它与简单的def f(a,b): pass; def g(a=1,b=2): f(a,b) 有何不同。如果您只在顶部指定默认值,则很容易只指定一次。问题是我希望 f 作为一个独立的函数工作,它本身具有合理的默认值。 f 的典型用例不会被 g 调用。这只是它的一种可能用途。
  • 我认为您的第一个解决方案会起作用,但我不太喜欢它。我认为它把fg 结合得太紧了。 g 的用户基本上会传入一个“f 参数”对象,这会将 f 用于实现 g 的实现细节泄露给用户。
  • @amaurea,感谢您接受我的回答!我更改了函数式编程示例,所以现在 f()g() 之间的紧密依赖关系被打破了。另外,我的回答是概念性的,应该选择的路径取决于fg 的性质。
【解决方案2】:

与@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 来执行此操作。

【讨论】:

    【解决方案3】:

    定义全局常量:

    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 选项。
    • 如果fg 定义在不同的模块中(如果你有另一个函数h 调用g),那么这看起来像def g(accuracy=fmod.ACCURACY, nstep=fmod.NSTEPh(accyracy=gmod.fmod.ACCURACY, nstep=gmod.fmod.NSTEP) ,不是吗?有没有一种方便的方法可以在各个模块中传播这些默认值?
    • @amaurea,在所有其他模块中使用from fmod import ACCURACY, NSTEP 导入来自fmod.py 的常量,那么您不必在具有完整命名空间的函数协议中使用常量,您可以使用@ unutbu 的回答,def g(accuracy=ACCURACY, nstep=NSTEP)h(accuracy=ACCURACY, nstep=NSTEP)
    • 通常如果我有很多这些常量,并且它们适用于包中的每个模块,那么我会将它们偷偷放入__init__.py 或名为constants.py 的模块中。然后你也可以设置__all__ = ['ACCURACY', 'NSTEP']之类的东西,然后在每个模块中使用from mypackage import *来导入每个子模块中的所有常量。
    • @unutbu,我没有阅读您的更新,不确定我的答案是否与您的大不相同,我们可以合并它们,我可以删除我的。
    【解决方案4】:

    我最喜欢的,kwargs!

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

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

    【讨论】:

    • 此方法类似于None 方法,因为实际默认值在f 的主体内定义。但是kwargs g 不能重命名参数,所以如果g 同时调用f1f2 并且这些函数有冲突的参数名称,你就有问题了。 kwargs 方法还使得函数实际采用哪些参数变得模糊不清。所以我不确定我是否更喜欢这种方法而不是None
    • 您可以在g 中添加pop 并添加到kwargs。 kwargs 的重点是,当在整个代码中多次调用函数时,您可以在 两个 位置进行更改(即,您有 a、b、c、...z 都调用 f)而不是比他们所有人。如果您也选择的话,您仍然可以将 f 的签名显式定义为 f(accuracy=None...)
    • 如果构建了多个使用kwargs 相互调用的函数,那么在一个函数中修改kwargs 是危险的,因为它可能会重命名一个用于兄弟函数的选项。所以在修改之前必须复制kwargs。尽管如此,kwargs 方法的好处是,g 现在可以不知道f 的参数的名称和数量,而不仅仅是它们的默认值。如果向f 添加一个新参数,那么g 会自动支持它(除非发生冲突)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多