Python中的RAII - 离开范围时自动销毁

时间:2011-02-21 20:50:23

标签: python scope raii with-statement

我一直在尝试在Python中找到RAII。 资源分配是初始化是C ++中的一种模式 对象在创建时初始化。如果失败,则抛出 一个例外。通过这种方式,程序员知道这一点 对象永远不会处于半构造状态。蟒蛇 可以做这么多。

但RAII也适用于C ++的范围规则 确保迅速销毁对象。一旦变量 弹出堆栈它被摧毁。这可能发生在Python中,但仅限于此 如果没有外部或循环引用。

更重要的是,对象的名称在函数之前仍然存在 在退出(有时更长)。模块级别的变量将 坚持模块的生命周期。

如果我这样做,我想得到一个错误:

for x in some_list:
    ...

... 100 lines later ...

for i in x:
    # Oops! Forgot to define x first, but... where's my error?
    ...

我可以在使用它后手动删除它们, 但这很难看,我需要付出努力。

我希望在这种情况下做什么 - 我意味着什么:

for x in some_list:
    surface = x.getSurface()
    new_points = []
    for x,y,z in surface.points:
        ...     # Do something with the points
        new_points.append( (x,y,z) )
    surface.points = new_points
    x.setSurface(surface)

Python做了一些作用域,但不是在缩进级别,只是在 功能水平。要求我创建一个新功能似乎很愚蠢 只是为了确定变量的范围,以便重用名称。

Python 2.5有"with" statement 但这需要我明确地加入__enter____exit__函数 并且通常似乎更倾向于清理像文件这样的资源 和mutex锁定无论退出向量。它对范围界定没有帮助。 或者我错过了什么?

我搜索过“Python RAII”和“Python范围”,但我找不到任何东西 直接和权威地解决了这个问题。 我查看了所有的PEP。这个概念似乎没有得到解决 在Python中。

我是一个坏人,因为我想在Python中使用范围变量? 这是不是Pythonic?

我不是喜欢它吗?

也许我正试图消除语言动态方面的好处。 有时希望范围得到执行是否自私?

我是否因为想要编译器/解释器而懒惰 抓住我的疏忽变量重用错误?嗯,是的,当然我很懒, 但我是不是很懒?

5 个答案:

答案 0 :(得分:28)

tl; dr RAII是不可能的,你将它与一般的范围混合起来,当你错过那些额外的范围时,你可能会编写错误的代码。

也许我没有得到你的问题,或者你没有得到关于Python的一些非常重要的东西......首先,与范围相关的确定性对象破坏是不可能的垃圾收集语言。 Python中的变量仅仅是引用。一旦指向它的指针超出范围,你就不希望malloc'的内存块为free',是吗?如果您碰巧使用引用计数,某些情况下的实际例外 - 但没有语言足够疯狂地设置确切的实现。

并且即使你有引用计数,就像在CPython中一样,它是一个实现细节。通常,在Python中包含使用引用计数的各种实现 not ,您应该编写代码,好像每个对象都会挂起,直到内存耗尽。

对于函数调用的其余部分存在的名称:可以通过del语句从当前或全局范围中删除名称。但是,这与手动内存管理无关。它只是删除了引用。可能会或可能不会触发引用的对象为GC'd,而不是练习的重点。

  • 如果您的代码足够长,导致名称冲突,则应编写较小的函数。并使用更具描述性,不太可能的冲突名称。嵌套循环覆盖out循环的迭代变量也是一样的:我还没遇到这个问题,所以也许你的名字不够描述,或者你应该将这些循环分开?

你是对的,with与范围界定无关,只是确定性清理(因此它与末端的RAII重叠,但不在手段中)。

  

也许我正试图消除语言动态方面的好处。有时希望范围得到执行是否自私?

没有。体面的词汇范围是一个独立于动态/静态的优点。不可否认,Python(2 - 3几乎已经解决了这个问题)在这方面存在缺陷,尽管它们更多地属于闭包领域。

但是要解释“为什么”:Python 必须保守其开始新范围的地方因为没有声明,否则,对名称的赋值使其成为最内层/当前范围的本地。所以例如如果for循环有自己的范围,则无法轻易修改循环外的变量。

  

我是否因为希望编译器/解释器能够捕获我的疏忽变量重用错误而懒惰?嗯,是的,当然我很懒,但我是不是很懒?

再一次,我认为名义上的重复使用(以一种引入错误或陷阱的方式)是罕见的,无论如何都很小。

编辑:尽可能清楚地说明:

  • 在使用GC的语言中不能进行基于堆栈的清理。根据定义,它不可能是:变量是对堆上对象的潜在许多引用之一,既不知道也不知道关心变量何时超出范围,并且所有内存管理都掌握在GC的手中,GC在它喜欢的时候运行,而不是在弹出堆栈帧时运行。资源清理的解决方法有所不同,见下文。
  • 通过with语句进行确定性清理。是的,它没有引入新的范围(见下文),因为这不是它的用途。它删除托管对象绑定的名称并不重要 - 尽管如此,清理仍然是“不要碰我,我无法使用”对象(例如关闭的文件流)。 / LI>
  • Python每个函数,类和模块都有一个范围。期间。这就是语言的工作方式,无论你喜不喜欢。如果您希望/“需要”更细粒度的范围,请将代码分解为更细粒度的函数。您可能希望获得更细粒度的范围,但是没有 - 并且由于本答案前面指出的原因(“编辑:”上方的三个段落),有理由这样做。喜欢与否,但这就是语言的运作方式。

答案 1 :(得分:13)

  1. 你对with是对的 - 它与变量范围完全无关。

  2. 如果您认为全局变量存在问题,请避免使用全局变量。这包括模块级变量。

  3. 在Python中隐藏状态的主要工具是类。

  4. 生成器表达式(以及Python 3中的列表推导)也有自己的范围。

  5. 如果你的函数足够长,你可能无法跟踪局部变量,你可能应该重构你的代码。

答案 2 :(得分:8)

  

但RAII也适用范围界定   C ++规则确保提示   破坏物体。

这在GC语言中被认为是不重要的,这些语言基于内存为fungible的想法。只要在其他地方有足够的内存来分配新对象,就没有迫切需要回收对象的内存。诸如文件句柄,套接字和互斥体之类的不可替换资源被认为是特殊处理的特殊情况(例如,with)。这与C ++的模型形成对比,后者将所有资源都视为相同。

  

一旦变量弹出   堆栈被摧毁。

Python没有堆栈变量。在C ++术语中,所有shared_ptr

  

Python做了一些范围,但没有   缩进级别,只是在   功能水平。这看起来很傻   要求我做一个新功能   只是为了尽可能地调整变量的范围   重用一个名字。

确定了生成器理解级别(在3.x中,所有理解中)。

如果您不想破坏for循环变量,请不要使用这么多for个循环。特别是,在循环中使用append是非Pythonic。而不是:

new_points = []
for x,y,z in surface.points:
    ...     # Do something with the points
    new_points.append( (x,y,z) )

写:

new_points = [do_something_with(x, y, z) for (x, y, z) in surface.points]

# Can be used in Python 2.4-2.7 to reduce scope of variables.
new_points = list(do_something_with(x, y, z) for (x, y, z) in surface.points)

答案 3 :(得分:2)

基本上你可能使用了错误的语言。如果你想要理智的范围规则和可靠的破坏,那么坚持使用C ++或尝试Perl。关于何时释放内存的GC辩论似乎忽略了这一点。它是关于释放其他资源,如互斥锁和文件句柄。我相信C#在引用计数变为零时调用的析构函数与它决定回收内存时的区别。人们不关心内存回收,但一旦不再引用就想知道。遗憾的是,Python作为一种语言具有真正的潜力。但它是非传统的范围和不可靠的析构函数(或者至少是依赖于实现的析构函数)意味着一个人被C ++和Perl所获得的权力所剥夺。

有趣的是,如果可以使用新内存而不是在GC中重新使用新内存,则会发表评论。这不仅仅是一种说它泄漏记忆的奇特方式: - )

答案 4 :(得分:2)

在多年的C ++之后切换到Python时,我发现依靠__del__模仿RAII类型行为很有诱惑力,例如:关闭文件或连接。但是,有些情况(例如由Rx实现的观察者模式),被观察的东西保持对您的对象的引用,使其保持活力!因此,如果您想在源被终止之前关闭连接,那么在__del__尝试执行此操作时,您将无法到达任何地方。

UI编程出现以下情况:

class MyComponent(UiComponent):

    def add_view(self, model):
        view = TheView(model) # observes model
        self.children.append(view)

    def remove_view(self, index):
        del self.children[index] # model keeps the child alive

所以,这里是获取RAII类型行为的方法:创建一个带有添加和删除钩子的容器:

import collections

class ScopedList(collections.abc.MutableSequence):

    def __init__(self, iterable=list(), add_hook=lambda i: None, del_hook=lambda i: None):
        self._items = list()
        self._add_hook = add_hook
        self._del_hook = del_hook
        self += iterable

    def __del__(self):
        del self[:]

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, item):
        self._del_hook(self._items[index])
        self._add_hook(item)
        self._items[index] = item

    def __delitem__(self, index):
        if isinstance(index, slice):
            for item in self._items[index]:
                self._del_hook(item)
        else:
            self._del_hook(self._items[index])
        del self._items[index]

    def __len__(self):
        return len(self._items)

    def __repr__(self):
        return "ScopedList({})".format(self._items)

    def insert(self, index, item):
        self._add_hook(item)
        self._items.insert(index, item)

如果UiComponent.children是一个ScopedList,它会调用孩子们的acquiredispose方法,那么您可以获得与确定资源获取和处置相同的保证在C ++中。