Python中的RAII - 离开作用域时自动销毁
我一直在尝试在Python中找到RAII的用法。资源分配即初始化(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 scope”,但没有找到直接且权威地解决这个问题的内容。我查看了所有的PEP,似乎在Python中并没有涉及这个概念。
我想在Python中有作用域变量,是不是很糟糕?这是不是太不符合Python的风格了?
我是不是没有理解这个问题?
也许我在试图剥夺这个语言动态特性的好处。想要强制作用域是不是自私?
我想让编译器/解释器抓住我不小心重用变量的错误,是不是懒惰?当然,我是懒惰,但这种懒惰算不算坏呢?
6 个回答
但是RAII(资源获取即初始化)也利用C++的作用域规则,确保对象能及时被销毁。
在使用垃圾回收(GC)的编程语言中,这个问题被认为不太重要,因为这些语言的设计理念是内存是可以互换的。只要其他地方还有足够的内存可以用来分配新对象,就没有必要急着回收一个对象的内存。像文件句柄、套接字和互斥锁这样的非可互换资源被视为特殊情况,需要特别处理(例如,使用with
语句)。这与C++的模型不同,C++将所有资源都视为相同。
一旦变量从栈中弹出,它就会被销毁。
Python没有栈变量。在C++的说法中,所有的东西都是shared_ptr
。
Python确实有作用域,但不是在缩进级别,而是在函数级别。要求我为了作用域而新建一个函数,这听起来很傻,因为这样我才能重用一个名字。
它在生成器推导式的层面上也有作用域(在3.x版本中,所有的推导式都有)。
如果你不想覆盖你的for
循环变量,就不要用太多的for
循环。特别是,在循环中使用append
是不符合Python风格的。与其这样:
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)
你说得对,
with
这个东西和变量的作用域完全没有关系。如果你觉得全局变量会带来问题,那就尽量避免使用它们。这也包括模块级别的变量。
在Python中,隐藏状态的主要工具是类。
生成器表达式(在Python 3中,列表推导式也是如此)有自己独立的作用域。
如果你的函数太长,以至于你开始搞不清楚局部变量的情况,那你可能需要重构一下你的代码。
总结一下:RAII(资源获取即初始化)在这里是行不通的,你可能把它和一般的作用域搞混了。当你错过那些额外的作用域时,可能是你在写糟糕的代码。
也许我没理解你的问题,或者你对Python的一些基本概念不太清楚……首先,在垃圾回收的语言中,和作用域相关的确定性对象销毁是不可能的。Python中的变量只是对对象的引用。你不会希望一个用malloc
分配的内存块在指向它的指针超出作用域后立刻被free
掉吧?在某些情况下,如果你使用引用计数,可能会有例外,但没有哪个语言会把具体的实现写死。
即使你有引用计数,比如在CPython中,这也是一种实现细节。一般来说,包括Python在内的各种实现并不使用引用计数,你应该假设每个对象会一直存在,直到内存用完。
至于在函数调用期间名字的存在:你可以通过del
语句从当前或全局作用域中删除一个名字。不过,这和手动内存管理没有关系。它只是删除了引用。这可能会触发被引用对象的垃圾回收,但这并不是重点。
- 如果你的代码足够长,以至于会引发名字冲突,那你应该写更小的函数,并使用更具描述性、不容易冲突的名字。嵌套循环覆盖外部循环的迭代变量也是一样:我还没遇到过这个问题,也许是因为你的名字不够描述性,或者你应该把这些循环拆开?
你说得对,with
和作用域没有关系,只是和确定性清理有关(所以在结果上和RAII有重叠,但在实现方式上没有)。
也许我在试图剥夺语言动态特性的好处。想要强制作用域是否自私?
不是的。合理的词法作用域是一个独立于动态或静态的优点。诚然,Python(2到3基本上解决了这个问题)在这方面有一些弱点,主要是在闭包的领域。
但要解释“为什么”:Python必须在开始新作用域时保持保守,因为如果没有声明说明,否则对名字的赋值会使其成为最内层/当前作用域的局部变量。所以例如,如果一个for循环有自己的作用域,你就不能轻易修改循环外的变量。
我想让编译器/解释器抓住我不小心重用变量的错误,是不是懒惰?当然,我懒惰,但我这种懒惰算不算坏呢?
再说一次,我认为意外重用名字(以引入错误或陷阱的方式)是很少见的,而且影响也不大。
编辑:为了尽可能清楚地重申这一点:
- 在使用垃圾回收的语言中,不能有基于栈的清理。 这从定义上来说就是不可能的:一个变量可能是指向堆上对象的多个引用之一,它们既不知道也不关心变量何时超出作用域,所有的内存管理都在垃圾回收器手中,它在自己想的时候运行,而不是在栈帧弹出时。资源清理是用不同的方式解决的,见下文。
- 确定性清理通过
with
语句发生。 是的,它并不会引入新的作用域(见下文),因为这不是它的目的。被管理对象绑定的名字是否被移除并不重要——清理仍然发生,剩下的只是一个“别碰我,我不能用了”的对象(例如,一个关闭的文件流)。 - Python每个函数、类和模块都有一个作用域。就是这样。 这就是语言的工作方式,无论你喜欢与否。如果你想要/“需要”更细粒度的作用域,就把代码拆分成更细的函数。你可能希望有更细粒度的作用域,但实际上并没有——而且之前提到的原因(在“编辑”之前的三段)也解释了这一点。喜欢与否,这就是语言的工作方式。