跳至主要内容

Python 内存分析入门

学习如何通过 Python 的内存分析来优化性能,配合 memory_profiler、tracemalloc 和 objgraph 等工具。
· 14分钟 阅读

使用 AI 探索

Python 应用,尤其是处理大型数据集或复杂计算的程序,可能会占用过多内存,从而导致变慢、崩溃或内存不足错误。内存分析是一种技术,它可以让您分析 Python 代码的内存使用情况,识别内存泄漏和瓶颈,并优化内存消耗,以提升性能与稳定性。

在本教程中,我们将探讨内存分析、可用的工具,以及如何使用这些工具来分析您的 Python 代码的实用示例。

什么是内存分析?

内存分析是一种在程序运行时监控和评估其内存使用情况的技术。该方法可帮助开发者发现内存泄漏、优化内存利用,并理解程序的内存消耗模式。内存分析对于防止应用程序使用超出必要的内存、从而引发性能下降或崩溃至关重要。

内存分析的优势

内存分析带来多项关键好处,对于维护高效、健壮的应用不可或缺:

  • 识别内存泄漏:找出未正确释放内存的代码段。
  • 优化内存使用:定位使用内存过多的函数或对象。
  • 提升性能:增强应用的速度与响应能力。
  • 防止崩溃:避免内存不足错误。

常用的内存分析工具

我们来看看几种常用的内存分析工具,介绍其特性、安装方法与用法。具体将涵盖用于逐行内存分析的 memory_profiler,用于拍摄并比较内存快照的 tracemalloc,以及用于可视化对象关系的 objgraph。

memory_profiler

memory_profiler 是一款功能强大的 Python 工具,可详细洞察代码的内存使用情况。它通过提供细粒度的逐行内存使用明细,方便定位导致高内存消耗的确切代码行。通过这种层级的了解,开发者可以有针对性地进行优化。

安装

要安装 memory_profiler 及所需的其他包,请按以下步骤操作:

  • 打开终端或命令提示符。
  • 在 Windows 上,使用命令提示符或 PowerShell。
  • 在 macOS 或 Linux 上,使用终端。

以下命令安装 memory_profiler 包:

pip install memory_profiler

如果您希望用图表可视化内存使用情况,可以运行以下命令安装 matplotlib:

pip install matplotlib

用法

要使用 memory_profiler,请按照以下步骤:

  • 为需要分析的函数添加装饰器。

  • 在文本编辑器或 IDE 中打开您的 Python 脚本。

  • 从 memory_profiler 导入 profile 装饰器,并将其应用于要分析的函数。

示例脚本如下: 

from memory_profiler import profile
@profile

def allocate_memory():
    # Allocate a list with a range of numbers
    a = [i for i in range(10000)]
    # Allocate another list with squares of numbers
    b = [i ** 2 for i in range(10000)]
    return a, b

if __name__ == "__main__":
    allocate_memory()

代码说明

这里我从 memory_profiler 包中导入了 profile 装饰器。该装饰器将帮助我们监控被装饰函数的内存使用。

from memory_profiler import profile

我将 @profile 装饰器应用到 allocate_memory() 函数上。这意味着当 allocate_memory() 执行时,memory_profiler 会跟踪其内存使用并提供详细报告。

@profile

在 allocate_memory() 函数内,我创建了两个列表。第一个列表包含 0 到 9999 的数字;第二个列表包含 0 到 9999 的平方。

a = [i for i in range(10000)]  # List of numbers from 0 to 9999

b = [i**2 for i in range(10000)]  # List of squares of numbers from 0 to 9999

以下代码块确保仅在脚本被直接运行时才调用 allocate_memory() 函数,而不是在作为模块导入时调用。运行该脚本后,memory_profiler 会输出 allocate_memory() 的内存使用情况,便于您了解在创建列表 a 和 b 时消耗了多少内存。

if __name__ == "__main__":

   allocate_memory()

在终端或命令提示符中,导航到脚本所在目录。使用 -m memory_profiler 选项运行脚本。

-m memory_profiler your_script.py

每行代码的内存使用情况。图片来源:作者。

在上图中,我们可以看到每行代码的内存使用以及每行的增量。下方的图表更详细地展示了各行代码如何使用内存,以及随着每次迭代内存如何增加。

内存使用随时间变化。图片来源:作者。

tracemalloc

tracemalloc 是 Python 内置的强大模块,用于跟踪内存的分配与释放。理解应用如何使用内存对于优化性能与识别内存泄漏至关重要。Tracemalloc 提供了易用的接口来捕获与分析内存使用快照,是每位 Python 开发者都非常有价值的工具。

我们将看看 tracemalloc 如何拍摄内存使用快照、比较这些快照来识别内存泄漏,并分析内存占用最高的部分

拍摄内存使用快照

tracemalloc 允许您在程序的特定时间点拍摄内存使用快照。快照会捕获内存分配的状态,提供内存使用位置的详细视图。

以下是使用 tracemalloc 拍摄快照的方法:

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Code block for which memory usage is to be tracked
def allocate_memory():
    a = [i for i in range(10000)]  # List of numbers from 0 to 9999
    b = [i**2 for i in range(10000)]  # List of squares of numbers from 0 to 9999
    return a, b

# Keep the allocated lists in scope
allocated_lists = allocate_memory()

# Take a snapshot
snapshot = tracemalloc.take_snapshot()

# Stop tracing memory allocations
tracemalloc.stop()

# Analyze the snapshot to see memory usage
top_stats = snapshot.statistics('lineno')

# Write the memory usage data to a text file
with open('memory_usage.txt', 'w') as f:
    f.write("[ Top 10 memory consumers ]\n")
    for stat in top_stats[:10]:
        f.write(f"{stat}\n")
    # Detailed traceback for the top memory consumer
    f.write("\n[ Detailed traceback for the top memory consumer ]\n")
    for stat in top_stats[:1]:
        f.write('\n'.join(stat.traceback.format()) + '\n')

print("Memory usage details saved to 'memory_usage.txt'")

我使用 tracemalloc 模块在 Python 应用中跟踪并检查内存使用。通过拍摄内存分配的快照并查看内存占用最高的部分,我确定了代码中使用最多内存的区域。随后,我将这些分析写入文本文件以便进一步阅读与参考。

比较快照以识别内存泄漏

tracemalloc 的强大功能之一是可以比较在程序不同时间点拍摄的快照,从而识别内存泄漏。通过分析快照之间的差异,您可以定位哪些地方分配了额外内存且未被释放。

以下是比较快照的方法:

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Function to allocate memory
def allocate_memory():
    a = [i for i in range(10000)]  # List of numbers from 0 to 9999
    b = [i**2 for i in range(10000)]  # List of squares of numbers from 0 to 9999
    return a, b

# Initial memory usage snapshot
snapshot1 = tracemalloc.take_snapshot()

# Perform memory allocations
allocated_lists = allocate_memory()

# Memory usage snapshot after allocations
snapshot2 = tracemalloc.take_snapshot()

# Further memory allocations (simulate more work to find potential leaks)
allocated_lists += allocate_memory()

# Memory usage snapshot after additional allocations
snapshot3 = tracemalloc.take_snapshot()

# Stop tracing memory allocations
tracemalloc.stop()

# Compare the snapshots
stats1_vs_2 = snapshot2.compare_to(snapshot1, 'lineno')
stats2_vs_3 = snapshot3.compare_to(snapshot2, 'lineno')

# Save the comparison results to a text file
with open('memory_leak_analysis.txt', 'w') as f:
    f.write("[ Memory usage increase from snapshot 1 to snapshot 2 ]\n")
    for stat in stats1_vs_2[:10]:
        f.write(f"{stat}\n")

    f.write("\n[ Memory usage increase from snapshot 2 to snapshot 3 ]\n")
    for stat in stats2_vs_3[:10]:
        f.write(f"{stat}\n")

    # Detailed traceback for the top memory consumers
    f.write("\n[ Detailed traceback for the top memory consumers ]\n")
    for stat in stats2_vs_3[:1]:
        f.write('\n'.join(stat.traceback.format()) + '\n')

print("Memory leak analysis saved to 'memory_leak_analysis.txt'")

从快照 1 到快照 2,我们看到创建列表 a 和 b 是内存使用激增的主要原因。从napshot 2 到快照 3,我们也看到更多对列表 a 和 b 的分配提升了内存利用率。

通过比较这些快照,我们发现内存使用增加的主要原因是反复分配大型列表。在真实应用中,如果这些分配并非必要,可能就意味着内存泄漏。详细的回溯可以定位导致这些分配的精确代码行。

分析内存占用最高的部分

tracemalloc 还提供工具来分析应用中内存占用最高的部分。这有助于您将优化重点放在最消耗内存的代码片段上。

以下是分析高内存占用位置的方法:

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Code block for which memory usage is to be tracked
def allocate_memory():
    a = [i for i in range(10000)]
    b = [i**2 for i in range(10000)]

allocate_memory()

# Take a snapshot
snapshot = tracemalloc.take_snapshot()

# Analyze top memory consumers
top_stats = snapshot.statistics('lineno')
print("[ Top 10 memory consumers ]")
for stat in top_stats[:10]:
    print(stat)

# Stop tracing memory allocations
tracemalloc.stop()

在此示例中,拍摄快照后,我使用 snapshot.statistics('lineno') 获取按行号分组的内存使用统计。该输出可帮助您快速识别代码中哪些部分消耗内存最多,从而有针对性地进行优化。

objgraph

Objgraph 是一个 Python 库,旨在帮助开发者可视化和分析代码中的对象引用。它提供工具来创建显示内存中对象彼此引用关系的可视化图表。这种可视化有助于理解程序内部的结构与关系、识别内存泄漏并优化内存使用。

安装

首先,我们安装 objgraph 库。您可以通过 pip 安装:

pip install objgraph

objgraph 的关键特性

objgraph 为内存分析与可视化提供了若干关键功能:

  1. 生成对象引用的交互式图:使用 objgraph,您可以创建对象及其引用关系的可视化表示。这有助于理解对象之间的互联结构,使调试与优化更为容易。
  2. 检测循环引用:当两个或更多对象互相引用时会形成循环,垃圾回收器可能无法回收。Objgraph 可帮助识别这些循环引用,如果不加以处理,可能会导致内存泄漏。
  3. 分析对象类型与大小:Objgraph 允许您检查内存中对象的类型与大小。这些信息对于优化内存使用、提升应用性能至关重要。

使用 objgraph 可视化对象关系

通过一些示例了解 objgraph 的实际用法。

示例 1:生成对象引用图

假设我们有一个简单的类,并创建一些实例:

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []

# Create some nodes
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)

# Establish relationships
node1.children.append(node2)
node2.children.append(node3)

import Objgraph

# Generate a graph of object references
objgraph.show_refs([node1], filename='object_refs.png')

在该示例中,我们定义一个 Node 类,并创建具有关联关系的节点实例。objgraph.show_refs() 函数会从 node1 开始生成对象引用的可视化图,并保存为 object_refs.png。该图有助于我们理解节点之间的互联方式。

示例 2:检测循环引用

要检测循环引用,我们可以使用 objgraph.find_backref_chain() 函数:

# Create a circular reference
node 3.children.append(node1)

# Detect circular references
chain = objgraph.find_backref_chain(node1, objgraph.is_proper_module)
objgraph.show_chain(chain, filename='circular_refs.png')

此处我们通过将 node1 添加为 node3 的子节点来创建循环引用。objgraph.find_backref_chain() 函数会检测到该循环引用,而 objgraph.show_chain 会对其进行可视化,并将结果保存为 circular_refs.png。

示例 3:分析对象类型与大小

要分析对象的类型与大小,我们可以使用 objgraph.show_most_common_types 和 objgraph.by_type:

# Show the most common object types
objgraph.show_most_common_types()

# Get the details of a specific object type
objs = objgraph.by_type('Node')
objgraph.show_refs(objs[:3], refcounts=True, filename='node_details.png')

在该示例中,objgraph.show_most_common_types 会显示当前内存中最常见的对象类型。objgraph.by_type() 函数会检索所有 Node 类的实例,我们再可视化其引用(包含引用计数),并将图表保存为 node_details.png。

如何使用内存分析工具

现在我们来看看高效使用内存分析工具的多种技巧。我们将重点介绍如何分析特定函数、跟踪整体内存使用,以及可视化对象图。

分析特定函数

使用 memory_profiler 或 tracemalloc 针对特定函数进行分析。

memory_profiler 示例

memory_profiler 提供了一种直接的方法,可在函数内部逐行监控内存消耗,帮助您定位可降低内存使用的区域。

from memory_profiler import profile

@profile

def allocate_memory():
    a = [i for i in range(10000)]
    b = [i ** 2 for i in range(10000)]
    return a, b

if __name__ == "__main__":
    allocate_memory()

tracemalloc 示例

import tracemalloc

tracemalloc.start()

def allocate_memory():
    a = [i for i in range(10000)]
    b = [i ** 2 for i in range(10000)]
    return a, b

allocate_memory()

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

跟踪整体内存使用

要随时间监控程序的总内存使用情况,我们可以使用内置的 tracemalloc 模块。以下展示如何跟踪总体内存使用并解读结果以识别趋势。

使用 tracemalloc 的步骤

我们开始于脚本开头初始化内存跟踪。

import tracemalloc

tracemalloc.start()

随后我们运行希望监控的代码部分。

def run_heavy_computation():
   a = [i for i in range(10000)]
   b = [i**2 for i in range(10000)]
   return a, b

run_heavy_computation()

我们拍摄内存使用的快照,以便在不同时间点捕获内存分配状态。

snapshot1 = tracemalloc.take_snapshot()

run_heavy_computation()

snapshot2 = tracemalloc.take_snapshot()

最后,我们比较快照以理解内存使用模式并检测泄漏。

stats = snapshot2.compare_to(snapshot1, 'lineno')

for stat in stats[:10]:
   print(stat)

示例

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Function to run heavy computation
def run_heavy_computation():
    a = [i for i in range(10000)]
    b = [i**2 for i in range(10000)]
    return a, b

# Initial memory usage snapshot
snapshot1 = tracemalloc.take_snapshot()

# Perform memory allocations
run_heavy_computation()

# Memory usage snapshot after allocations
snapshot2 = tracemalloc.take_snapshot()

# Compare the snapshots
stats = snapshot2.compare_to(snapshot1, 'lineno')

# Print the top memory-consuming lines of code
print("[ Top 10 memory consumers ]")
for stat in stats[:10]:
    print(stat)

# Stop tracing memory allocations
tracemalloc.stop()

 结果解读提示

以下提示有助于您理解分析数据:

  • 关注显著变化:重点查看内存增量较大的代码行。
  • 识别热点:确定哪些代码行或函数持续消耗最多内存。
  • 纵向跟踪:在程序不同阶段多次拍摄快照,以识别趋势并定位内存使用激增的时间与位置。
  • 检查内存泄漏:比较重要操作前后的快照以检测泄漏。内存使用持续上升通常意味着泄漏。

可视化对象图

使用 objgraph 库,我们可以创建对象关系的可视化图。这有助于分析内存使用、检测泄漏并理解对象生命周期。

以下是可视化对象图的分步流程。首先生成对象引用图;随后检测循环引用;最后对图表进行分析。 

 生成对象引用图

import objgraph

class Node:
   def __init__(self, value):
       self.value = value
       self.children = []

# Create some nodes and establish relationships
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.children.append(node2)
node2.children.append(node3)

# Generate a graph of object references
objgraph.show_refs([node1], filename='object_refs.png')

检测循环引用

# Create a circular reference
node3.children.append(node1)

# Detect circular references
chain = objgraph.find_backref_chain(node1, objgraph.is_proper_module)

objgraph.show_chain(chain, filename='circular_refs.png')

分析对象类型与大小

# Show the most common object types
objgraph.show_most_common_types()

# Get details of a specific object type
nodes = objgraph.by_type('Node')

objgraph.show_refs(nodes[:3], refcounts=True, filename='node_details.png')

 示例

import objgraph

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []

# Create some nodes and establish relationships
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.children.append(node2)
node2.children.append(node3)

# Generate a graph of object references
objgraph.show_refs([node1], filename='object_refs.png')

# Create a circular reference
node3.children.append(node1)

# Detect circular references
chain = objgraph.find_backref_chain(node1, objgraph.is_proper_module)
objgraph.show_chain(chain, filename='circular_refs.png')

# Show the most common object types
objgraph.show_most_common_types()

# Get details of a specific object type
nodes = objgraph.by_type('Node')
objgraph.show_refs(nodes[:3], refcounts=True, filename='node_details.png')

分析图表

最后一步,我们来分析图表。可按以下顺序考虑:

  • 识别连接关系:查看对象的连接方式,以理解依赖与生命周期。
  • 检测泄漏:寻找那些本应被垃圾回收却仍留在内存中的对象,这可能表明存在泄漏。
  • 发现循环引用:识别会阻碍垃圾回收、导致内存膨胀的循环引用。
  • 优化内存使用:基于图表洞见重构代码,提升内存效率。

实用技巧与最佳实践

以下关键技巧与最佳实践可帮助您最大化分析成效:

  1. 尽早分析:在开发流程的早期并持续进行分析。
  2. 使用真实数据:使用真实的数据与负载,以获得准确的分析结果。
  3. 组合使用工具:结合多种工具,以获得全面认识。
  4. 警惕误报: 注意内存分析中的潜在误报。
  5. 优化内存密集部分:将优化重点放在最耗内存的代码部分。

结论

在本教程中,我们探讨了理解并优化 Python 应用内存使用的关键工具与技术。其中包括 memory_profiler,它提供在函数内部逐行监控内存消耗的简便方式,以及 objgraph,它具备强大的可视化能力,帮助您分析对象关系、检测循环引用,并理解内存使用结构。

将内存分析工具融入您的开发流程,可帮助您深入洞察代码如何管理内存,做出明智的优化决策,并提升应用的整体性能与可靠性。无论您是在编写小型脚本还是构建大规模系统,内存分析都是每位致力于编写高效稳健代码的 Python 开发者必不可少的实践。

若要加深您对内存分析与高效编码的理解和实践,建议学习 Writing Efficient Code in Python Course。此外,我们的 Python Programming 课程提供关于最佳实践与高级技术的全面概览,助您成长为一名熟练的开发者。


Oluseye Jeremiah

作者

Oluseye Jeremiah

专注于人工智能、机器学习与数据科学的技术写作者,擅长将复杂概念讲解得清晰易懂。

主题