Python 中的 RAII - 离开范围时自动销毁
我一直在尝试在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 scope”,但找不到任何内容 直接且权威地解决了这个问题。 我已经查看了所有 PEP。这个概念似乎没有得到解决 在 Python 中。
因为我想在 Python 中定义作用域变量,所以我是一个坏人吗? 这是不是太不Pythonic了?
我这不是在摸索吗?
也许我正试图剥夺该语言动态方面的好处。 有时想要强制执行范围是自私吗?
我是否懒惰想要编译器/解释器 捕捉我疏忽的变量重用错误?嗯,是的,我当然很懒, 但我是不是太懒了?
I've been trying to findRAII in Python.
Resource Allocation Is Initialization is a pattern in C++ whereby
an object is initialized as it is created. If it fails, then it throws
an exception. In this way, the programmer knows that
the object will never be left in a half-constructed state. Python
can do this much.
But RAII also works with the scoping rules of C++
to ensure the prompt destruction of the object. As soon as the variable
pops off the stack it is destroyed. This may happen in Python, but only
if there are no external or circular references.
More importantly, a name for an object still exists until the function it is
in exits (and sometimes longer). Variables at the module level will
stick around for the life of the module.
I'd like to get an error if I do something like this:
for x in some_list:
...
... 100 lines later ...
for i in x:
# Oops! Forgot to define x first, but... where's my error?
...
I could manually delete the names after I've used it,
but that would be quite ugly, and require effort on my part.
And I'd like it to Do-What-I-Mean in this case:
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 does some scoping, but not at the indentation level, just at
the functional level. It seems silly to require that I make a new function
just to scope the variables so I can reuse a name.
Python 2.5 has the "with" statement
but that requires that I explicitly put in __enter__
and __exit__
functions
and generally seems more oriented towards cleaning up resources like files
and mutex locks regardless of the exit vector. It doesn't help with scoping.
Or am I missing something?
I've searched for "Python RAII" and "Python scope" and I wasn't able to find anything that
addressed the issue directly and authoritatively.
I've looked over all the PEPs. The concept doesn't seem to be addressed
within Python.
Am I a bad person because I want to have scoping variables in Python?
Is that just too un-Pythonic?
Am I not grokking it?
Perhaps I'm trying to take away the benefits of the dynamic aspects of the language.
Is it selfish to sometimes want scope enforced?
Am I lazy for wanting the compiler/interpreter
to catch my negligent variable reuse mistakes? Well, yes, of course I'm lazy,
but am I lazy in a bad way?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
tl;dr RAII 是不可能的,你将它与一般的作用域混合在一起,当你错过那些额外的作用域时,你可能正在编写糟糕的代码。
也许我没有明白你的问题,或者你没有得到关于 Python 的一些非常重要的东西......首先,与范围相关的确定性对象销毁在垃圾收集语言中不可能 。 Python 中的变量仅仅是引用。您不希望一旦指向它的指针超出范围,
malloc
的内存块就被free
释放,不是吗?如果您碰巧使用引用计数,那么在某些情况下会出现实际异常 - 但没有一种语言疯狂到足以一成不变地设置确切的实现。即使您有引用计数(如 CPython 中那样),它也是一个实现细节。一般来说,包括在具有各种不使用引用计数的实现的 Python 中,您应该像每个对象都挂起直到内存耗尽一样进行编码。
至于函数调用其余部分中现有的名称:您可以通过
del
语句从当前或全局作用域中删除名称。然而,这与手动内存管理无关。它只是删除引用。这可能会也可能不会触发引用的对象被 GC,这不是练习的重点。你是对的,
with
与范围界定无关,只是与确定性清理有关(因此它在末端与 RAII 重叠,但在方法上不重叠)。不。良好的词法作用域是一个独立于动态/静态的优点。不可否认,Python(2 - 3 几乎修复了这个问题)在这方面有弱点,尽管它们更多地处于闭包领域。
但要解释一下“为什么”:Python必须对于开始新作用域的位置保持保守,因为如果没有声明另行说明,对名称的赋值会使其成为最内部/当前作用域的本地名称。因此,例如,如果 for 循环有其自己的作用域,则您无法轻松修改循环之外的变量。
再次,我认为意外重复使用名称(以引入错误或陷阱的方式)是罕见的,而且无论如何都是很小的。
编辑:为了尽可能清楚地再次说明这一点:
with
语句进行。是的,它不会引入新的作用域(见下文),因为这不是它的用途。托管对象绑定的名称没有被删除并不重要 - 尽管如此,清理还是发生了,剩下的是“不要碰我,我不可用”对象(例如关闭的文件流)。tl;dr RAII is not possible, you mix it up with scoping in general and when you miss those extra scopes you're probably writing bad code.
Perhaps I don't get your question(s), or you don't get some very essential things about Python... First off, deterministic object destruction tied to scope is impossible in a garbage collected language. Variables in Python are merely references. You wouldn't want a
malloc
'd chunk of memory to befree
'd as soon as a pointer pointing to it goes out of scope, would you? Practical exception in some circumstances if you happen to use ref counting - but no language is insane enough to set the exact implementation in stone.And even if you have reference counting, as in CPython, it's an implementation detail. Generally, including in Python which has various implementations not using ref counting, you should code as if every object hangs around until memory runs out.
As for names existing for the rest of a function invocation: You can remove a name from the current or global scope via the
del
statement. However, this has nothing to do with manual memory management. It just removes the reference. That may or may not happen to trigger the referenced object to be GC'd and is not the point of the exercise.You are correct,
with
has nothing to do with scoping, just with deterministic cleanup (so it overlaps with RAII in the ends, but not in the means).No. Decent lexical scoping is a merit independent of dynamic-/staticness. Admittedly, Python (2 - 3 pretty much fixed this) has weaknesses in this regard, although they're more in the realm of closures.
But to explain "why": Python must be conservative with where it starts a new scope because without declaration saying otherwise, assignment to a name makes it a local to the innermost/current scope. So e.g. if a for loop had it's own scope, you couldn't easily modify variables outside of the loop.
Again, I imagine that accidential resuse of a name (in a way that introduces errors or pitfalls) is rare and a small anyway.
Edit: To state this again as clearly as possible:
with
statement. Yes, it doesn't introduce a new scope (see below), because that's not what it's for. It doesn't matter the name the managed object is bound to isn't removed - the cleanup happened nonetheless, what remains is a "don't touch me I'm unusable" object (e.g. a closed file stream).您对
with
的看法是正确的 - 它与变量作用域完全无关。如果您认为全局变量有问题,请避免使用它们。这包括模块级变量。
Python 中隐藏状态的主要工具是类。
生成器表达式(在 Python 3 中还有列表推导式)有自己的作用域。
如果你的函数足够长,以至于你无法跟踪局部变量,那么你可能应该重构你的代码。
You are right about
with
-- it is completely unrelated to variable scoping.Avoid global variables if you think they are a problem. This includes module level variables.
The main tool to hide state in Python are classes.
Generator expressions (and in Python 3 also list comprehensions) have their own scope.
If your functions are long enough for you to lose track of the local variables, you should probably refactor your code.
这在 GC 语言中被认为是不重要的,GC 语言基于内存可替换的思想。只要其他地方有足够的内存来分配新对象,就没有迫切需要回收对象的内存。文件句柄、套接字和互斥体等不可替代资源被视为需要特殊处理的特殊情况(例如,
with
)。这与对所有资源一视同仁的 C++ 模型形成鲜明对比。Python 没有堆栈变量。用 C++ 术语来说,一切都是
shared_ptr
。它还在生成器理解级别(以及在3.x中,在所有理解中)进行范围界定。
如果您不想破坏
for
循环变量,请不要使用这么多for
循环。特别是,在循环中使用append
是不符合 Python 风格的。而不是:写:
或
This is considered unimportant in GC languages, which are based on the idea that memory is fungible. There is no pressing need to reclaim an object's memory as long as there's enough memory elsewhere to allocate new objects. Non-fungible resources like file handles, sockets, and mutexes are considered a special case to be dealt with specially (e.g.,
with
). This contrasts with C++'s model that treats all resources the same.Python doesn't have stack variables. In C++ terms, everything is a
shared_ptr
.It also does scoping at the generator comprehension level (and in 3.x, in all comprehensions).
If you don't want to clobber your
for
loop variables, don't use so manyfor
loops. In particular, it's un-Pythonic to useappend
in a loop. Instead of:write:
or
基本上你可能使用了错误的语言。如果您想要合理的范围规则和可靠的破坏,那么坚持使用 C++ 或尝试 Perl。关于何时释放内存的 GC 争论似乎没有抓住要点。它是关于释放其他资源,例如互斥体和文件句柄。我相信 C# 区分了当引用计数变为零时和决定回收内存时调用的析构函数。人们并不那么关心内存回收,但确实想在它不再被引用时立即知道。遗憾的是,Python 作为一门语言具有真正的潜力。但它的非传统作用域和不可靠的析构函数(或者至少是依赖于实现的析构函数)意味着人们无法获得 C++ 和 Perl 的强大功能。
有趣的是关于仅使用新内存(如果可用)而不是在 GC 中回收旧内存的评论。这不就是内存泄漏的一种奇特说法吗:-)
Basically you are probably using the wrong language. If you want sane scoping rules and reliable destruction then stick with C++ or try Perl. The GC debate about when memory is released seems to miss the point. It's about releasing other resources like mutexes and file handles. I believe C# makes the distinction between a destructor that is called when the reference count goes to zero and when it decides to recycle the memory. People aren't that concerned about the memory recycling but do want to know as soon as it is no longer referenced. It's a pity as Python had real potential as a language. But it's unconventional scoping and unreliable destructors (or at least implementation dependent ones) means that one is denied the power you get with C++ and Perl.
Interesting the comment made about just using new memory if it's available rather than recycling old in GC. Isn't that just a fancy way of saying it leaks memory :-)
在使用 C++ 多年后切换到 Python 时,我发现依赖
__del__
来模仿 RAII 类型的行为(例如关闭文件或连接)很诱人。然而,在某些情况下(例如,由 Rx 实现的观察者模式),被观察的事物会维护对对象的引用,使其保持活动状态!因此,如果您想在连接被源终止之前关闭连接,则尝试在 __del__ 中执行此操作将无济于事。UI 编程中会出现以下情况:
因此,这是获得 RAII 类型行为的方法:创建一个带有添加和删除钩子的容器:
如果
UiComponent.children
是一个ScopedList
,它对子级调用acquire
和dispose
方法,您可以获得与在 C++ 中习惯的相同的确定性资源获取和处置保证。When switching to Python after years of C++, I have found it tempting to rely on
__del__
to mimic RAII-type behavior, e.g. to close files or connections. However, there are situations (e.g. observer pattern as implemented by Rx) where the thing being observed maintains a reference to your object, keeping it alive! So, if you want to close the connection before it is terminated by the source, you won't get anywhere by trying to do that in__del__
.The following situation arises in UI programming:
So, here is way to get RAII-type behavior: create a container with add and remove hooks:
If
UiComponent.children
is aScopedList
, which callsacquire
anddispose
methods on the children, you get the same guarantee of deterministic resource acquisition and disposal as you are used to in C++.正如一些评论所指出的,上下文管理器(使用
with
)是在 Python 中实现 RAII 的方式(尽管正如许多答案所表明的那样,它更多地与非内存资源有关,因为 GC 负责处理记忆)。自定义上下文管理器可以通过定义 __enter__ 和 __exit__ 方法来实现。例如,来自 https://dev.to/ fronkan/比较-c-raii-和-python-context-managers-50eg:
As some of the comments have indicated, context managers (using
with
) are the way to have RAII in Python (although as many of the answers indicate, it's more about non-memory resources since GC takes care of memory).Custom context managers can be implemented by defining
__enter__
and__exit__
methods. For example, from https://dev.to/fronkan/comparing-c-raii-and-python-context-managers-50eg: