【Python】内存泄漏2

好的,我们继续深入Python内存泄漏的预防策略。


# ... existing code ...
    def __del__(self): # 析构函数
        print(f"TemporaryBigData '{
     
     self.obj_id}' deleted.") # 打印删除信息

def process_data_locally(data_id): # 定义一个函数,用于局部处理数据
    print(f"\nEntering process_data_locally for ID: {
     
     data_id}") # 打印进入函数信息
    temp_obj = TemporaryBigData(f"local_{
     
     data_id}") # 在函数内部创建TemporaryBigData实例,作为局部变量
    print(f"Ref count for '{
     
     temp_obj.obj_id}' inside function: {
     
     sys.getrefcount(temp_obj) - 1}") # 打印对象引用计数,减去getrefcount自身增加的引用

    # 模拟一些数据处理
    for _ in range(5): # 循环5次
        _ = temp_obj.payload[0] # 访问数据,模拟使用
    
    print(f"Exiting process_data_locally for ID: {
     
     data_id}") # 打印退出函数信息
    # temp_obj 在这里超出作用域,其引用计数会自然减少到0,对象将被回收

def demonstrate_local_variable_release(): # 演示局部变量释放的函数
    print("\n--- Demonstrating Local Variable Release ---") # 打印演示开始信息
    
    gc.collect() # 手动触发一次垃圾回收,确保环境干净
    print("Initial memory state (after GC collect):") # 打印初始内存状态信息
    initial_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     initial_memory}") # 打印已分配的内存块数量

    process_data_locally(1) # 调用函数处理数据,局部变量将在函数结束后释放
    gc.collect() # 再次手动触发垃圾回收,看是否回收了
    print("\nAfter first local processing:") # 打印第一次局部处理后的信息
    current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     current_memory} (Change: {
     
     current_memory - initial_memory})") # 打印已分配的内存块数量及变化

    process_data_locally(2) # 再次调用函数处理数据
    gc.collect() # 再次手动触发垃圾回收
    print("\nAfter second local processing:") # 打印第二次局部处理后的信息
    current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     current_memory} (Change: {
     
     current_memory - initial_memory})") # 打印已分配的内存块数量及变化

    print("\n--- Local Variable Release Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# demonstrate_local_variable_release() # 调用演示函数,取消注释即可运行

解析与思考:
在上述 demonstrate_local_variable_release 示例中,TemporaryBigData 实例 temp_objprocess_data_locally 函数内部创建,作为局部变量。一旦 process_data_locally 函数执行完毕,temp_obj 就不再有任何可达的引用,其引用计数会降为零,并立即被引用计数机制回收。即使不手动调用 gc.collect(),这些对象也会在函数返回后迅速被标记为可回收。通过观察 sys.getallocatedblocks() 的变化,我们可以看到内存占用在每次函数调用结束后恢复到接近初始状态,这正是局部变量有效管理内存的体现。

2. 全局变量的陷阱与谨慎使用

与局部变量不同,全局变量的生命周期与程序的生命周期相同。一旦一个对象被全局变量引用,它将一直存在于内存中,直到程序终止或者该全局变量被显式地解除引用(例如,赋值为 None)。不恰当地使用全局变量,尤其是用来存储临时或大量数据时,极易导致内存泄漏。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class PersistentBigData: # 持久大数据类
    def __init__(self, obj_id): # 构造函数
        self.obj_id = obj_id # 对象ID
        self.payload = bytearray(1024 * 100) # 模拟100KB的数据
        print(f"PersistentBigData '{
     
     self.obj_id}' created.") # 打印创建信息
    
    def __del__(self): # 析构函数
        print(f"PersistentBigData '{
     
     self.obj_id}' deleted.") # 打印删除信息

global_storage = [] # 定义一个全局列表,用于存储PersistentBigData实例

def add_to_global_storage(data_id): # 定义一个函数,将数据添加到全局存储中
    print(f"\nEntering add_to_global_storage for ID: {
     
     data_id}") # 打印进入函数信息
    new_obj = PersistentBigData(f"global_{
     
     data_id}") # 创建PersistentBigData实例
    global_storage.append(new_obj) # 将新创建的对象添加到全局列表,这会增加对象的引用计数
    print(f"Ref count for '{
     
     new_obj.obj_id}' after adding to global_storage: {
     
     sys.getrefcount(new_obj) - 1}") # 打印对象引用计数
    print(f"Exiting add_to_global_storage for ID: {
     
     data_id}") # 打印退出函数信息

def demonstrate_global_variable_leak(): # 演示全局变量导致泄漏的函数
    print("\n--- Demonstrating Global Variable Leak ---") # 打印演示开始信息
    
    gc.collect() # 手动触发一次垃圾回收
    print("Initial memory state (after GC collect):") # 打印初始内存状态信息
    initial_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     initial_memory}") # 打印已分配的内存块数量

    add_to_global_storage(1) # 添加第一个大数据对象
    gc.collect() # 手动触发垃圾回收
    print("\nAfter first global addition:") # 打印第一次全局添加后的信息
    current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     current_memory} (Change: {
     
     current_memory - initial_memory})") # 打印已分配的内存块数量及变化

    add_to_global_storage(2) # 添加第二个大数据对象
    gc.collect() # 手动触发垃圾回收
    print("\nAfter second global addition:") # 打印第二次全局添加后的信息
    current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     current_memory} (Change: {
     
     current_memory - initial_memory})") # 打印已分配的内存块数量及变化

    print("\n--- Global Variable Leak Demonstration Complete ---") # 打印演示完成信息
    print("WARNING: Objects in 'global_storage' are still alive and consuming memory.") # 警告信息:对象仍在消耗内存
    # 为了避免实际的内存泄漏,我们手动清空全局列表
    # print("\nManually clearing global_storage to release memory...") # 打印手动清空信息
    # global_storage.clear() # 清空列表,解除对对象的引用
    # gc.collect() # 再次手动触发垃圾回收
    # print("After clearing global_storage and GC collect:") # 打印清空后信息
    # current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    # print(f"Allocated blocks: {current_memory} (Change: {current_memory - initial_memory})") # 打印已分配的内存块数量及变化

# 调用演示函数
# demonstrate_global_variable_leak() # 调用演示函数,取消注释即可运行

解析与思考:
demonstrate_global_variable_leak 示例中,PersistentBigData 实例被添加到 global_storage 这个全局列表中。只要 global_storage 存在,它就会持有对这些对象的强引用。即使 add_to_global_storage 函数执行完毕,这些对象也不会被回收,因为它们的引用计数不会降为零。这导致内存占用持续增加,形成典型的内存泄漏。
最佳实践是,如果确实需要使用全局存储,务必确保在不再需要这些数据时,显式地将其从全局容器中移除或将全局变量赋值为 None,以便引用计数能降为零,允许垃圾回收器回收内存。例如,通过 global_storage.clear()del global_storage[:] 来清空列表,或者直接 global_storage = None

3. 闭包(Closures)中的变量捕获与生命周期

闭包是Python中一个强大的特性,它允许内部函数记住并访问其外部(封闭)作用域中的变量,即使外部函数已经执行完毕。然而,这种“记忆”机制如果处理不当,也可能导致内存泄漏。如果闭包捕获了一个大对象,并且闭包本身被长期持有(例如,作为另一个对象的属性、存储在全局列表中、作为回调函数等),那么被捕获的大对象也将无法被释放。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class DataContainer: # 数据容器类
    def __init__(self, data_id, size_mb=1): # 构造函数,初始化数据ID和数据大小
        self.data_id = data_id # 数据ID
        self.big_data = bytearray(1024 * 1024 * size_mb) # 创建一个大型字节数组,模拟大数据
        print(f"DataContainer '{
     
     self.data_id}' created with {
     
     size_mb}MB data.") # 打印创建信息
    
    def __del__(self): # 析构函数
        print(f"DataContainer '{
     
     self.data_id}' deleted.") # 打印删除信息

# 全局列表,用于存储可能导致泄漏的闭包
leaky_closures = [] # 定义一个全局列表,用于存储可能导致泄漏的闭包

def create_processing_closure(container_id, size): # 创建一个处理闭包的函数
    data_obj = DataContainer(f"captured_data_{
     
     container_id}", size) # 创建一个DataContainer实例,这个实例会被闭包捕获

    def process_data_in_closure(): # 定义内部函数,作为闭包
        print(f"Processing data from captured container '{
     
     data_obj.data_id}'...") # 打印处理信息
        # 模拟对捕获数据的操作
        _ = data_obj.big_data[0] # 访问数据

    print(f"Closure for '{
     
     data_obj.data_id}' created.") # 打印闭包创建信息
    return process_data_in_closure # 返回内部函数(闭包)

def demonstrate_closure_leak(): # 演示闭包导致泄漏的函数
    print("\n--- Demonstrating Closure Leak ---") # 打印演示开始信息
    
    gc.collect() # 手动触发一次垃圾回收
    print("Initial memory state (after GC collect):") # 打印初始内存状态信息
    initial_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     initial_memory}") # 打印已分配的内存块数量

    print("\nCreating and storing 3 leaky closures...") # 打印创建并存储闭包的信息
    for i in range(3): # 循环3次
        closure = create_processing_closure(i + 1, 10) # 创建闭包,每个闭包捕获一个10MB的DataContainer
        leaky_closures.append(closure) # 将闭包添加到全局列表中

    gc.collect() # 手动触发垃圾回收
    print("\nAfter creating and storing closures:") # 打印创建并存储闭包后的信息
    current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     current_memory} (Change: {
     
     current_memory - initial_memory})") # 打印已分配的内存块数量及变化
    print("Memory has likely increased significantly due to captured data.") # 提示内存显著增加

    # 即使我们不再直接调用 create_processing_closure,但由于闭包被 leaky_closures 持有,
    # 它们捕获的 DataContainer 对象也无法被回收。
    print("\nAttempting to clear leaky_closures to release memory...") # 打印尝试清空闭包的信息
    leaky_closures.clear() # 清空列表,解除对闭包的引用
    gc.collect() # 再次手动触发垃圾回收

    print("\nAfter clearing leaky_closures and GC collect:") # 打印清空后信息
    current_memory = sys.getallocatedblocks() # 获取当前已分配的内存块数量
    print(f"Allocated blocks: {
     
     current_memory} (Change: {
     
     current_memory - initial_memory})") # 打印已分配的内存块数量及变化
    print("Memory should now be closer to initial state, as captured objects are released.") # 提示内存恢复

    print("\n--- Closure Leak Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# demonstrate_closure_leak() # 调用演示函数,取消注释即可运行

解析与思考:
demonstrate_closure_leak 示例中,create_processing_closure 函数内部创建了一个 DataContainer 对象 data_obj。然后,它定义并返回了一个内部函数 process_data_in_closure,这个内部函数(闭包)捕获了 data_obj。当我们将这些闭包存储到全局列表 leaky_closures 中时,即使 create_processing_closure 函数执行完毕,被捕获的 data_obj 实例也不会被回收,因为闭包持续持有对它的引用。只有当 leaky_closures 列表被清空,从而解除对闭包的引用,进而解除闭包对 data_obj 的引用时,data_obj 才能被垃圾回收。
预防策略是:

  • 审慎使用闭包捕获大对象:如果闭包需要访问外部作用域的变量,尽量避免直接捕获整个大对象,而是考虑只捕获必要的少量数据。
  • 管理闭包的生命周期:确保闭包不会被不必要地长期持有。如果闭包是临时性的,确保它在不再需要时能被解除引用。

4.1.2 及时解除对不再需要对象的引用

除了作用域管理,显式地解除对不再需要对象的引用是避免内存泄漏的直接有效手段,尤其是在处理大型数据结构或长期运行的服务中。

1. 将变量赋值为 None

将变量赋值为 None 会使其不再引用之前的对象,从而降低该对象的引用计数。当引用计数降至零时,对象即可被回收。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class VolatileData: # 可变数据类
    def __init__(self, obj_id, size_kb=500): # 构造函数
        self.obj_id = obj_id # 对象ID
        self.data = bytearray(1024 * size_kb) # 模拟500KB数据
        print(f"VolatileData '{
     
     self.obj_id}' created.") # 打印创建信息
    
    def __del__(self): # 析构函数
        print(f"VolatileData '{
     
     self.obj_id}' deleted.") # 打印删除信息

def process_and_release(): # 处理并释放的函数
    print("\n--- Demonstrating Explicit Release ---") # 打印演示开始信息
    
    gc.collect() # 手动触发一次垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    # 创建一个大数据对象
    big_object = VolatileData("temp_data_1") # 创建一个VolatileData实例
    print(f"Ref count for '{
     
     big_object.obj_id}' after creation: {
     
     sys.getrefcount(big_object) - 1}") # 打印引用计数

    # 模拟一些操作
    for _ in range(10): # 循环10次
        _ = big_object.data[0] # 访问数据

    print("Data processing complete.") # 打印数据处理完成信息

    # 显式解除引用
    del big_object # 删除big_object变量,降低对象引用计数
    # 或者 big_object = None # 将big_object赋值为None,同样降低引用计数
    print("Explicitly 'del'eted big_object variable.") # 打印已删除变量信息

    gc.collect() # 手动触发垃圾回收
    final_blocks = sys.getallocatedblocks() # 获取最终内存块数量
    print(f"Final allocated blocks: {
     
     final_blocks} (Change: {
     
     final_blocks - initial_blocks})") # 打印最终内存块数量及变化
    print("Memory should have been released.") # 提示内存已释放
    print("\n--- Explicit Release Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# process_and_release() # 调用演示函数,取消注释即可运行

解析与思考:
process_and_release 示例中,big_object 被创建并使用。在完成其任务后,通过 del big_object(或 big_object = None),我们显式地解除了对该对象的引用。这会使 big_object 的引用计数降为零,从而允许Python的引用计数机制立即回收其占用的内存(除非存在循环引用,但这里是单对象)。这种方法在处理短期内需要大量内存但之后不再需要的场景中非常有效。

2. 清空大型集合(列表、字典、集合)

当大型列表、字典或集合不再需要时,仅仅将变量赋值为 None 不足以释放其内部所有元素的内存,因为集合本身会持有对这些元素的引用。需要清空集合来解除其对元素的引用。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class SmallObject: # 小对象类
    def __init__(self, obj_id): # 构造函数
        self.obj_id = obj_id # 对象ID
    def __del__(self): # 析构函数
        print(f"SmallObject '{
     
     self.obj_id}' deleted.") # 打印删除信息

def demonstrate_collection_clearing(): # 演示集合清空的函数
    print("\n--- Demonstrating Collection Clearing ---") # 打印演示开始信息
    
    gc.collect() # 手动触发垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    print("\nCreating a large list of objects...") # 打印创建大型列表信息
    large_list = [] # 创建一个空列表
    for i in range(100_000): # 循环10万次
        large_list.append(SmallObject(i)) # 向列表中添加10万个SmallObject实例

    print(f"List size: {
     
     len(large_list)}") # 打印列表大小
    current_blocks_after_creation = sys.getallocatedblocks() # 获取创建后内存块数量
    print(f"Allocated blocks after creation: {
     
     current_blocks_after_creation} (Change: {
     
     current_blocks_after_creation - initial_blocks})") # 打印创建后内存块数量及变化

    # 模拟对列表的使用
    _ = large_list[0] # 访问列表第一个元素

    print("\nClearing the large list...") # 打印清空大型列表信息
    large_list.clear() # 清空列表,解除对所有元素的引用
    # 或者 large_list = [] # 重新创建一个空列表,原列表和其内容将被回收
    # 或者 del large_list[:] # 删除列表所有切片元素

    gc.collect() # 手动触发垃圾回收
    final_blocks = sys.getallocatedblocks() # 获取最终内存块数量
    print(f"Final allocated blocks: {
     
     final_blocks} (Change: {
     
     final_blocks - initial_blocks})") # 打印最终内存块数量及变化
    print("Memory for list elements should have been released.") # 提示列表元素内存已释放
    print("\n--- Collection Clearing Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# demonstrate_collection_clearing() # 调用演示函数,取消注释即可运行

解析与思考:
demonstrate_collection_clearing 示例中,我们创建了一个包含10万个 SmallObject 实例的大型列表。仅仅让 large_list 变量超出作用域(如果它是个局部变量)或者赋值为 None(如果它是全局变量),并不会立即回收列表内部的 SmallObject 实例,因为列表对象本身需要时间被回收,并且是列表持有了这些对象的引用。通过 large_list.clear(),我们显式地让列表解除对其所有元素的引用,使得这些 SmallObject 的引用计数降为零,从而可以被立即回收。这对于那些需要临时存储大量数据的列表、字典或集合来说,是至关重要的内存管理技巧。

3. 使用 del 语句删除属性或字典项

与清空整个集合类似,当对象的某个属性或字典中的某个键值对存储了大量数据且不再需要时,可以通过 del 语句将其删除,从而解除引用。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class LargePayload: # 大负载类
    def __init__(self, identifier, size_mb=10): # 构造函数
        self.identifier = identifier # 标识符
        self.data = bytearray(1024 * 1024 * size_mb) # 模拟10MB数据
        print(f"LargePayload '{
     
     self.identifier}' created.") # 打印创建信息
    
    def __del__(self): # 析构函数
        print(f"LargePayload '{
     
     self.identifier}' deleted.") # 打印删除信息

class Processor: # 处理器类
    def __init__(self, name): # 构造函数
        self.name = name # 名称
        self.cache = {
   
   } # 初始化一个字典作为缓存
        print(f"Processor '{
     
     self.name}' initialized.") # 打印初始化信息

    def load_data(self, key, size): # 加载数据方法
        print(f"Loading data '{
     
     key}' into processor cache...") # 打印加载数据信息
        self.cache[key] = LargePayload(key, size) # 将LargePayload实例作为字典值存储
        print(f"Data '{
     
     key}' loaded. Current cache size: {
     
     len(self.cache)}") # 打印加载完成信息

    def process_item(self, key): # 处理项目方法
        if key in self.cache: # 如果键存在于缓存中
            print(f"Processing item '{
     
     key}' from cache.") # 打印处理信息
            _ = self.cache[key].data[0] # 访问缓存数据
            return True # 返回True
        print(f"Item '{
     
     key}' not found in cache.") # 打印未找到信息
        return False # 返回False

    def unload_data(self, key): # 卸载数据方法
        if key in self.cache: # 如果键存在于缓存中
            print(f"Unloading data '{
     
     key}' from processor cache...") # 打印卸载数据信息
            del self.cache[key] # 从字典中删除键值对,解除对LargePayload实例的引用
            print(f"Data '{
     
     key}' unloaded. Current cache size: {
     
     len(self.cache)}") # 打印卸载完成信息
            return True # 返回True
        print(f"Data '{
     
     key}' not in cache, nothing to unload.") # 打印未卸载信息
        return False # 返回False

def demonstrate_attribute_or_dict_item_release(): # 演示属性或字典项释放的函数
    print("\n--- Demonstrating Attribute/Dict Item Release ---") # 打印演示开始信息
    
    gc.collect() # 手动触发垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    processor_instance = Processor("MainProcessor") # 创建一个Processor实例

    # 加载多个大数据负载
    processor_instance.load_data("report_A", 10) # 加载10MB数据
    processor_instance.load_data("image_B", 20) # 加载20MB数据
    processor_instance.load_data("model_C", 5) # 加载5MB数据

    gc.collect() # 手动触发垃圾回收
    current_blocks_after_load = sys.getallocatedblocks() # 获取加载后内存块数量
    print(f"\nAllocated blocks after loading data: {
     
     current_blocks_after_load} (Change: {
     
     current_blocks_after_load - initial_blocks})") # 打印加载后内存块数量及变化

    # 卸载不再需要的单个大数据负载
    processor_instance.unload_data("image_B") # 卸载20MB数据

    gc.collect() # 手动触发垃圾回收
    current_blocks_after_unload = sys.getallocatedblocks() # 获取卸载后内存块数量
    print(f"\nAllocated blocks after unloading 'image_B': {
     
     current_blocks_after_unload} (Change: {
     
     current_blocks_after_unload - initial_blocks})") # 打印卸载后内存块数量及变化
    print("Memory for 'image_B' should have been released.") # 提示内存已释放

    # 确保其他数据仍然存在并可访问
    processor_instance.process_item("report_A") # 处理数据A
    processor_instance.process_item("model_C") # 处理数据C

    # 清理所有剩余缓存
    print("\nClearing remaining cache...") # 打印清空剩余缓存信息
    processor_instance.cache.clear() # 清空缓存字典
    gc.collect() # 手动触发垃圾回收
    final_blocks = sys.getallocatedblocks() # 获取最终内存块数量
    print(f"Final allocated blocks: {
     
     final_blocks} (Change: {
     
     final_blocks - initial_blocks})") # 打印最终内存块数量及变化
    print("All cache memory should be released.") # 提示所有缓存内存已释放

    print("\n--- Attribute/Dict Item Release Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# demonstrate_attribute_or_dict_item_release() # 调用演示函数,取消注释即可运行

解析与思考:
demonstrate_attribute_or_dict_item_release 示例中,Processor 类的 cache 字典作为其属性,用于存储多个 LargePayload 实例。当某个特定的数据(例如 image_B)不再需要时,通过 del self.cache[key] 我们可以精确地从字典中移除该条目,从而解除 cacheimage_B 对应的 LargePayload 实例的引用。这使得 image_BLargePayload 实例的引用计数降为零并被回收,而其他缓存中的数据不受影响。这种细粒度的内存管理对于那些需要动态加载和卸载大型数据集的应用程序至关重要。

4.1.3 使用上下文管理器(with 语句)管理资源

Python的上下文管理器(Context Managers)和 with 语句是处理需要显式资源清理(如文件句柄、网络连接、锁、甚至是大内存对象)的场景的强大工具。它们确保资源在代码块执行完毕后(无论是否发生异常)被正确地获取和释放。

1. 文件操作中的内存安全

最常见的 with 语句用法是文件操作,它确保文件句柄在操作完成后被关闭,即使在读写过程中发生错误。这不仅避免了文件句柄泄漏,也间接防止了因文件内容过大而导致内存持续增长的问题(如果文件内容被完全读入内存且未释放)。

# python
import sys # 导入sys模块
import os # 导入os模块

def process_large_file_safely(file_path): # 安全处理大文件的函数
    print(f"\n--- Processing large file: {
     
     file_path} ---") # 打印处理文件信息
    total_lines = 0 # 初始化总行数
    try: # 尝试执行
        with open(file_path, 'r', encoding='utf-8') as f: # 使用with语句打开文件,确保文件自动关闭
            for line_num, line in enumerate(f): # 遍历文件每一行
                # 模拟处理每一行,不将整个文件加载到内存
                if line_num % 100000 == 0: # 每10万行打印一次进度
                    print(f"Processing line {
     
     line_num}...") # 打印处理进度
                total_lines += 1 # 行数加1
                # 假设这里不对行内容做持久化存储,只做瞬时处理
        print(f"Finished processing. Total lines: {
     
     total_lines}") # 打印处理完成信息
    except FileNotFoundError: # 捕获文件未找到异常
        print(f"Error: File '{
     
     file_path}' not found.") # 打印错误信息
    except Exception as e: # 捕获其他异常
        print(f"An error occurred: {
     
     e}") # 打印错误信息
    finally: # 最终执行
        print(f"File handle for '{
     
     file_path}' is guaranteed to be closed.") # 提示文件句柄已关闭
    print(f"--- Finished processing large file: {
     
     file_path} ---") # 打印处理完成信息

# 创建一个大型模拟文件
def create_dummy_large_file(file_path, num_lines, line_size_bytes): # 创建模拟大文件的函数
    print(f"Creating dummy file '{
     
     file_path}' with {
     
     num_lines} lines...") # 打印创建文件信息
    with open(file_path, 'w', encoding='utf-8') as f: # 使用with语句打开文件
        for i in range(num_lines): # 循环行数
            f.write("A" * line_size_bytes + "\n") # 写入指定大小的字符串和换行符
    print("Dummy file created.") # 打印文件创建完成信息

# 示例:创建并安全处理一个大文件
# dummy_file = "large_data_file.txt" # 定义文件路径
# create_dummy_large_file(dummy_file, 1_000_000, 100) # 创建一个100MB的模拟文件 (1M行 * 100字节/行)
# process_large_file_safely(dummy_file) # 安全处理大文件
# os.remove(dummy_file) # 清理:删除模拟文件

解析与思考:
process_large_file_safely 示例中,通过 with open(...) as f: 结构,我们确保了文件 f 在代码块结束时自动关闭,无论是否发生异常。更重要的是,我们逐行读取文件 (for line_num, line in enumerate(f):),而不是一次性将整个文件内容读入内存。这对于处理超大文件至关重要,它避免了将整个文件内容作为单个大对象长期驻留在内存中,从而有效地防止了潜在的内存泄漏。即使是每一行,在处理完毕后也会被Python回收,因为 line 变量是局部作用域的。

2. 自定义上下文管理器管理对象生命周期

对于自定义的、需要特殊初始化和清理逻辑的内存密集型对象,我们可以实现自己的上下文管理器。通过定义 __enter____exit__ 方法,我们可以精确控制资源的获取和释放。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class MemoryResource: # 内存资源类
    def __init__(self, name, size_mb): # 构造函数
        self.name = name # 名称
        self.size_mb = size_mb # 大小(MB)
        self.data = None # 初始化数据为None
        print(f"MemoryResource '{
     
     self.name}' (uninitialized) created.") # 打印创建信息

    def __enter__(self): # 进入上下文方法
        print(f"Entering context for MemoryResource '{
     
     self.name}'. Allocating {
     
     self.size_mb}MB data...") # 打印进入上下文信息
        self.data = bytearray(1024 * 1024 * self.size_mb) # 在进入时分配大数据
        print(f"MemoryResource '{
     
     self.name}' data allocated.") # 打印数据分配信息
        return self # 返回自身实例

    def __exit__(self, exc_type, exc_val, exc_tb): # 退出上下文方法
        print(f"Exiting context for MemoryResource '{
     
     self.name}'. Deallocating data...") # 打印退出上下文信息
        del self.data # 删除数据属性,解除对大数据的引用
        self.data = None # 将数据设置为None,明确释放
        print(f"MemoryResource '{
     
     self.name}' data deallocated.") # 打印数据解除分配信息
        
        # 强制垃圾回收,以便我们能看到内存变化
        gc.collect() # 手动触发垃圾回收
        if exc_type: # 如果发生异常
            print(f"Exception occurred in context: {
     
     exc_type.__name__}: {
     
     exc_val}") # 打印异常信息
        return False # 返回False表示不抑制异常(如果发生)

def demonstrate_custom_context_manager(): # 演示自定义上下文管理器的函数
    print("\n--- Demonstrating Custom Context Manager for Memory ---") # 打印演示开始信息
    
    gc.collect() # 手动触发垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    print("\nCreating a MemoryResource instance and entering its context...") # 打印创建实例并进入上下文信息
    with MemoryResource("TemporaryBuffer", 50) as buffer: # 使用with语句创建和管理MemoryResource实例(50MB)
        print(f"Inside context for '{
     
     buffer.name}'. Data size: {
     
     len(buffer.data) / (1024*1024)} MB.") # 打印上下文内部信息
        # 模拟对缓冲数据的操作
        for _ in range(5): # 循环5次
            _ = buffer.data[0] # 访问数据

        print("Simulating some operations within the context.") # 打印模拟操作信息
        # 即使在这里发生异常,__exit__也会被调用

    # 当with块结束时,__exit__方法会自动执行,确保资源被释放
    print("\nExited context for 'TemporaryBuffer'.") # 打印退出上下文信息
    final_blocks = sys.getallocatedblocks() # 获取最终内存块数量
    print(f"Final allocated blocks: {
     
     final_blocks} (Change: {
     
     final_blocks - initial_blocks})") # 打印最终内存块数量及变化
    print("Memory for 'TemporaryBuffer' should have been released.") # 提示内存已释放

    print("\n--- Custom Context Manager Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# demonstrate_custom_context_manager() # 调用演示函数,取消注释即可运行

解析与思考:
demonstrate_custom_context_manager 示例中,我们创建了一个 MemoryResource 类,并为其实现了 __enter____exit__ 方法,使其成为一个上下文管理器。

  • __enter__ 方法在 with 语句块开始时被调用,我们在这里执行了内存分配(创建了 bytearray)。
  • __exit__ 方法在 with 语句块结束时(无论正常退出还是发生异常)被调用,我们在这里显式地通过 del self.data 解除了对大型 bytearray 的引用,确保这部分内存能够被及时回收。
    这种模式极大地增强了代码的健壮性和内存安全性,因为它保证了资源在不再需要时,无论程序流程如何,都会被正确地清理。对于自定义的、生命周期短暂但内存占用巨大的对象,使用自定义上下文管理器是预防内存泄漏的优雅且强大的方法。

4.1.4 避免在循环中创建或累积大量对象

在长时间运行的程序或处理大量数据批次的循环中,不加控制地创建新对象或向集合中添加对象而不进行清理,是导致内存泄漏的常见模式。

1. 临时对象在循环内的自动回收

Python的局部作用域规则在循环中同样适用。在每次循环迭代中创建的临时对象,如果它们没有被外部引用持有,将在当前迭代结束时超出作用域并被回收。

# python
import sys # 导入sys模块
import gc # 导入gc模块
import time # 导入time模块

class TinyItem: # 小型项目类
    def __init__(self, item_id): # 构造函数
        self.item_id = item_id # 项目ID
        # print(f"TinyItem {item_id} created.") # 调试信息,通常不打印以避免输出过多
    def __del__(self): # 析构函数
        # print(f"TinyItem {self.item_id} deleted.") # 调试信息,通常不打印
        pass # 不做任何操作

def process_items_in_loop_safely(num_iterations): # 安全循环处理项目的函数
    print(f"\n--- Processing {
     
     num_iterations} items safely in a loop ---") # 打印处理信息
    
    gc.collect() # 手动触发垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    print("Starting loop...") # 打印开始循环信息
    for i in range(num_iterations): # 循环指定次数
        temp_item = TinyItem(i) # 在每次迭代中创建一个新的TinyItem实例
        # 模拟对temp_item的瞬时操作
        _ = temp_item.item_id # 访问项目ID
        
        # temp_item 在每次迭代结束时超出作用域,其引用计数归零,会被回收
        if i % (num_iterations // 10) == 0: # 每处理10%的迭代,打印一次内存使用情况
            current_blocks = sys.getallocatedblocks() # 获取当前内存块数量
            print(f"Iteration {
     
     i}: Allocated blocks = {
     
     current_blocks} (Change: {
     
     current_blocks - initial_blocks})") # 打印迭代和内存使用情况
            gc.collect() # 在循环中适度触发GC,以便观察内存释放效果

    print("Loop finished.") # 打印循环完成信息
    gc.collect() # 循环结束后再次触发垃圾回收
    final_blocks = sys.getallocatedblocks() # 获取最终内存块数量
    print(f"Final allocated blocks: {
     
     final_blocks} (Change: {
     
     final_blocks - initial_blocks})") # 打印最终内存块数量及变化
    print("Memory should remain stable as temporary objects are released.") # 提示内存保持稳定

    print("\n--- Safe Loop Processing Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# process_items_in_loop_safely(100_000) # 处理10万个临时对象

解析与思考:
process_items_in_loop_safely 示例中,TinyItem 实例 temp_item 在每次循环迭代中创建。由于 temp_item 是一个局部变量,并且在循环体内没有被任何外部持久化结构引用,因此在每次迭代结束时,temp_item 的引用计数会降为零,其所占用的内存会立即被回收。因此,即使循环迭代次数很大,程序的内存占用也会保持相对稳定,不会持续增长。这展示了Python局部变量自动回收的强大之处,是避免在循环中产生内存泄漏的关键。

2. 累积对象导致内存增长

与上一节形成对比的是,如果在循环内部将新创建的对象添加到某个持久化的集合(如列表、字典)中,而该集合本身没有被及时清理,则会导致内存持续增长,形成泄漏。

# python
import sys # 导入sys模块
import gc # 导入gc模块
import time # 导入time模块

class CachedItem: # 缓存项目类
    def __init__(self, item_id, data_size_kb=100): # 构造函数
        self.item_id = item_id # 项目ID
        self.data = bytearray(1024 * data_size_kb) # 模拟100KB数据
        # print(f"CachedItem {item_id} created.") # 调试信息
    def __del__(self): # 析构函数
        # print(f"CachedItem {self.item_id} deleted.") # 调试信息
        pass # 不做任何操作

global_cache = [] # 定义一个全局列表作为缓存

def accumulate_items_in_loop_leaky(num_iterations): # 循环累积项目导致泄漏的函数
    print(f"\n--- Accumulating {
     
     num_iterations} items in a loop (Leaky) ---") # 打印处理信息
    
    gc.collect() # 手动触发垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    print("Starting leaky accumulation loop...") # 打印开始泄漏累积循环信息
    for i in range(num_iterations): # 循环指定次数
        new_item = CachedItem(i) # 创建新的CachedItem实例
        global_cache.append(new_item) # 将新对象添加到全局缓存列表
        
        if i % (num_iterations // 10) == 0: # 每处理10%的迭代,打印一次内存使用情况
            current_blocks = sys.getallocatedblocks() # 获取当前内存块数量
            print(f"Iteration {
     
     i}: Cache size = {
     
     len(global_cache)}, Allocated blocks = {
     
     current_blocks} (Change: {
     
     current_blocks - initial_blocks})") # 打印迭代、缓存大小和内存使用情况
            gc.collect() # 在循环中适度触发GC,看是否回收(预期不会)

    print("Loop finished.") # 打印循环完成信息
    gc.collect() # 循环结束后再次触发垃圾回收
    final_blocks = sys.getallocatedblocks() # 获取最终内存块数量
    print(f"Final allocated blocks: {
     
     final_blocks} (Change: {
     
     final_blocks - initial_blocks})") # 打印最终内存块数量及变化
    print(f"WARNING: Objects in 'global_cache' are still alive and consuming memory: {
     
     len(global_cache)} items.") # 警告信息
    print("This demonstrates a memory leak due to unmanaged accumulation.") # 提示这是一个内存泄漏示例

    # 为了避免实际的内存泄漏,我们手动清空全局列表
    # print("\nManually clearing global_cache to release memory...") # 打印手动清空信息
    # global_cache.clear() # 清空列表,解除对对象的引用
    # gc.collect() # 再次手动触发垃圾回收
    # print("After clearing global_cache and GC collect:") # 打印清空后信息
    # final_blocks_after_clear = sys.getallocatedblocks() # 获取清空后内存块数量
    # print(f"Allocated blocks: {final_blocks_after_clear} (Change: {final_blocks_after_clear - initial_blocks})") # 打印清空后内存块数量及变化

    print("\n--- Leaky Accumulation Demonstration Complete ---") # 打印演示完成信息

# 调用演示函数
# accumulate_items_in_loop_leaky(10_000) # 累积1万个对象 (总计约1GB数据)

解析与思考:
accumulate_items_in_loop_leaky 示例中,每次循环迭代都会创建一个 CachedItem 实例,并将其添加到 global_cache 这个全局列表中。由于 global_cache 持续持有对这些 CachedItem 实例的强引用,它们的引用计数永远不会降为零,导致内存占用线性增长,形成严重的内存泄漏。即使在循环中调用 gc.collect() 也无济于事,因为这些对象仍然是可达的。
预防策略是:

  • 定期清理:如果必须在循环中累积数据,请确保在达到一定阈值或完成特定任务后,定期对集合进行清理(例如,使用 list.pop(0) 移除旧元素,或 list.clear() 完全清空)。
  • 使用生成器或迭代器:对于只需处理一次性数据的场景,使用生成器或迭代器可以避免一次性将所有数据加载到内存中。
  • 限制缓存大小:如果需要缓存数据,实现一个有固定大小限制的缓存机制(例如,使用 collections.deque 或自定义LRU缓存),以确保旧数据在达到容量时被自动淘汰。

4.1.5 迭代器和生成器的高效利用

迭代器和生成器是Python中处理大量数据流的内存高效方法。它们的核心思想是“惰性计算”(lazy evaluation),即一次只生成或处理一个数据项,而不是一次性将所有数据加载到内存中。这对于避免内存泄漏和提高性能至关重要。

1. 避免一次性加载所有数据

当处理大型数据集(如大型文件、数据库查询结果)时,避免将所有数据一次性加载到内存中是防止内存溢出和内存泄漏的关键。

# python
import sys # 导入sys模块
import gc # 导入gc模块
import psutil # 导入psutil模块,用于获取进程内存信息

# 模拟一个大型数据集
def get_large_dataset_as_list(num_items, item_size_kb): # 获取大型数据集作为列表
    print(f"\n--- Loading {
     
     num_items} items into a list (Memory Intensive) ---") # 打印加载信息
    data = [] # 初始化列表
    for i in range(num_items): # 循环项目数量
        data.append(bytearray(1024 * item_size_kb)) # 创建字节数组并添加到列表
    print("Finished loading list.") # 打印加载完成信息
    return data # 返回列表

# 使用生成器处理大型数据集
def process_large_dataset_with_generator(num_items, item_size_kb): # 使用生成器处理大型数据集
    print(f"\n--- Processing {
     
     num_items} items with a generator (Memory Efficient) ---") # 打印处理信息
    
    gc.collect() # 手动触发垃圾回收
    initial_memory_rss = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) # 获取当前进程的RSS内存使用量(MB)
    print(f"Initial RSS Memory: {
     
     initial_memory_rss:.2f} MB") # 打印初始RSS内存

    def data_generator(count, size_kb): # 数据生成器函数
        for i in range(count): # 循环计数
            yield bytearray(1024 * size_kb) # 每次迭代生成一个字节数组

    processed_count = 0 # 初始化已处理计数
    for item in data_generator(num_items, item_size_kb): # 遍历生成器
        # 模拟对每个item的瞬时操作
        _ = item[0] # 访问item
        processed_count += 1 # 已处理计数加1
        if processed_count % (num_items // 10) == 0: # 每处理10%的项目,打印一次进度和内存
            current_memory_rss = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) # 获取当前RSS内存
            print(f"Processed {
     
     processed_count} items. Current RSS Memory: {
     
     current_memory_rss:.2f} MB (Delta: {
     
     current_memory_rss - initial_memory_rss:.2f} MB)") # 打印进度和内存信息
    
    print(f"Finished processing {
     
     processed_count} items.") # 打印处理完成信息
    gc.collect() # 再次触发垃圾回收
    final_memory_rss = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) # 获取最终RSS内存
    print(f"Final RSS Memory: {
     
     final_memory_rss:.2f} MB (Delta: {
     
     final_memory_rss - initial_memory_rss:.2f} MB)") # 打印最终RSS内存
    print("Memory usage should remain relatively stable.") # 提示内存使用保持稳定

    print("\n--- Generator Processing Demonstration Complete ---") # 打印演示完成信息

# 示例:对比两种数据处理方式
# num_items = 100_000 # 项目数量
# item_kb = 1 # 每个项目大小1KB
# try: # 尝试执行
#     # 方式1: 一次性加载到列表 (可能会导致内存溢出)
#     print("\n--- Attempting to load all data into a list (might consume too much memory) ---") # 打印尝试加载所有数据到列表的信息
#     # large_list = get_large_dataset_as_list(num_items, item_kb) # 调用一次性加载函数,取消注释会占用大量内存
#     # print(f"List object size: {sys.getsizeof(large_list) / (1024*1024):.2f} MB") # 打印列表对象大小
#     # del large_list # 删除列表
#     # gc.collect() # 触发GC
#     # print("List data released (if not crashed).") # 打印列表数据已释放信息
# except MemoryError: # 捕获内存错误
#     print("MemoryError: Could not allocate memory for the list.") # 打印内存错误信息

# 方式2: 使用生成器 (内存高效)
# process_large_dataset_with_generator(num_items, item_kb) # 调用生成器处理函数

解析与思考:
process_large_dataset_with_generator 示例中,我们定义了一个 data_generator 函数,它是一个生成器。当 for item in data_generator(...) 循环迭代时,data_generator 每次只 yield 一个 bytearray 实例。这意味着在任何给定时间点,内存中只存在一个或少数几个 bytearray 实例,而不是所有 num_items 个实例。这与 get_large_dataset_as_list 函数形成鲜明对比,后者会一次性创建并持有所有 num_itemsbytearray 实例,从而可能导致内存溢出。
使用生成器和迭代器是处理大规模数据的黄金法则,因为它将内存需求从 O(N) 降低到 O(1)(或一个非常小的常数),极大地减少了内存泄漏的风险。

2. 自定义迭代器实现

除了生成器函数,我们也可以通过实现 __iter____next__ 方法来创建自定义迭代器,以实现对复杂数据源的内存高效处理。

# python
import sys # 导入sys模块
import gc # 导入gc模块

class CustomDataReader: # 自定义数据读取器类
    def __init__(self, start_id, end_id, item_size_bytes): # 构造函数
        self.current_id = start_id # 当前ID
        self.end_id = end_id # 结束ID
        self.item_size_bytes = item_size_bytes # 项目大小(字节)
        print(f"CustomDataReader initialized for IDs {
     
     start_id} to {
     
     end_id}.") # 打印初始化信息

    def __iter__(self): # 迭代器方法
        return self # 返回自身,因为该类本身就是迭代器

    def __next__(self): # 下一个方法
        if self.current_id < self.end_id: # 如果当前ID小于结束ID
            item_id = self.current_id # 获取当前ID
            self.current_id += 1 # 当前ID加1
            
            # 模拟生成一个数据项
            data_item = bytearray(self.item_size_bytes) # 创建一个指定大小的字节数组
            # 假设对数据项进行初始化或其他操作
            # data_item[0] = item_id % 256 # 设置第一个字节
            
            # print(f"Generated item {item_id}") # 调试信息
            return data_item # 返回数据项
        else: # 如果当前ID达到或超过结束ID
            print("No more items to generate.") # 打印没有更多项目信息
            raise StopIteration # 抛出StopIteration异常,表示迭代结束

def process_with_custom_iterator(start, end, size): # 使用自定义迭代器处理的函数
    print(f"\n--- Processing items with CustomDataReader from {
     
     start} to {
     
     end} ---") # 打印处理信息
    
    gc.collect() # 手动触发垃圾回收
    initial_blocks = sys.getallocatedblocks() # 获取初始内存块数量
    print(f"Initial allocated blocks: {
     
     initial_blocks}") # 打印初始内存块数量

    reader = CustomDataReader(start, end, size) # 创建CustomDataReader实例
    processed_count = 0 # 初始化已处理计数
    for item_data in reader: # 遍历自定义迭代器
        # 模拟处理每个数据项
        _ = item_data[0] # 访问数据
        processed_count += 1 # 已处理计数加1
        
        if processed_count % ((end - start) // 10) == 0: # 每处理10%的项目,打印一次进度和内存
            current_blocks = sys.getallocatedblocks() # 获取当前内存块数量
            print(f"Processed {
     
     processed_count} items. Allocated blocks = {
     
     current_blocks} (Change: {
     
     current_blocks - initial_blocks})") # 打印进度和内存信息
            gc.collect() # 适度触发GC

    print(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值