Frost's Blog
363 字
2 分钟
一个 monkeypatch 引起的循环引用问题

最近社区里有人提了一个PR1,解决了一个潜在的内存泄漏问题。我认为这个问题在很多场景都容易忽略,所以分享在这里。

上代码

import gc

class Foo:
    def bar(self):
        print("bar")

foo = Foo()

print("Before", gc.get_count())
foo.bar = foo.bar
print("setattr", gc.get_count())
del foo
print("After", gc.get_count())

运行发现 foo 对象无法被正常回收,造成了内存泄漏。这是因为看似不起眼的一行 foo.bar = foo.bar,实际上创建了一个循环引用。

点解?

因为,foo.bar 这个表达式,实际上执行了 Foo.bar.__get__(foo, Foo),返回了一个绑定了 foo 的方法,此方法中持有 foo 的引用。而 foo.bar = foo.bar,相当于把这个结果缓存在了 foo.bar 属性上。之后的 foo.bar 只是从属性上取值,这样 foo 和该方法互相持有对方的引用,出现了循环引用。

现实中我们不太可能会写出 foo.bar = foo.bar 这样的代码,但是在 monkeypatch 场景下,可能很容易被坑到,比如:

import requests

resp = requests.get("https://example.com", stream=True)
resp._fp = CallbackFileWrapper(resp, callback)
...

上述代码 patch 了 requests 库的 Response 对象,让它在被读取完成中调用我们指定的 callback 函数。这里 CallbackFileWrapper 会持有 resp 的引用。

如何避免?

在上述例子中,我们可以使用 weakref 来避免循环引用:

import weakref

class CallbackFileWrapper:
    def __init__(self, resp, callback):
        self.resp_ref = weakref.ref(resp)
        self.callback = callback

    def read(self, size):
        resp = self.resp_ref()
        if resp is None:
            return b""
        read_bytes = resp.read(size)
        if not read_bytes:
            self.callback(resp)

Footnotes#

  1. https://github.com/psf/cachecontrol/pull/344/

一个 monkeypatch 引起的循环引用问题
https://frostming.com/2024/circular-ref-monkeypatch/
作者
Frost Ming
发布于
2024-12-18
许可协议
CC BY-NC-SA 4.0