函数闭包与可调用类

时间:2012-01-23 03:04:38

标签: python performance python-3.x closures

在许多情况下,有两种实现选择:闭包和可调用类。例如,

class F:
  def __init__(self, op):
    self.op = op
  def __call__(self, arg1, arg2):
    if (self.op == 'mult'):
      return arg1 * arg2
    if (self.op == 'add'):
      return arg1 + arg2
    raise InvalidOp(op)

f = F('add')

def F(op):
  if op == 'or':
    def f_(arg1, arg2):
      return arg1 | arg2
    return f_
  if op == 'and':
    def g_(arg1, arg2):
      return arg1 & arg2
    return g_
  raise InvalidOp(op)

f = F('add')

在任何一个方向上做出选择时应该考虑哪些因素?

我能想到两个:

  • 似乎关闭总会有更好的表现(不能 想一个反例)。

  • 我认为有些情况下关闭无法完成工作(例如,如果 其状态随时间而变化。)

我在这些方面是否正确?还有什么可以添加?

5 个答案:

答案 0 :(得分:10)

闭包更快。类更灵活(即可用的方法多于__call __)。

答案 1 :(得分:3)

我意识到这是一个较旧的帖子,但我没有看到列出的一个因素是在Python(pre-nonlocal)中你不能修改引用环境中包含的局部变量。 (在你的例子中,这样的修改并不重要,但从技术上讲,缺乏能够修改这样的变量意味着它不是真正的闭包。)

例如,以下代码不起作用:

def counter():
    i = 0
    def f():
        i += 1
        return i
    return f

c = counter()
c()

上面对 c 的调用将引发UnboundLocalError异常。

使用mutable(如字典)很容易解决这个问题:

def counter():
    d = {'i': 0}
    def f():
        d['i'] += 1
        return d['i']
    return f

c = counter()
c()     # 1
c()     # 2

但当然这只是一种解决方法。

答案 2 :(得分:1)

我认为课程方法一目了然更容易理解,因此更易于维护。由于这是优秀Python代码的前提之一,我认为所有事情都是平等的,最好使用类而不是嵌套函数。这是Python的灵活性使得语言违反“应该有一个,最好只有一个,明显的做某种方式”的方法的情况之一。用于Python编码的谓词。

任何一方的性能差异都应该可以忽略不计 - 如果你的代码中性能很重要,你当然应该对其进行分析并优化相关部分,可能会将一些代码重写为本机代码。

但是,是的,如果使用状态变量存在紧密循环,那么评估闭包变量应该比评估类属性要快一些。当然,这可以通过在进入循环之前在类方法中插入类似op = self.op的行来克服,使循环内的变量访问变为局部变量 - 这将避免属性外观 - 为每次访问提取和获取。同样,性能差异应该可以忽略不计,如果您需要这么多额外的性能并且使用Python进行编码,那么您会遇到更严重的问题。

答案 3 :(得分:1)

请注意,由于我之前在测试代码中发现的错误,我的原始答案不正确。修订后的版本如下。

我做了一个小程序来测量运行时间和内存消耗。我创建了以下可调用类和闭包:

class CallMe:
    def __init__(self, context):
        self.context = context

    def __call__(self, *args, **kwargs):
        return self.context(*args, **kwargs)

def call_me(func):
    return lambda *args, **kwargs: func(*args, **kwargs)

我定时调用接受不同数量参数的简单函数(math.sqrt()包含1个参数,math.pow()包含2个,max()包含12个参数。

我在Linux x64上使用了CPython 2.7.10和3.4.3+。我只能在Python 2上进行内存分析。我使用的源代码可用here

我的结论是:

  • 闭包运行速度比等效的可调用类快:在Python 2上快3倍,但在Python 3上只快1.5倍。缩小是因为闭包变得更慢而且可调用的类更慢。
  • 闭包占用的内存少于等效的可调用类:大约是内存的2/3(仅在Python 2上测试过)。
  • 虽然不是原始问题的一部分,但值得注意的是,通过闭包调用的运行时开销与调用math.pow()大致相同,而通过可调用类它大概翻了两倍。

这些是非常粗略的估计,它们可能因硬件,操作系统以及您进行比较的功能而有所不同。但是,它让您了解使用每种可调用的影响。

因此,这支持(与之前我所写的相反),@ RaymondHettinger给出的接受的答案是正确的,并且对于间接呼叫,闭包应该是首选,至少只要它没有'妨碍可读性。另外,感谢@AXO指出原始代码中的错误。

答案 4 :(得分:-1)

我会用以下内容重写class示例:

class F(object):
    __slots__ = ('__call__')
    def __init__(self, op):
        if op == 'mult':
            self.__call__ = lambda a, b: a * b
        elif op == 'add':
            self.__call__ = lambda a, b: a + b
        else:
            raise InvalidOp(op)

使用Python 3.2.2在我的机器上给出了0.40 usec / pass(函数0.31,因此慢了29%)。不使用object作为基类,它给出0.65 usec / pass(比基于object慢55%)。由于某些原因,在op中检查__call__的代码提供的结果几乎与在__init__中完成的结果相同。以object为基础,在__call__内检查得到0.61 usec / pass。

您使用类的原因可能是多态性。

class UserFunctions(object):
    __slots__ = ('__call__')
    def __init__(self, name):
        f = getattr(self, '_func_' + name, None)
        if f is None: raise InvalidOp(name)
        else: self.__call__ = f

class MyOps(UserFunctions):
    @classmethod
    def _func_mult(cls, a, b): return a * b
    @classmethod
    def _func_add(cls, a, b): return a + b