在函数之间传递参数的 pythonic 方式是什么?

What's the pythonic way to pass arguments between functions?

我从用户那里获取了一些参数并将其传递给函数(每个函数在不同的 class 中),直到它最终到达一个函数进行一些处理然后返回解决方案连锁,链条。在链上,函数变得越来越抽象,合并了下层函数多次运行的结果。

我应该在哪里使用 *args**kwargs

我认为 *args*kwargs 可以用于函数不显式使用参数的每个函数。但是,实际参数需要在 top_level 处定义,以便用户知道函数期望什么。

我应该在哪里定义输入的含义?

我认为它们应该在 top_level 中定义,因为这是最终用户可能希望查看其文档的内容。

我应该在哪里定义默认值?

同样,我认为它们应该在 top_level 处定义,因为这是最终用户与之交互的地方。

这是一个演示参数传递的简单示例,其中我没有展示函数如何变得越来越抽象或它们如何与不同的 classes 交互,正如我所想的那样不必要的细节。

def top_level(a=1, b=1, c=1, d=1, e=1):
    """ Compute sum of five numbers.
    :param a: int, a
    :param b: int, b
    :param c: int, c
    :param d: int, d
    :param e: int, e
    :return: int, sum
    """
    return mid_level(a, b, c, d, e)


def mid_level(*args, **kwargs):
    return bottom_level(*args, **kwargs)


def bottom_level(a, b, c, d, e):
    return a + b + c + d + e

print top_level(1, 2, 3)
8

像这样传递参数有Python约定吗?

我会争辩说,将参数向下传递到函数的几个级别本身并不是 pythonic。

来自Python的禅宗:

  • 简单胜于复杂
  • 平面优于嵌套

编辑:

如果有很多参数并且它们之间的函数只是将它们传递下去,我可能会将它们包装在 tuple 中并在最低级别展开它们。

这不是最常见的模式,但我在具有嵌套命令级别的命令行程序中看到过它:子命令、子命令等。在该模型中,"upper" 级函数可能或多或少是调度程序,并且没有关于给定路由中的子函数需要哪些参数的信息。这个模型最纯粹的场景是当子命令是插件并且 "upper" 层实际上没有关于子功能的信息,除了插件应该遵守的调用约定。

在这些情况下,我认为 pythonic 方法是将参数从较高级别的函数传递到较低级别的函数,并让 worker 级别决定哪些有用。可能参数的范围将在调用约定中定义。这是基于 DRY 的 pythonic——不要重复你自己。如果低级/工作函数定义了哪些输入是必需的或可选的,那么在更高级别不重复此信息通常是有意义的。

任何控制反转流程设计都可以这样说,而不仅仅是带有插件的 CLI 应用程序。在许多应用程序设计中我不会使用这种方法,但它在这里有效。

必须将输入的含义设置在它可能出现的最高级别 -- 作为较低级别的接口规范(约定,而非编程)。否则输入将没有语义。

如果一个输入可以被多个子函数使用,即控制流中有链接或管道的概念,那么输入的默认值也需要在输入的最顶层定义。

我不会回答你的问题,因为这就像回答问题 "what's the best way to use a screwdriver to tighten a nut?"。 IE。我不相信您寻求指导的工具(*args**kwargs)旨在解决您想要解决的问题。

相反,我会回答这个问题:"how do I associate some data with a set of functions?",答案显然是 使用 类

欢迎使用面向对象编程。我想你会喜欢的!

这是我的意思的一个非常基本的例子,但是很难确切地知道你想要从你的例子中得到什么,因为它很简单,但基本原则是将你的数据封装在 class,然后使用 class 的方法对其进行操作。

  • 然后您可以在 class 中的方法之间调用,而无需一直传递大量参数(例如下面的 .calculate() 方法),您不知道顶层将需要或底层。
  • 您可以只在一处记录参数,即 __init__ 方法。
  • 您可以通过对代码透明的 subclassing 进行自定义(因为如果您重写 subclass 中的方法,它仍然可以被更通用的 superclass),就像我为下面的 .reduce(x, y) 方法所做的那样。

示例:

class ReductionCalculator:

    def __init__(self, *args):
        self.args = args

    def calculate(self):

        start = self.args[0]
        for arg in self.args[1:]:
            start = self.reduce(start, arg)

        return start


class Summer(ReductionCalculator):

    def reduce(self, x, y):
        return x + y


class Multiplier(ReductionCalculator):

    def reduce(self, x, y):
        return x * y

summer = Summer(1, 2, 4)
print('sum: %d' % (summer.calculate(),))
multiplier = Multiplier(1, 2, 4)
print('sum: %d' % (multiplier.calculate(),))

这个方法怎么样:创建一个 class,称之为 AllInputs,代表所有 "arguments taken from the user." 的集合这个 class 的唯一目的是作为一组值的容器。当然,这个 class 的一个实例在程序的顶层被初始化。

class AllInputs:
   def __init__(self,a=1, b=1, c=1, d=1, e=1):
    """ Compute sum of five numbers.
    :param a: int, a
    :param b: int, b
    :param c: int, c
    :param d: int, d
    :param e: int, e
    """
    self.a = a
    self.b = b
    self.c = c
    self.d = d
    self.e = e

这个对象,称之为 all_inputs,现在作为单个参数传递给示例中的所有函数。如果函数不使用对象中的任何字段,那很好;它只是将它传递给完成实际工作的较低级别的功能。要重构您的示例,您现在将拥有:

def top_level(all_inputs):
    """ Compute sum of all inputs
    :return: int, sum
    """
    return mid_level(all_inputs)


def mid_level(all_inputs):
    return bottom_level(all_inputs)


def bottom_level(all_inputs):
    return (all_inputs.a + all_inputs.b + all_inputs.c + 
      all_inputs.d + all_inputs.e)

all_inputs = AllInputs(1, 2, 3)
print top_level(all_inputs)
8

我不知道这是 "Pythonic" 还是 "non-Pythonic",我不在乎。我认为将程序将使用的数据组合在一起是一个很好的编程思想。将默认值与从用户那里获取的其他值组合在一起的初始化过程集中在一个易于理解的地方。它是合理的自我记录。你说函数调用分布在几个 classes 上,这没问题。函数调用清晰,程序流程易于理解。通过将一些计算放在 AllInputs 中可以进行优化,这样您就可以避免重复代码。

我不喜欢你的例子(我想你也不喜欢它,或者你可能一开始就不会问这个问题)是它如何使用 *args 句法。当我看到该语法时,我将其视为所有参数都具有相同语义的暗示,就像在标准库函数 os.path.join 中一样。在您的应用程序中,如果我理解这个问题,低级函数要求参数列表按特定顺序排列并具有特定含义(您的示例没有反映出这一点,但文本表明了这一点)。看到作为 *args 传递给函数的参数,然后在较低级别,它们的特定名称和含义再次出现,这让人感到困惑。将它们分组到一个对象中可以清楚地了解正在发生的事情。