37

我一直在尝试在 Python中找到RAII 。资源分配是初始化是 C++ 中的一种模式,对象在创建时就被初始化。如果失败,则抛出异常。这样,程序员就知道对象永远不会处于半构建状态。Python 可以做到这一点。

但 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”语句 ,但这需要我明确地放入__enter____exit__函数,并且通常似乎更倾向于清理文件和互斥锁等资源,而不管退出向量如何。它对范围界定没有帮助。还是我错过了什么?

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

我是一个坏人,因为我想在 Python 中使用范围变量吗?那是不是太不像 Pythonic 了?

我不是在摸索吗?

也许我正试图消除语言动态方面的好处。有时想要强制执行范围是自私的吗?

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

4

5 回答 5

39

tl; dr RAII 是不可能的,你通常将它与范围界定混为一谈,当你错过那些额外的范围时,你可能正在编写糟糕的代码。

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

即使你有引用计数,就像在 CPython 中一样,它也是一个实现细节。通常,包括在具有使用引用计数的各种实现的 Python 中,您应该像每个对象都挂起一样进行编码,直到内存耗尽。

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

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

您是with对的,与范围界定无关,仅与确定性清理有关(因此它最终与 RAII 重叠,但与手段无关)。

也许我正试图消除语言动态方面的好处。有时想要强制执行范围是自私的吗?

不。体面的词法作用域是独立于动态/静态的优点。诚然,Python(2 - 3 几乎解决了这个问题)在这方面存在弱点,尽管它们更多地属于闭包领域。

但是要解释“为什么”:Python必须对它开始新范围的位置保持保守,因为如果没有声明另有说明,分配给一个名称会使它成为最内层/当前范围的本地。因此,例如,如果一个 for 循环有它自己的范围,您就不能轻易地修改循环外的变量。

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

同样,我认为意外重用名称(以引入错误或陷阱的方式)是罕见的,而且很小。

编辑:再次尽可能清楚地说明这一点:

  • 在使用 GC 的语言中不能进行基于堆栈的清理。根据定义,这是不可能的:变量是对堆上对象的潜在众多引用之一,这些对象既不知道也不关心变量何时超出范围,并且所有内存管理都掌握在 GC 手中,当它运行时喜欢,而不是在弹出堆栈帧时。资源清理的解决方法不同,见下文。
  • 确定性清理通过with语句发生。是的,它没有引入新的范围(见下文),因为这不是它的用途。托管对象绑定的名称不被删除并不重要——尽管如此,清理工作还是发生了,剩下的是一个“不要碰我,我不可用”的对象(例如,一个关闭的文件流)。
  • Python 对每个函数、类和模块都有一个作用域。时期。这就是语言的工作原理,不管你喜不喜欢。如果您想要/“需要”更细粒度的范围,请将代码分解为更细粒度的函数。您可能希望更细粒度的范围界定,但没有 - 并且由于此答案前面指出的原因(“编辑:”上方的三个段落),这是有原因的。不管你喜不喜欢,但这就是语言的工作方式。
于 2011-02-21T21:17:07.647 回答
18
  1. 你是对的with- 它与变量范围完全无关。

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

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

  4. 生成器表达式(在 Python 3 中还有列表推导式)有自己的作用域。

  5. 如果你的函数足够长以至于你忘记了局部变量,你可能应该重构你的代码。

于 2011-02-21T21:00:24.060 回答
12

但 RAII 也适用于 C++ 的范围规则,以确保对象的迅速销毁。

这在 GC 语言中被认为是不重要的,这些语言基于内存是可替代的思想。只要其他地方有足够的内存来分配新对象,就没有迫切需要回收对象的内存。文件句柄、套接字和互斥锁等不可替代的资源被认为是需要特别处理的特殊情况(例如,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)
于 2011-02-22T13:30:13.803 回答
2

基本上你可能使用了错误的语言。如果你想要健全的范围规则和可靠的破坏,那么坚持使用 C++ 或尝试 Perl。关于何时释放内存的 GC 辩论似乎没有抓住重点。这是关于释放其他资源,如互斥锁和文件句柄。我相信 C# 区分了当引用计数变为零时调用的析构函数和决定回收内存时调用的析构函数。人们并不关心内存回收,但一旦不再引用它就想知道。遗憾的是,Python 作为一种语言具有真正的潜力。但它是非常规的作用域和不可靠的析构函数(或至少是依赖于实现的析构函数)意味着你无法获得 C++ 和 Perl 的强大功能。

有趣的是关于仅使用新内存(如果可用)而不是在 GC 中回收旧内存的评论。这不是说它泄漏内存的一种奇特方式吗:-)

于 2011-08-25T14:08:08.883 回答
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是 a ScopedList,它在子级上调用acquiredispose方法,您将获得与在 C++ 中习惯的相同的确定性资源获取和处置保证。

于 2018-02-08T16:04:09.903 回答