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

mar*_*ets 31 python scope raii with-statement

我一直试图在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?
    ...
Run Code Online (Sandbox Code Playgroud)

我可以在使用它之后手动删除这些名称,但这样会非常难看,而且我需要付出努力.

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

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)
Run Code Online (Sandbox Code Playgroud)

Python做了一些范围界定,但不是在缩进级别,只是在功能级别.要求我创建一个新函数来定义变量以便我可以重用一个名称似乎很愚蠢.

Python 2.5具有"with"语句, 但这需要我明确地放入__enter____exit__函数,并且通常似乎更倾向于清理文件和互斥锁等资源,而不管退出向量.它对范围界定没有帮助.或者我错过了什么?

我搜索过"Python RAII"和"Python范围",我无法直接和权威地找到解决问题的任何内容.我查看了所有的PEP.这个概念似乎没有在Python中得到解决.

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

我不是喜欢它吗?

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

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

小智 32

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

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

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

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

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

你是正确的,with与范围无关,只与确定性清理有关(所以它在结尾处与RAII重叠,但不在手段中).

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

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

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

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

再一次,我想象一个名称的重复使用(以一种引入错误或陷阱的方式)是罕见的,无论如何.

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

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

  • @markets:嗯,你问 RAII 做什么?为了清理?这就是`with`的用途。因为资源清理应该在`__exit__` 中,或者,如果你是一个从未听说过上下文管理器的可怜人,那么在一些外部可管理的资源中。根据定义,GC 几乎是非确定性的,并且您不会让 Python 执行面跟转向并重新引入手动内存管理或它的一些稍微放电的变体。按原样使用该语言或使用另一种语言。您如何看待 C++ 中对 GC 的需求? (2认同)
  • @markets:正如我已经说过的那样,`with`*是确定性清理的方法.虽然名称在`with`块结束后仍然存在,但清理完成后因此可以获得与应用RAII相同的效果,尽管通过其他方式.或者你是否滥用了RAII这个术语,实际上只是想要更细粒度的范围? (2认同)

Sve*_*ach 14

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

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

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

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

  5. 如果您的函数足够长,以至于无法跟踪局部变量,那么您应该重构代码.

  • 第5点表明,当他被要求接触时,Sven与他的时间无关,而不是重构其他人的代码.虽然我很同情这种情绪,而且我在书中读到了很多东西,但我并没有那么奢侈. (7认同)

dan*_*n04 9

但RAII也适用于C++的范围规则,以确保迅速破坏对象.

这在GC语言中被认为是不重要的,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) )
Run Code Online (Sandbox Code Playgroud)

写:

new_points = [do_something_with(x, y, z) for (x, y, z) in surface.points]
Run Code Online (Sandbox Code Playgroud)

要么

# 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)
Run Code Online (Sandbox Code Playgroud)