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

发布于 2024-10-18 08:51:53 字数 1467 浏览 2 评论 0原文

我一直在尝试在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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

爱冒险 2024-10-25 08:51:53

tl;dr RAII 是不可能的,你将它与一般的作用域混合在一起,当你错过那些额外的作用域时,你可能正在编写糟糕的代码。

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

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

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

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

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

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

不。良好的词法作用域是一个独立于动态/静态的优点。不可否认,Python(2 - 3 几乎修复了这个问题)在这方面有弱点,尽管它们更多地处于闭包领域。

但要解释一下“为什么”:Python必须对于开始新作用域的位置保持保守,因为如果没有声明另行说明,对名称的赋值会使其成为最内部/当前作用域的本地名称。因此,例如,如果 for 循环有其自己的作用域,则您无法轻松修改循环之外的变量。

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

再次,我认为意外重复使用名称(以引入错误或陷阱的方式)是罕见的,而且无论如何都是很小的。

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

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

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 be free'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.

  • If your code is long enough for this to cause name clashes, you should write smaller functions. And use more descriptive, less likely-to-clash names. Same for nested loops overwriting the out loop's iteration variable: I'm yet to run into this issue, so perhaps your names are not descriptive enough or you should factor these loops apart?

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).

Perhaps I'm trying to take away the benefits of the dynamic aspects of the language. Is it selfish to sometimes want scope enforced?

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.

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?

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:

  • There can't be stack-based cleanup in a language using GC. It's just not possibly, by definition: a variable is one of potentially many references to objects on the heap that neither know nor care about when variables go out of scope, and all memory management lies in the hands of the GC, which runs when it likes to, not when a stack frame is popped. Resource cleanup is solved differently, see below.
  • Deterministic cleanup happens through the 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).
  • Python has a scope per function, class, and module. Period. That's how the language works, whether you like it or not. If you want/"need" more fine-grained scoping, break the code into more fine-grained functions. You might wish for more fine-grained scoping, but there isn't - and for reasons pointed out earlier in this answer (three paragraphs above the "Edit:"), there are reasons for this. Like it or not, but this is how the language works.
漆黑的白昼 2024-10-25 08:51:53
  1. 您对 with 的看法是正确的 - 它与变量作用域完全无关。

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

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

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

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

  1. You are right about with -- it is completely unrelated to variable scoping.

  2. Avoid global variables if you think they are a problem. This includes module level variables.

  3. The main tool to hide state in Python are classes.

  4. Generator expressions (and in Python 3 also list comprehensions) have their own scope.

  5. If your functions are long enough for you to lose track of the local variables, you should probably refactor your code.

比忠 2024-10-25 08:51:53

但是 RAII 也适用于范围界定
C++规则保证提示
对象的破坏。

这在 GC 语言中被认为是不重要的,GC 语言基于内存可替换的思想。只要其他地方有足够的内存来分配新对象,就没有迫切需要回收对象的内存。文件句柄、套接字和互斥体等不可替代资源被视为需要特殊处理的特殊情况(例如,with)。这与对所有资源一视同仁的 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)

But RAII also works with the scoping
rules of C++ to ensure the prompt
destruction of the object.

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.

As soon as the variable pops off the
stack it is destroyed.

Python doesn't have stack variables. In C++ terms, everything is a shared_ptr.

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.

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 many for loops. In particular, it's un-Pythonic to use append in a loop. Instead of:

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

write:

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

or

# 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)
谁的年少不轻狂 2024-10-25 08:51:53

基本上你可能使用了错误的语言。如果您想要合理的范围规则和可靠的破坏,那么坚持使用 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 :-)

べ映画 2024-10-25 08:51:53

在使用 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++ 中习惯的相同的确定性资源获取和处置保证。

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:

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

So, here is way to get RAII-type behavior: create a container with add and remove hooks:

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)

If UiComponent.children is a ScopedList, which calls acquire and dispose methods on the children, you get the same guarantee of deterministic resource acquisition and disposal as you are used to in C++.

时光清浅 2024-10-25 08:51:53

正如一些评论所指出的,上下文管理器(使用 with)是在 Python 中实现 RAII 的方式(尽管正如许多答案所表明的那样,它更多地与非内存资源有关,因为 GC 负责处理记忆)。

自定义上下文管理器可以通过定义 __enter__ 和 __exit__ 方法来实现。例如,来自 https://dev.to/ fronkan/比较-c-raii-和-python-context-managers-50eg

class PrintingTempFileContext:
    def __enter__(self):
        print("<Opening File>")
        self.file = TemporaryFile(mode="w+t")
        return self.file

    def __exit__(self, exception_type, exception_value, traceback):
        print(f"<Exception info: {exception_type=} - {exception_value=} - {traceback=}>")
        self.file.seek(0)
        print(self.file.read())
        self.file.close()
        print("<File closed>")
with PrintingTempFileContext() as tempfile:
    tempfile.write("Hello DEV!")

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:

class PrintingTempFileContext:
    def __enter__(self):
        print("<Opening File>")
        self.file = TemporaryFile(mode="w+t")
        return self.file

    def __exit__(self, exception_type, exception_value, traceback):
        print(f"<Exception info: {exception_type=} - {exception_value=} - {traceback=}>")
        self.file.seek(0)
        print(self.file.read())
        self.file.close()
        print("<File closed>")
with PrintingTempFileContext() as tempfile:
    tempfile.write("Hello DEV!")
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文