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
一个 monkeypatch 引起的循环引用问题
https://frostming.com/2024/circular-ref-monkeypatch/