Python中的习语:闭包 vs 函数对象 vs 对象

14

我对更有经验的Python程序员在以下样式问题上的看法很好奇。假设我正在构建一个函数,该函数将逐行迭代遍历pandas数据帧或任何类似的用例,其中函数需要访问其先前的状态。在Python中似乎有至少四种实现方式:

  1. 闭包:
def outer():
    previous_state = None
    def inner(current_state) :
        nonlocal previous_state
        #do something
        previous_state=current_state
        return something

如果您来自JavaScript背景,这对您来说无疑是很自然的。在Python中也感觉非常自然,直到您需要访问封闭作用域时,您将会做类似以下操作:inner.__code__.co_freevars,它将以元组的形式给出封闭变量的名称,并找到您想要的一个的索引,接着转到inner.__closure__[index].cell_contents以获取其值。并不是特别优雅,但我想重点通常是隐藏范围,因此难以访问是有道理的。另一方面,当Python放弃了与OOP语言相比几乎所有其他创建私有变量的方法时,它也感觉有点奇怪,却使封闭函数变成了私有。

  1. 函子
def outer():
    def inner(current_state):
        #do something
        inner.previous_state=current_state
        return something
    ret = inner
    ret.previous_state=None
    return ret

这样做“打开了闭包”,因为现在封闭状态完全可见,作为函数的属性。这起作用是因为函数只是伪装成对象。我倾向于它是最Pythonic的方式。它清晰、简洁、易读。

  1. 对象
    这对面向对象编程程序员来说可能最为熟悉。
class Calculator(Object) :
    def __init__(self):
        self.previous_state=None

    def do_something(self, current_state) :
        #do_something
        self.previous_state = current_state
        return something

这里最大的缺点是你往往会得到很多类定义。在完全面向对象的语言比如 Java 中这没问题,因为你可以使用接口等方式来管理它,但在一个鸭子类型的语言中,为了携带一个需要一些状态的函数而写许多简单的类似乎有点奇怪。

  1. 全局变量 - 我不会演示这个,因为我特意想避免污染全局命名空间。

  2. 装饰器 - 这有点出人意料,但你可以使用装饰器来存储部分状态信息。

@outer
def inner(previous_state, current_state):
    #do something
    return something

def outer(inner) :
    def wrapper(current_state) :
        result =  inner(wrapper.previous_state, current_state)
        wrapper.previous_state = current_state
        return result
    ret = wrapper
    ret.previous_state=None
    return result

这种语法对我来说是最不熟悉的,但如果现在我调用

func = inner

实际上,我理解

func = outer(inner)

然后,反复调用func()就像函数对象的例子一样。其实我非常讨厌这种方式。在我看来,它的语法非常不透明,因为无法确定多次调用inner(current_state)是否会给你相同的结果,或者它是否每次都会给你一个新的装饰函数,因此使用这种方式为函数添加状态的装饰器似乎是不好的做法。

那么正确的方式是什么?我漏掉了哪些优缺点?


1
我不太明白为什么你认为需要通过 inner.__closure__ 访问闭包变量;因为只有当你已经在内部函数中主动使用它们时,这些名称才会出现在该结构中。__closure__ 结构实际上是一个内部实现细节。 - Martijn Pieters
我会让人们开发这个,但是根据经验,Python 开发人员往往会在这种场景下使用类。或者在不需要完全清晰度和喜欢具有 @ 漂亮语法的情况下使用装饰器。您不希望使用 nonlocalglobal 关键字。而 functor 通常被留给 closure。 - Cyrbil
封闭名称在这方面与本地变量没有区别;通常情况下,您不会进入函数来读取它们的本地变量,为什么要对闭包做同样的事情?如果需要将该状态公开供函数外部使用,则使用函数对象或类方法。 - Martijn Pieters
1
Python也是完全面向对象的语言。Java真正是以类为导向的,比面向对象更多,尽管Java8加入了更好的支持表示方法的对象。 一个像Java一样的完全面向对象的编程语言。 - dsh
1
@MartijnPieters,想要访问封闭变量似乎并不过分。您可能希望在Java/C++中使用反射访问私有变量的任何地方都可以访问它们。例如:测试、调试或序列化。 - phil_20686
2个回答

8

因此,正确的答案是可调用对象,其实质上取代了Python中闭包的惯用语。

所以,基于上述第3个选项进行修改:

class Calculator(Object) :
    def __init__(self):
        self.previous_state=None

    def do_something(self, current_state) :
        #do_something
        self.previous_state = current_state
        return something

to

class Calculator(Object) :
    def __init__(self):
        self.previous_state=None

    def __call__(self, current_state) :
        #do_something
        self.previous_state = current_state
        return something

现在你可以像调用函数一样调用它。

func = Calculator():
for x in list:
    func(x)

3
您可以定义一个生成器,它是协程的一种受限形式。
def make_gen():
    previous_state = None
    for row in rows:
        # do something
        previous_state = current_state
        yield something

thing = make_gen()
for item in thing:
    # Each iteration, item is a different value
    # "returned" by the yield statement in the generator

不要重复调用 thing(替换你的内部函数),而是迭代它(基本上相当于不断调用 next(thing))。

状态完全包含在生成器的主体中。

如果你不想真正地迭代它,仍然可以通过显式调用 next 来有选择地“重新进入”协程。

thing = make_gen()
first_item = next(thing)
# do some stuff
second_item = next(thing)
# do more stuff
third_item = next(thing)
fourth_item = next(thing)
# etc

从问题中并不清楚这将如何在OP的代码中使用。我怀疑数据框中的行将作为参数传递给make_gen(目前,我没有参数,但循环中的自由变量“rows”旨在来自数据框)。 - chepner

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