Python中真的可能因代码导致内存泄漏吗?

80 投票
6 回答
32483 浏览
提问于 2025-04-15 17:43

我没有代码示例,但我很好奇,是否有可能写出会导致内存泄漏的Python代码。

6 个回答

11

当然可以。内存泄漏的一个典型例子就是你创建了一个缓存,但从来没有手动清理过,而且这个缓存也没有自动清除的规则。

20

内存泄漏的经典定义是:某些内存曾经被使用过,但现在不再使用,却没有被释放掉。用纯Python代码出现这种情况几乎是不可能的。不过,正如Antoine所说,如果你让数据结构无限制地增长,即使你并不需要保留所有的数据,也很容易就会导致内存被耗尽。

而使用C扩展时,你就回到了不受管理的领域,任何事情都有可能发生。

174

是的,这是可能的。

这要看你说的内存泄漏是什么类型。在纯Python代码中,不像C语言那样会“忘记释放”内存,但确实有可能在某个地方留下一个引用。以下是一些例子:

一个未处理的追踪对象,导致整个栈帧仍然存在,即使函数已经不再运行

while game.running():
    try:
        key_press = handle_input()
    except SomeException:
        etype, evalue, tb = sys.exc_info()
        # Do something with tb like inspecting or printing the traceback

在这个可能是游戏循环的简单例子中,我们把tb赋值给了一个局部变量。我们本是好心,但这个tb包含了关于我们在handle_input中发生的事情的栈信息,可能涉及到非常深的调用和那些栈中的任何内容。假设你的游戏继续运行,这个'tb'在你下次调用handle_input时仍然存在,甚至可能永远存在。关于这个潜在的循环引用问题,文档现在建议,如果你不绝对需要tb,就不要赋值。如果你只需要获取一个追踪信息,可以考虑使用traceback.format_exc

在类或全局范围内存储值,而不是在实例范围内,并且没有意识到这一点。

这个问题可能以隐蔽的方式发生,但通常是在你在类范围内定义可变类型时出现。

class Money:
    name = ''
    symbols = []   # This is the dangerous line here

    def set_name(self, name):
        self.name = name

    def add_symbol(self, symbol):
        self.symbols.append(symbol)

在上面的例子中,假设你这样做了:

m = Money()
m.set_name('Dollar')
m.add_symbol('$')

你可能会很快发现这个特定的错误。发生的情况是,你把一个可变值放在了类范围内,即使你在实例范围内正确访问它,它实际上是“掉落”到了类对象__dict__中。

在某些情况下,这可能导致你的应用程序的堆不断增长,并在某些情况下,例如在不定期重启进程的生产网络应用中,造成问题。

类中存在循环引用,同时还有一个__del__方法。

作者注 - 从Python 3.4开始,这个问题大部分通过PEP-0442解决了

讽刺的是,存在一个__del__方法使得循环垃圾收集器在Python 2和早期版本的Python 3中无法清理一个实例。假设你有一个想要做析构函数以进行清理的情况:

class ClientConnection:
    def __del__(self):
        if self.socket is not None:
            self.socket.close()
            self.socket = None

现在这个单独运行得很好,你可能会认为这是一个良好的资源管理方式,以确保套接字被“处理掉”。

然而,如果ClientConnection保持对User的引用,而User又保持对连接的引用,你可能会想在清理时让用户解除对连接的引用。但这实际上是一个缺陷循环GC不知道正确的操作顺序,无法清理它

解决这个问题的方法是确保在断开连接事件时进行清理,调用某种关闭方法,但将该方法命名为其他名称,而不是__del__

实现不佳的C扩展,或者没有正确使用C库。

在Python中,你依赖垃圾收集器来处理那些你不再使用的东西。但如果你使用一个包装了C库的扩展,大多数情况下,你需要负责确保显式关闭或释放资源。大多数情况下,这些都有文档说明,但习惯于不需要显式释放的Python程序员可能会在不知道资源仍被占用的情况下丢弃对该库或其中一个对象的引用。

包含闭包的作用域,可能包含比你预想的更多内容

class User:
    def set_profile(self, profile):
        def on_completed(result):
            if result.success:
                self.profile = profile

        self._db.execute(
            change={'profile': profile},
            on_complete=on_completed
        )

在这个人为构造的例子中,我们似乎在使用某种“异步”调用,当数据库调用完成时会在on_completed中回调我们(实现可以是承诺,结果是相同的)。

你可能没有意识到的是,on_completed闭包绑定了对self的引用,以便执行self.profile赋值。现在,假设数据库客户端跟踪活动查询和指向完成时调用的闭包的指针(因为是异步的),如果由于某种原因崩溃了。如果数据库客户端没有正确清理回调等,那么在这种情况下,数据库客户端现在持有对on_completed的引用,而on_completed又持有对User的引用,User保持着_db - 你现在创建了一个可能永远无法被收集的循环引用。

(即使没有循环引用,闭包绑定局部变量甚至实例的事实也可能导致你认为已经被收集的值长时间存在,这可能包括套接字、客户端、大缓冲区和整个对象树)

默认参数是可变类型

def foo(a=[]):
    a.append(time.time())
    return a

这是一个人为构造的例子,但人们可能会认为a的默认值是一个空列表,意味着可以向其中添加内容,实际上它是对同一个列表的引用。这再次类似于之前的Money例子,可能会导致不受限制的增长,而你并不知道自己做了这件事。

(2023年8月更新说明:这篇文章最初写于2010年,里面的信息至今仍然大部分有效,我只是对网址参考做了一些小更新,并确保代码示例在Python 2和Python 3中都有效)

撰写回答