Python 的弱引用到底怎么用?看完这个内存管理案例,我彻底悟了.
让我跟你讲一个真实的故事。我以前写了一个爬虫程序,每天抓取几万个网页。刚开始运行得好好的,过了两天程序就卡死了。我查了半天内存,发现一个缓存字典占了500多兆。
你可能觉得奇怪,缓存能有多大问题。问题在于我存的是网页对象,每个对象都带着一大堆属性和方法。这些对象被字典引用着,不管程序用不用它们,内存都释放不掉。这就好比你把所有东西都塞进衣柜,柜门一关就以为眼不见心不烦了,可衣柜里的东西一个没少。
后来我发现了Python的弱引用。弱引用这东西说白了就是,它不增加对象的引用计数。对象被强引用指着的时候,弱引用照样能用。一旦强引用没了,对象就会被回收,弱引用自动变成None。
具体怎么用呢。拿我那个爬虫举例,我原来是这样写的:
import time class WebPage:
def __init__(self, url):
self.url = url
self.content = None
def fetch(self):
模拟网络请求
time.sleep(0.1)
self.content = f'<html>{self.url}</html>'
强引用版本
page_cache = {}
def get_page(url):
if url in page_cache:
return page_cache[url]
page = WebPage(url)
page.fetch()
page_cache[url] = page
return page
这么做的问题在于,page_cache里存的是实际对象引用。就算程序其他地方已经不需要这些页面对象了,它们还赖在缓存里不走。一个爬虫跑一天,可能有一半的页面只用过一次,却一直占着内存。
改成弱引用就行了:
import weakref page_cache = {}
def get_page(url):
if url in page_cache:
weakref.ref() 返回的是一个弱引用对象
ref = page_cache[url]
page = ref()
if page is not None:
return page
page = WebPage(url)
page.fetch()
page_cache[url] = weakref.ref(page)
return page
改动就这么简单。把weakref.ref(page)存进字典。外部变量不再引用page的时候,page就被Python回收了。缓存字典里那个弱引用变成空指针,下次访问这个URL会重新创建一个新页面。缓存变成了一个自动清理的字典,只保留正在被使用的页面。
还有个更好用的东西叫WeakValueDictionary。它专门处理这种情况,直接存储弱引用。上面的代码可以简化成:
page_cache = weakref.WeakValueDictionary() def get_page(url):
if url in page_cache:
return page_cache[url]
page = WebPage(url)
page.fetch()
page_cache[url] = page
return page
WeakValueDictionary会自动处理弱引用,键值对的值是弱引用的。一旦值对象没被其他地方引用,字典里的条目就被移除。你连判断None的代码都不用写。
我改了代码以后,爬虫程序跑了三天,内存始终稳定在100兆左右。以前那些用过就扔的页面对象,很快就被回收了。缓存里只保留了那些还在被上层逻辑引用的页面。
还有一个场景你可能遇到过。写GUI程序的时候,要给按钮绑定回调函数。不小心绑成了对象方法,整个对象就被按钮的引用拖着跑不掉。用弱引用做回调就能避免这种情况:
import weakref class Controller:
def __init__(self):
self.data = '重要数据'
def on_click(self):
print(self.data)
btn = Button()
直接绑定会导致Controller对象被Button引用
btn.click.connect(controller.on_click) 错误做法
用弱引用绑定,Controller被销毁后回调自动失效
btn.click.connect(weakref.WeakMethod(controller.on_click))
weakref.WeakMethod专门用来绑定方法。对象被析构以后,弱引用指向的方法自动失效,不会再触发回调。这在复杂的UI交互中能避免很多内存泄漏问题。
当然弱引用也不是万能的。有些类型不能使用弱引用,比如列表、字典、整数这些内置类型。你可以自己定义一个类,设置__slots__或者给类加上__weakref__属性。大部分自定义类默认就支持弱引用。
再提醒一点,别滥用弱引用。如果你所有的对象都用弱引用,那它们可能还没用完就被垃圾回收了。弱引用适合的场景是缓存、回调、观察者模式这些,不需要保持对象生命周期的场合。核心业务逻辑该用强引用还得用强引用。
我后来把弱引用用在了配置管理上。公司有个动态配置系统,配置频繁更新。我用WeakValueDictionary缓存配置对象,配置不再被使用就自动清除。新配置来的时候重新加载,不用操心清理旧缓存。代码简洁多了。
最后说一句,读懂弱引用的关键在于理解Python的内存管理机制。Python用引用计数管理内存,每个对象被引用就计数加一,引用没了就减一。计数归零就回收。强引用会让计数一直保持正数,弱引用不计数。搞懂这个,你就明白什么时候该用弱引用了。