Python基础:内存管理与引用计数的简单理解 Python基础内存管理与引用计数的简单理解一、开篇Python帮你管理内存但你得懂规则C语言中你需要手动malloc和freeJava有复杂的垃圾回收器而Python呢Python使用引用计数为主的自动内存管理——大多数时候你不需要操心内存但理解它背后的机制能帮你写出更高效的代码也能让你在遇到内存不释放的问题时有排查方向。⌨️ 先来感受几个现象# 现象1小整数的永生a256b256print(aisb)# True —— 为什么a257b257print(aisb)# False —— 为什么变了# 现象2列表赋值不复制list_a[1,2,3]list_blist_a list_b.append(4)print(list_a)# [1, 2, 3, 4] —— 为什么list_a也变了# 现象3del不会立即删除x[1,2,3]yxdelxprint(y)# [1, 2, 3] —— 为什么还能访问 这些现象的背后都指向同一个核心概念——引用计数。今天我们就来揭开Python内存管理的面纱。二、引用计数Python内存管理的核心2.1 什么是引用计数Python中的每个对象都有一个引用计数器记录着当前有多少个变量名称引用了它。当引用计数降为0时对象的内存就会被释放。# 用图理解引用计数## 创建对象 → 引用计数 1# x [1, 2, 3] # x → [1, 2, 3] (refcount1)## 增加引用 → 引用计数 1# y x # x → [1, 2, 3] ← y (refcount2)## 删除引用 → 引用计数 -1# del x # y → [1, 2, 3] (refcount1)## 最后一个引用被删除 → 引用计数 0 → 对象被销毁# del y # [1, 2, 3] 被回收# 用代码和sys.getrefcount()观察importsys# 创建一个列表data[1,2,3]print(f创建后:{sys.getrefcount(data)})# 2data getrefcount的参数# 添加一个引用aliasdataprint(f添加引用后:{sys.getrefcount(data)})# 3# 删除一个引用delaliasprint(f删除引用后:{sys.getrefcount(data)})# 2# ⚠️ getrefcount()本身也会增加一个临时引用# 所以显示的数字比实际多12.2 引用计数的增减规则# 增加引用计数的情况 # 1. 赋值操作x[1,2,3]# 创建refcount1yx# 新引用refcount1# 2. 函数传参deffunc(param):print(sys.getrefcount(param))func(x)# 传参时refcount2param getrefcount的参数# 3. 放入容器container[]container.append(x)# 容器中的引用refcount1# 4. 作为属性classMyClass:passobjMyClass()obj.datax# 属性引用refcount1# 减少引用计数的情况 # 1. del语句dely# refcount-1# 2. 变量重新赋值xnew value# x原来引用的[1,2,3]的refcount-1# 3. 引用离开作用域defcreate_list():temp[1,2,3]# 函数结束时temp被销毁refcount-1returntemp# 4. 从容器中删除container.remove(container[0])# refcount-12.3 引用计数的重要特征# 特征1引用计数为0时对象立即被销毁# (在CPython中是这样的其他Python实现可能不同)classWatchedObject:def__del__(self):print(f对象{id(self)}被销毁了)defdemo():objWatchedObject()print(函数内...)# 函数结束obj的引用计数变为0print(调用前)demo()print(调用后)# 输出# 调用前# 函数内...# 对象 xxxxx 被销毁了# 调用后# 特征2del 不直接删除对象只删除引用a[1,2,3]badela# 只删除a这个引用print(b)# [1, 2, 3] —— 对象还在# 特征3引用计数无法处理循环引用# 这是引用计数最大的缺陷classNode:def__init__(self,name):self.namename self.refNonedef__del__(self):print(fNode({self.name}) 被销毁)aNode(A)bNode(B)a.refb# a引用bb.refa# b引用a —— 循环引用deladelb# ⚠️ 两个Node都不会被销毁它们互相引用引用计数永远不为0# Python使用垃圾回收器GC来处理这种情况三、小整数缓存与字符串驻留3.1 小整数缓存-5到256# Python启动时预创建了 -5 到 256 之间的所有整数# 对这些整数的引用总是返回同一个对象# 在缓存范围内a100b100print(aisb)# True —— 同一个对象a256b256print(aisb)# True# 超出缓存范围a257b257print(aisb)# False —— 创建了两个不同的对象a-6b-6print(aisb)# False —— 超出缓存范围# 为什么缓存# 小整数使用非常频繁缓存可以避免重复创建对象节省内存和时间# ⚠️ 注意不要依赖is来比较整数值# 总是用 比较值print(10001000)# True —— ✅print(1000is1000)# 可能是False取决于Python实现3.2 字符串驻留Interning# Python也会驻留某些字符串# 驻留 相同内容的字符串复用同一个对象# 看起来像标识符的字符串会被自动驻留ahellobhelloprint(aisb)# True —— 驻留了ahello_worldbhello_worldprint(aisb)# True —— 像标识符的都被驻留# 但包含特殊字符的可能不会ahello world!bhello world!print(aisb)# 可能是True也可能是False取决于Python实现ahello world!#bhello world!#print(aisb)# 通常为False# 手动驻留importsys asys.intern(hello world!#)bsys.intern(hello world!#)print(aisb)# True —— 手动驻留保证是同一个对象# ⚠️ 同样的不要依赖is来比较字符串值# 总是用 比较字符串3.3 其他缓存机制# None、True、False 是单例aNonebNoneprint(aisb)# True —— 全局只有一个None# 空元组也被缓存a()b()print(aisb)# True# 短字符串长度0或1通常被缓存axbxprint(aisb)# True# 编译时常量被优化ahello worldbhello worldprint(aisb)# True —— 编译器做了常量折叠四、垃圾回收器GC4.1 为什么需要GC# 引用计数无法处理循环引用# Python的GC专门用来解决这个问题importgcclassCyclicNode:def__init__(self,name):self.namename self.otherNone# 创建循环引用aCyclicNode(A)bCyclicNode(B)a.otherb b.othera# 此时引用计数a1(external), b1(external)1(from a)2# 删除外部引用deladelb# 引用计数a1(from b), b1(from a)# 没有外部引用能访问到它们但因为循环引用计数不为0# GC会检测这种循环引用并回收print(手动触发GC前:)print(f 垃圾对象数:{len(gc.garbage)})# 手动触发垃圾回收collectedgc.collect()print(f 回收了{collected}个对象)4.2 GC的分代回收# Python的GC使用分代回收策略# 对象分为3代generation 0, 1, 2# 新对象在0代活得越久代数越高# GC频率0代 1代 2代importgcprint(fGC阈值:{gc.get_threshold()})# 默认: (700, 10, 10)# 含义:# 0代对象数超过700 → 触发0代GC# 每10次0代GC → 触发1次1代GC# 每10次1代GC → 触发1次2代GC# 查看各代对象数print(f各代对象数:{gc.get_count()})# 调整GC阈值# gc.set_threshold(1000, 15, 15)# 禁用自动GC不推荐除非有特殊需求# gc.disable()# 重新启用# gc.enable()五、内存管理的最佳实践5.1 避免循环引用# ❌ 循环引用classParent:def__init__(self):self.children[]defadd_child(self,child):self.children.append(child)child.parentself# 循环引用classChild:def__init__(self):self.parentNone# ✅ 使用弱引用importweakrefclassParentGood:def__init__(self):self.children[]defadd_child(self,child):self.children.append(child)child.parentweakref.ref(self)# 弱引用不影响引用计数classChildGood:def__init__(self):self.parentNone# 使用弱引用后即使Parent被删除Child也不会阻止GCpParentGood()cChildGood()p.add_child(c)delp# c的parent现在是一个死的弱引用print(c.parent())# None5.2 使用上下文管理器# ✅ 用with语句确保资源正确释放# 文件、网络连接、锁等需要及时释放的资源# 文件操作withopen(data.txt,r)asf:contentf.read()# 离开with块后文件自动关闭# 自定义上下文管理器classManagedResource:def__init__(self,name):self.namenamedef__enter__(self):print(f 获取资源:{self.name})returnselfdef__exit__(self,*args):print(f 释放资源:{self.name})withManagedResource(数据库连接)asres:print(f 使用{res.name})# 输出# 获取资源: 数据库连接# 使用 数据库连接# 释放资源: 数据库连接5.3 大对象的处理# 处理大对象时的内存意识# 1. 使用生成器而不是列表# ❌ 一次性加载所有数据defread_all_lines(filename):withopen(filename)asf:returnf.readlines()# 全部读入内存# ✅ 逐行处理defprocess_lines(filename):withopen(filename)asf:forlineinf:# 每次只读一行yieldline.strip()# 2. 显式删除不再需要的大对象defprocess_large_data():data[0]*10_000_000# 大对象resultsum(data)# data不再需要deldata# 显式删除让内存尽早释放returnresult# 3. 使用__slots__减少实例内存classRegularUser:def__init__(self,name,age,email):self.namename self.ageage self.emailemail# 每个实例有一个__dict__占用额外内存classSlimUser:__slots__[name,age,email]def__init__(self,name,age,email):self.namename self.ageage self.emailemail# 没有__dict__内存占用少得多六、实战案例6.1 内存使用监控装饰器importsysimporttracemallocfromfunctoolsimportwrapsdeftrack_memory(func):追踪函数内存使用的装饰器wraps(func)defwrapper(*args,**kwargs):tracemalloc.start()snapshot_beforetracemalloc.take_snapshot()resultfunc(*args,**kwargs)snapshot_aftertracemalloc.take_snapshot()tracemalloc.stop()statssnapshot_after.compare_to(snapshot_before,lineno)print(f\n{func.__name__}内存统计:)print(f 新分配:{sum(s.size_diffforsinstatsifs.size_diff0)/1024:.1f}KB)print(f 释放:{sum(abs(s.size_diff)forsinstatsifs.size_diff0)/1024:.1f}KB)returnresultreturnwrappertrack_memorydefcreate_large_list(n):创建大列表并返回data[i**2foriinrange(n)]returnsum(data)# create_large_list(100000)6.2 对象生命周期追踪器importweakrefclassLifecycleTracker:追踪对象的创建和销毁_instances{}_creation_count0_destruction_count0def__init__(self,name):self.namename LifecycleTracker._creation_count1LifecycleTracker._instances[name]weakref.ref(self,lambdaref:LifecycleTracker._on_destroy(name))print(f 创建:{name}(第{LifecycleTracker._creation_count}个))classmethoddef_on_destroy(cls,name):cls._destruction_count1print(f 销毁:{name}(第{cls._destruction_count}个))classmethoddefstats(cls):return{created:cls._creation_count,destroyed:cls._destruction_count,alive:cls._creation_count-cls._destruction_count,}# 使用aLifecycleTracker(对象A)bLifecycleTracker(对象B)cLifecycleTracker(对象C)print(f\n当前状态:{LifecycleTracker.stats()})delbprint(f删除B后:{LifecycleTracker.stats()})dela,cimportgc;gc.collect()# 强制GCprint(f全部删除后:{LifecycleTracker.stats()})七、本章小结✅ 本文我们学习了Python内存管理的核心知识引用计数每个对象都有一个引用计数器。引用数降为0时对象立即被释放CPython。赋值、传参、放入容器会增加引用del、重新赋值、离开作用域会减少引用。缓存机制小整数-5~256、短字符串、None/True/False被缓存复用。理解缓存可以避免对is的误用。垃圾回收器处理引用计数无法解决的循环引用问题。使用分代回收策略0代→1代→2代。最佳实践使用弱引用weakref避免循环引用使用with语句确保资源释放用生成器替代大列表减少内存占用使用__slots__减少实例内存开销Python的内存管理相对自动化但理解引用计数能帮你写出更高效、更易调试的代码。记住核心原则当没有变量引用一个对象时它就会被回收。⌨️ 下一篇文章我们将学习Python函数的定义与调用——进入函数编程的世界