同一函数中多次调用时,最Pythonic的数据重用方式是什么?

7

通常我不会问这样的问题,但是Python似乎在惯用语方面有着非同寻常的社区共识,并且通过使它们更加高效来鼓励它们(例如列表推导式与map、filter相比)。

当我编码时,我发现自己经常使用以下JavaScript模式:

var f = (function() {
  var closedOver = "whatever"
  return function(param) {
    // re-uses closure variable again and again with different param
  }
})();

或者C:
int foo(int x)
{
  /* 
    compile-time constant, will not be recalced for every call,
    name 'someConst' not visible in other scopes 
   */
  const int someConst = 134;
  /* do stuff */
  return whatever;
}

一些可能的将其翻译成Python的方法:

globalConstant = someConstant
def foo(param):
    # does stuff with param and constant
    return whatever

或者可能是:
from functools import partial
def foo(invariant, variant):
    """Actually bar"""
    # does stuff
    return whatever

bar = partial(foo, someInvariant)

或者:

class Foo(object):
    """I'm just here to hold a non-visible binding. Actually bar"""
    def __init__(self, invariant):
        super(Foo, self).__init__()
        self.value = invariant

    def __call__(self, param):
        return actualFnResultWithSelfValue

bar = Foo(invariant)

或者:

def foo(variant, invariant=someConstantValue):
  return whatever
    

很不幸,现在我可能需要为初始函数定义使用临时名称,因为我只使用部分应用的版本,编写很多样板类(这些也有临时名称),或者在仅使用一个函数时污染模块命名空间与全局常量,或限制了我的函数参数,并确保某人可以通过以错误的参数调用它来破坏它。
我也可以通过在每次调用时重新实例化并希望它会被优化掉来“解决”这个问题,但由于我没有使用pypy,我对这方面不太乐观。
所以我的问题有两个方面:首先,是否有一种方法可以避免权衡?其次,如果没有,上述哪一种是最“pythonic”的(惯用语,性能,合理等)?

在标准库中寻找一些例子。特别是grep '^del ' Lib/,以及对__all__的赋值。 - Josh Lee
1
你似乎过于关注一次性名称——是有某种短缺吗?同时,对“污染”模块命名空间的担忧也似乎是不必要的——除非人们使用*进行导入,而这是不应该做的,如果需要,你可以通过__all__来控制。无论如何,一种方法是将一次性项放在一个单独的模块中,throwaways。然后只需导入该模块:您进行导入的顶级命名空间将仅感染一个新名称。 - FMc
@FMc 把所有的杂乱代码放在一个单独的模块中以进行抽象化的想法可能是我听过的最好的想法,而且我不知道使用 __all__ 来控制导出直到 Josh Lee 在上面的评论中提到。至于一次性名称的问题,我的担忧主要与可读性有关,对我来说,用一个与调用它的名称不同的名称定义函数似乎很突兀。 - Jared Smith
1
另外,您可以覆盖函数名称,这是一种非常常见的做法,例如def foo(a, x): return x ; foo = partial(foo, 'const'),在装饰函数时很常见。 - Fruch
5个回答

4
Jared,我完全理解你的犹豫,因为这个问题可能会有很多不同的意见和引发争论。但是,我同意你的观察:Python社区确实倾向于在许多实现问题上保持一致性。这是Python的优势之一。
这是我的经验法则:当有疑问时,尽可能使用Python标准库。你对functools.partial的直觉是正确的,原因如下:
  1. Python标准库是高度优化的C代码,比你想出来的任何Python类或函数闭包结构都要快。
  2. 其他Python程序员广泛使用Python标准库,因此当你使用它时,其他Python程序员将更广泛地理解你的编码意图。(“代码被阅读的次数比被编写的次数多。”)
  3. Python标准库由核心Python开发人员编写;没有哪段代码能够声称比标准库更符合“Pythonic”的标准了。
希望这可以帮到你!

1
我建议一些通常是代码异味的东西——默认可变参数。
简单的例子:
def f(x, cache={'x': 0}):
    cache['x'] += x;
    return cache['x']


assert f(1) == 1
assert f(1) == 2  
assert f(1) == 3
assert f(3) == 6

你的字典(或列表,或任何可变对象)与函数对象绑定。在后续调用中,如果省略了缓存关键字参数,则会引用同一对象。此对象状态将跨调用保持不变。

我更喜欢这个答案,即使它是一个“Python反模式”。但如果调用者传入额外的参数仍然会出错。我猜我可以对__defaults__进行引用相等性检查。 - Jared Smith
@JaredSmith,这就是我说“通常”的原因。这是一种明确定义的语言行为,99.8%的可变默认参数使用是新手错误,只剩下0.2%的情况像这样。请注意,如果您想为函数编写测试,传递外部缓存对象可能是唯一的方法(如果您使用某种并行测试运行器)。 - Łukasz Rogalski

0

我不喜欢在外部作用域中使用var的想法,可能会建议使用闭包,我认为这是更好的方式,正如你所看到的,它更像JavaScript,因此您可以将函数视为对象进行操作:

def foo():
    const = 1
    def bar(param):
      return const + param
    return bar

a = foo()
print(a(5))

除了我仍然使用一次性名称(实际上在这种情况下有两个,barfoo,因为a现在是“真正”的函数名称)之外,这与我使用functools.partial的示例没有本质区别。 - Jared Smith
我认为是这样,但由于我的 JavaScript 背景,我更喜欢当前的方式 :) - user4547691
JavaScript确实使这种特定的习惯用法更加方便,因为它支持函数表达式,而在Python中它们只能是语句。 - Jared Smith
我同意,但无论如何,我认为这是更好的版本,然后在外部作用域中使用变量,我认为你可以将其与functools.partial进行比较,并使用你认为最好的方法。 - user4547691

0

你没有提到一个替代方案,这是我认为最符合Python风格的方式,它不会创建一个丢弃的名称 :)

def foo(variant):
    try:
        return foo.invariant + variant
    except AttributeError:
        foo.invariant = 1
        return foo(variant)

虽然,坚持使用标准库是最好的选择。


是的,我认为异常应该只在特殊情况下使用(而不是完全预料到的情况下使用)。 - Jared Smith
似乎你无法避免权衡取舍 :p 但正如上面提到的,坚持使用标准库是最好的选择。 - Adriano Martins

0

我认为你的前两个例子并不相等。第一个似乎是一个闭包,而第二个使用了一个全局常量。无论如何,对于第一个例子,在Python中的直接等价物应该是

def f(x):
   def g(y):
      return x + y
   return g

并像这样使用它

add2 = f(2)
print(add2(3)) # == 5

实际上,您使用 partial 实现的功能,在幕后执行了类似的操作。


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