跳至主要内容

如何在 Python 中编写内存高效的类

通过高级技巧学习 Python 的内存管理,编写更省内存的类。包含实用练习,助您获得最佳性能。
· 10分钟 阅读

使用 AI 探索

在 Python 中编写内存高效的类,对于在各种系统约束下依然表现出色的健壮应用至关重要。高效的内存使用能够加快应用运行时间并提升可扩展性,因为它消耗的资源更少,这在内存有限的环境中尤为重要。

在本文中,我将带您了解 Python 内存管理的基础知识,并介绍多种编写内存高效类的实用技巧。无论您是在构建复杂系统还是简单脚本,这些见解都将帮助您编写更简洁、更高效的 Python 代码。

Python 内存管理基础

Python 的内存管理系统十分完善,旨在通过处理内存操作的复杂性来简化开发。该系统是为了确保Python 应用高效运行。

Python 如何管理内存

私有堆(private heap)是 Python 内存管理的核心,所有 Python 对象和数据结构都存储在其中。程序员无法直接访问这一私有堆,而是通过 Python 的内存管理系统与对象交互。

内存管理系统使用:

  • 内存分配器:Python 使用内置的内存分配器来管理内存块的分配与释放。该分配器针对小对象使用“空闲列表”(free lists)以复用先前分配的内存块,从而加速后续分配。对于更复杂的对象,如列表和字典,Python 采用动态内存分配以管理其大小变化。
  • 内存池:Python 按对象大小将内存组织为多个池,以尽量减少开销与碎片化。通过对相似大小的对象进行分组,有助于更高效地管理内存。

内存释放与垃圾回收

Python 采用双重机制进行垃圾回收:

  • 引用计数:这是主要方法,Python 会跟踪每个对象的引用数。当某个对象的引用计数降为零(即不再有任何引用)时,内存分配器会立即释放其内存。
  • 循环垃圾回收器:Python 包含一个用于处理循环引用的垃圾回收器,单靠引用计数无法解决这类问题。该回收器会定期识别并清理那些彼此引用、但在程序其他地方已不再使用的对象组。

分析与优化内存使用

理解并优化内存使用对于维持应用性能至关重要。

Python 提供了多种内存分析工具,能提供超越常规调试的信息,帮助开发者识别并解决内存低效问题:

  • pympler:一款全面的工具,可跟踪内存使用并分析对象空间,适用于深入的内存调查。
  • memory_profiler:提供逐行的内存使用分析,便于开发者精确定位内存消耗较高的代码行。
  • tracemalloc:集成于 Python 标准库,可跟踪内存分配并检测泄漏,提供内存分配趋势洞察。

本教程将使用pympler库,您可以在终端使用 pip 包管理器安装它:

pip install pympler

您可以在我们的教程中阅读更多内存分析内容——Python 内存分析入门。

在 Python 中编写内存高效类的技巧

既然我们已经了解了 Python 内存管理的基础,接下来看看一些用于编写内存高效类的高级技巧。

1. 使用 __slots__ 优化类结构

在 Python 中,每个类实例都有一个名为__dict__的字典来存储实例变量。尽管这允许动态赋值新变量,但在创建大量类实例时会导致显著的内存开销。通过定义__slots__,您可以告知 Python 仅为固定属性集分配空间,从而无需动态字典。

使用__slots__ 有两大好处:

  • 降低内存占用:没有实例字典后,每个对象使用更少内存。当需要创建数百万个类实例时,单个实例的节省会被成倍放大。
  • 加快属性访问速度:通过__slots__访问属性比通过字典更快,因为其直接查找属性,跳过了字典访问涉及的哈希表操作。

实践示例:二维空间中的点

考虑一个在二维空间中表示点的简单类:

class PointWithoutSlots:
   def __init__(self, x, y):
       self.x = x
       self.y = y

class PointWithSlots:
   __slots__ = ('x', 'y')
   def __init__(self, x, y):
       self.x = x
       self.y = y

在PointWithoutSlots类中,每个实例都有一个__dict__来存储x和y属性,而PointWithSlots类没有,从而使每个实例占用更少内存。

为展示内存使用差异,我们创建一千个点,并使用pympler包测量总内存使用:

from pympler import asizeof

points_without_slots = [PointWithoutSlots(i, i) for i in range(1000)]
points_with_slots = [PointWithSlots(i, i) for i in range(1000)]

print("Total memory (without __slots__):", asizeof.asizeof(points_without_slots))
print("Total memory (with __slots__):", asizeof.asizeof(points_with_slots))

我们看到的输出是:

有无 slots 的内存对比

有无 slots 的内存对比

通常,PointWithSlots实例比PointWithoutSlots消耗更少内存,这证明了__slots__在降低单实例内存开销方面的有效性。

使用 __slots__ 的局限性

尽管__slots__有利于内存优化,但也存在一些限制:

  • 灵活性不足:一旦定义了__slots__,便无法再为实例动态添加新属性。
  • 继承的复杂性:如果带有__slots__的类被继承,子类也必须定义__slots__才能继续受益于内存优化,这可能会增加类设计的复杂度。

尽管如此,对于希望优化 Python 应用内存使用的开发者而言,使用__slots__依然是一项强有力的工具,尤其适用于内存约束严格、或实例数量极多的场景。

2. 实现内存高效的设计模式

内存高效的设计模式是帮助优化内存使用与管理的架构性解决方案。

在类设计中,两种对内存效率尤为有效的模式是享元模式和单例模式。在需要大量类实例或共享数据的场景中,若正确实现,这些模式可以显著降低内存消耗。

享元(Flyweight)设计模式

享元模式通过在多个对象之间共享状态的公共部分,而不是在每个对象中保存全部数据,从而大幅降低整体内存占用。

设想一个绘图应用中的图形圆类,每个圆拥有颜色、半径与坐标。使用享元模式,颜色和半径这类很可能被多个圆共享的属性可以存储在共享对象中:

class Circle:
   # Flyweight object
   def __init__(self, color):
       self.color = color
   def draw(self, radius, x, y):
       print(f"Drawing a {self.color} circle with radius {radius} at ({x},{y})")

class CircleFactory:
   # Factory to manage Flyweight objects
   _circles = {}
   @classmethod
   def get_circle(cls, color):
       if not cls._circles.get(color):
           cls._circles[color] = Circle(color)
       return cls._circles[color]

在这个例子中,多个圆实例会基于颜色共享来自CircleFactory的同一个Circle对象,从而仅存储一次颜色数据以更高效地使用内存。

让我们用这个类来创建实例,看看如何节省内存。

import sys

factory = CircleFactory()
circle1 = factory.get_circle("Red")
circle2 = factory.get_circle("Red")
circle3 = factory.get_circle("Blue")

print("Memory address of circle1:", id(circle1))
print("Memory address of circle2:", id(circle2))
print("Memory address of circle3:", id(circle3))
print ("-----------------------------")
print("Memory used by circle1:", sys.getsizeof(circle1))
print("Memory used by circle2:", sys.getsizeof(circle2))
print("Memory used by circle3:", sys.getsizeof(circle3))

我们看到的输出是:

享元设计模式的内存使用

享元设计模式的内存使用

可以看到,circle1和circle2具有相同的内存地址,意味着它们确实是同一个对象(共享颜色“Red”的同一实例)。circle3拥有不同的内存地址,因为它表示颜色不同(“Blue”)的圆。

circle1与circle2为同一实例,表明CircleFactory成功在两者间共享了Circle实例。这正是内存效率的体现:如果您的应用中有一千个红色圆,使用享元模式它们都会共享一个用于颜色的Circle实例,而不是每个圆都拥有各自的Circle实例,从而大幅节省内存。

单例(Singleton)设计模式

单例模式确保某个类只有一个实例,并提供对它的全局访问点。该模式非常适合在整个应用中管理配置或共享资源,通过避免在多个地方复制共享资源或配置数据来减少内存开销。

来看这个Database类:

class Database:
   _instance = None
   def __new__(cls):
       if cls._instance is None:
           cls._instance = super(Database, cls).__new__(cls)
           # Initialize any attributes of the instance here
       return cls._instance
   def connect(self):
       # method to simulate database connection
       return "Connection established"

在此实现中,无论实例化多少次,Database都只会使用一个共享实例,通过避免多个相同对象来节省内存。

我们可以通过以下代码进行验证:

db1 = Database()
print(db1.connect())
db2 = Database()
print(db1 is db2) 

输出如下:

单例设计模式的实例创建

单例设计模式的实例创建

“True”表示db1和db2 变量引用的是同一实例。

这些模式展示了合理设计如何显著提升内存效率。通过选择并实现合适的设计模式,开发者可以让 Python 应用更精简、更高效。

3. 在类中使用内置数据类型

选择合适的Python 内置数据类型,会对内存效率产生显著影响。由于在 Python 解释器中的内部实现,内置类型在性能和内存使用方面都经过了优化。

例如,基于其特性,在某些用例中,元组和集合可能比列表和字典更省内存。

元组通常比列表更省内存,因为它们大小固定且创建后不可更改,这使得 Python 的内部机制可以优化其内存存储。对于那些在创建后无需修改数据的类,元组是极佳的选择。

实践示例:多边形类

考虑两个简单的多边形类,分别使用元组和列表来存储数值:

class PolygonWithTuple:
   def __init__(self, points):
       self.points = tuple(points)  # Storing points as a tuple
   def display_points(self):
       return self.points

class PolygonWithList:
   def __init__(self, points):
       self.points = list(points)  # Storing points as a list
   def display_points(self):
       return self.points

我们可以创建这两个类的实例,比较各自的内存使用:

from pympler import asizeof

points = [(1, 2), (3, 4), (5, 6), (7, 8)]

# Creating instances of each class
polygon_tuple = PolygonWithTuple(points)
polygon_list = PolygonWithList(points)

# Memory usage comparison using pympler
print("Total memory used by Polygon with Tuple:", asizeof.asizeof(polygon_tuple))
print("Total memory used by Polygon with List:", asizeof.asizeof(polygon_list))

得到的输出是:

元组与列表的内存对比

元组与列表的内存对比

该输出表明,元组通常比列表更省内存,因此对于存储几何坐标等固定、不变的数据,PolygonWithTuple类在内存效率上更占优势。

4. 利用生成器进行惰性求值

在 Python 中,生成器是管理内存的强大工具,尤其适用于处理大型数据集。

生成器允许一次只处理一个数据项,并在任意时刻仅将必要数据加载到内存中。这种惰性求值的方法在需要处理大量数据、但每次只需处理少量数据的应用中极为有利,可降低整体内存占用。

实践示例:处理大型数据集

设想一个应用需要处理大量用户记录,例如计算用户的平均年龄。与其将所有记录加载到列表(可能占用大量内存),不如使用生成器一次处理一条记录。

def process_records(file_name):
   """Generator that yields user ages from a file one at a time."""
   with open(file_name, 'r') as file:
       for line in file:
           user_data = line.strip().split(',')
           yield int(user_data[1]) 

def average_age(file_name):
   """Calculate the average age from user records."""
   total_age = 0
   count = 0
   for age in process_records(file_name):
       total_age += age
       count += 1
   return total_age / count if count else 0

print("Average Age:", average_age('user_data.csv'))

在这个示例中,process_records()生成器函数一次从文件读取一行,将相关数据转换为整数并产出。这意味着任一时刻仅有一行数据驻留内存,最大限度降低内存使用。average_age()函数则消费该生成器,累计总年龄与数量,而无需一次性将整个数据集放入内存。

因此,采用惰性求值方法,我们可以节省内存,并增强应用的响应性与可扩展性。

5. 避免循环引用

当两个或多个对象彼此引用时,会产生循环引用。这可能在列表、字典等结构,或更复杂的类定义中悄然发生——实例彼此持有引用,形成一个环。

如在内存管理基础部分所述,Python 具有额外的垃圾回收器来检测并清理循环引用。然而,依赖该机制可能带来更高的内存使用与低效:

  • 清理延迟:循环垃圾回收器是周期性运行而非持续运行。这意味着涉及循环引用的对象在不再需要后,可能不会被立即回收,导致不必要的内存占用。
  • 性能开销:循环垃圾回收器需要额外资源来定期检查并清理循环引用,可能影响性能。

实践示例:树形数据结构

考虑树形数据结构中的常见用例:每个节点可能同时保存对其子节点的引用,并可选地保存对其父节点的引用。这样的父子连接很容易产生循环引用。

class TreeNode:
   def __init__(self, value, parent=None):
       self.value = value
       self.parent = parent
       self.children = []
   def add_child(self, child):
       child.parent = self  # Set parent reference
       self.children.append(child)

root = TreeNode("root")
child = TreeNode("child1", root)
root.add_child(child)

在该示例中,root通过其children列表引用child,而child通过其parent属性引用root。在无其他干预的情况下,这一设置即可形成循环引用。

使用弱引用避免循环引用

弱引用是一种无需重构整个数据结构即可打破或避免循环引用的有效方式。

Python 的weakref模块允许对对象创建不会增加其引用计数的引用。弱引用不会阻止被引用对象被垃圾回收,从而避免与循环引用相关的内存泄漏。

以下是如何修改前述类以使用弱引用:

import weakref

class TreeNode:
   def __init__(self, value, parent=None):
       self.value = value
       self.parent = weakref.ref(parent) if parent else None
       self.children = []
   def add_child(self, child):
       child.parent = weakref.ref(self)
       self.children.append(child)
root = TreeNode("root")
child = TreeNode("child1", root)
root.add_child(child)

在该类中,parent 属性现在是对父节点的弱引用。这意味着只要不存在对父节点的其他强引用,即便子节点仍然存在,父节点也可以被垃圾回收。

这种方法有效地防止了此类结构中循环引用通常引发的内存泄漏问题。

结语

本文介绍了 Python 内存管理的基础,并给出了五种编写内存高效类的高级技巧,配以实践示例和内存对比。希望您能在项目中采纳这些技巧与最佳实践,构建具备可扩展性和高效率的应用。

欢迎查看我们的 Python Programming 路线,其中涵盖软件工程原则与 面向对象编程,以及 编写高效的 Python 代码

Arunn Thevapalan

作者

Arunn Thevapalan

作为一名高级数据科学家,我设计、开发并部署大规模机器学习解决方案,帮助企业做出更明智的数据驱动决策。作为数据科学写作者,我分享所学心得、职业建议以及深入的实践型教程。

FAQs

什么是 __slots__,它如何帮助编写内存高效的 Python 类?

__slots__ 是 Python 中的一种机制,允许显式声明实例属性,从而避免创建实例字典。通过在类中定义__slots__,Python 会使用比常规每实例字典更为省内存的数据结构。这能降低内存占用,尤其适用于需要创建大量类实例的程序。

循环引用如何影响内存使用,如何避免?

循环引用指两个或多个对象彼此引用,形成一个环,从而阻止 Python 的垃圾回收器回收它们的内存。为避免循环引用,您可以使用weakref模块提供的弱引用、通过重构设计移除不必要的引用,或在不再需要时将引用手动设为 None 以打破环路。

垃圾回收器在 Python 的内存管理中发挥什么作用?

Python 的垃圾回收器用于补充引用计数机制,负责检测并清理循环引用。它会定期扫描仅彼此可达、而在应用其他地方不再使用的对象组,从而释放单靠引用计数无法回收的内存。

为何使用内置数据类型和数据结构对内存效率很重要?

Python 的内置数据类型和数据结构在性能与内存使用上高度优化。使用这些内置类型(例如对不可变数据使用元组而非列表,或使用集合表示唯一元素集合)相比自定义或不合适的数据结构,可显著降低内存开销。

内存分析工具如何帮助编写内存高效的 Python 类?

memory_profiler、pympler 和 tracemalloc 等内存分析工具可帮助开发者了解应用的内存使用情况。它们能够定位内存泄漏、低效的数据结构,或使用内存过多的具体代码行,从而帮助开发者决定优化的方向与位置。

主题