Python 内存管理2025年3月17日 | 阅读 12 分钟 在本教程中,我们将学习 Python 如何管理内存,以及 Python 内部是如何处理我们的数据的。我们将深入探讨这个主题,以理解 Python 的内部工作原理以及它如何处理内存。 本教程将深入讲解 Python 的内存管理。当我们执行 Python 脚本时,Python 内存中有很多逻辑在后台运行,以使代码高效。 引言内存管理对于软件开发人员高效地使用任何编程语言都非常重要。众所周知,Python 是一种著名且广泛使用的编程语言。它几乎应用于每个技术领域。与编程语言相比,内存管理关系到编写内存高效的代码。在实现大量数据时,我们不能忽视内存管理的重要性。不当的内存管理会导致应用程序和服务器端组件变慢。它也成为工作不正常的原因。如果内存处理不当,预处理数据时会花费很多时间。 在 Python 中,内存由 Python 管理器管理,该管理器决定将应用程序数据放在内存的哪个位置。因此,我们必须了解 Python 内存管理器,以编写高效且可维护的代码。 让我们假设内存就像一本空书,我们想在书的页面上写任何东西。然后,我们写入任何数据,管理器会在书中找到可用空间并将其提供给应用程序。将内存提供给对象的过程称为分配。 另一方面,当数据不再使用时,Python 内存管理器可以将其删除。但问题是,如何删除?这块内存又是从哪里来的? Python 内存分配内存分配是开发者内存管理的重要组成部分。这个过程基本上是在计算机的虚拟内存中分配可用空间,执行程序时会用到两种类型的虚拟内存。
静态内存分配 -静态内存分配发生在编译时。例如,在 C/C++ 中,我们声明一个固定大小的静态数组。内存在编译时分配。但是,我们不能在后续程序中再次使用该内存。 栈分配 栈数据结构用于存储静态内存。它只在特定的函数或方法调用内部需要。每当我们调用一个函数时,该函数就会被添加到程序的调用栈中。函数内的变量赋值会临时存储在函数调用栈中;当函数返回值时,调用栈会移至下一个任务。编译器会处理所有这些过程,所以我们不需要担心它。 调用栈(栈数据结构)按调用顺序保存程序的运行数据,如子程序或函数调用。当我们调用这些函数时,它们会从栈中弹出。 动态内存分配与静态内存分配不同,动态内存在运行时为程序分配内存。例如,在 C/C++ 中,整数或浮点数数据类型有预定义的大小,但 Python 的数据类型没有预定义的大小。内存在运行时分配给对象。我们使用堆来实现动态内存管理。我们可以在整个程序中使用该内存。 众所周知,Python 中的一切都是对象,这意味着动态内存分配启发了 Python 的内存管理。当对象不再使用时,Python 内存管理器会自动清除它。 堆内存分配 堆数据结构用于动态内存,与命名对应物无关。它是一种在程序外部的全局空间中使用的内存类型。堆内存的最大优点之一是,如果对象不再使用或节点被删除,它会释放内存空间。 在下面的例子中,我们定义了函数的变量如何存储在栈和堆中。 默认的 Python 实现Python 是一种开源的、面向对象的编程语言,其默认实现是用 C 语言编写的。这是一个非常有趣的事实——一种最流行的语言是用另一种语言编写的?但这不完全是真的,但有点像。 基本上,Python 语言是用英语编写的。然而,它在参考手册中定义,本身并没有用处。所以,我们需要一个基于手册中规则的解释器代码。 默认实现的好处是,它可以在计算机中执行 Python 代码,并且它还将我们的 Python 代码转换为指令。所以,我们可以说 Python 的默认实现满足了这两个要求。 注意 - 虚拟机不是物理计算机,但它们是在软件中实现的。我们用 Python 语言编写的程序首先会转换成计算机相关的指令——字节码。虚拟机解释这个字节码。 Python 垃圾回收器正如我们前面解释的,Python 会移除那些不再使用的对象,或者说它会释放内存空间。这个清除不必要对象内存空间的过程称为垃圾回收。Python 垃圾回收器随程序一起启动执行,并在引用计数降至零时被激活。 当我们分配新名称或将其放入容器(如字典或元组)时,引用计数会增加。如果我们重新分配对象的引用,引用计数会减少。当对象的引用超出作用域或对象被删除时,它的值也会减少。 众所周知,Python 使用由堆数据结构管理的动态内存分配。内存堆保存程序中将要使用的对象和其他数据结构。Python 内存管理器通过 API 函数管理堆内存空间的分配或释放。 Python 对象在内存中众所周知,Python 中的一切都是对象。对象可以是简单的(包含数字、字符串等)或容器(字典、列表或用户自定义的类)。在 Python 中,我们不需要在使用变量之前声明它们或它们的类型。 让我们理解下面的例子。 示例 - 输出 10 Traceback (most recent call last): File " 从上面的输出中可以看出,我们给对象 x 赋了值并打印了它。当我们移除对象 x 并尝试在后续代码中访问它时,会出现一个错误,声称变量 x 未定义。 因此,Python 垃圾回收器是自动工作的,程序员不需要像 C 语言那样担心它。 Python 中的引用计数引用计数说明一个对象被其他对象引用了多少次。当分配一个对象的引用时,该对象的计数加一。当一个对象的引用被移除或删除时,该对象的计数减一。当引用计数变为零时,Python 内存管理器执行释放操作。让我们简单地理解一下。 示例 - 假设有两个或更多变量包含相同的值,那么 Python 虚拟机不会在私有堆中创建另一个具有相同值的对象。它实际上是让第二个变量指向私有堆中已经存在的值。 这对于节省内存非常有利,这些内存可以被其他变量使用。 ![]() 当我们给 x 赋值时,整数对象 10 在堆内存中被创建,并且它的引用被赋给 x。 ![]() 在上面的代码中,我们赋值 y = x,这意味着 y 对象将引用同一个对象,因为如果具有相同值的对象已经存在,Python 会将相同的对象引用分配给新变量。 现在,看另一个例子。 ![]() 示例 - 输出 x and y do not refer to the same object 变量 x 和 y 没有引用同一个对象,因为 x 增加了 1,x 创建了一个新的引用对象,而 y 仍然引用 10。 改变垃圾回收器Python 垃圾回收器使用其代(generation)来对对象进行分类。Python 垃圾回收器有三代。当我们在程序中定义一个新对象时,它的生命周期由垃圾回收器的第一代处理。如果该对象在不同的程序部分中使用,它将被提升到下一代。每一代都有一个阈值。 如果分配次数减去释放次数的差值超过了阈值,垃圾回收器就会启动。 我们可以使用 GC 模块手动修改阈值。该模块提供了 get_threshold() 方法来检查垃圾回收器不同代的阈值。让我们看下面的例子。 示例 - 输出 (700, 10, 10) 在上面的输出中,阈值 700 是第一代的,其他值是第二代和第三代的。 可以使用 set_threshold() 方法修改触发垃圾回收器的阈值。 示例 - 2 在上面的例子中,所有三代的阈值都增加了。这将影响垃圾回收器运行的频率。程序员不需要担心垃圾回收器,但它在为目标系统优化 Python 运行时方面起着至关重要的作用。 Python 垃圾回收器为开发者处理了底层的细节。 执行手动垃圾回收的重要性正如我们之前讨论的,Python 解释器处理程序中使用的对象引用。当引用计数变为零时,它会自动释放内存。这是一种经典的引用计数方法,但当程序出现引用循环时,它会失效。当一个或多个对象相互引用时,就会发生引用循环。因此,引用计数永远不会变为零。 让我们理解以下示例 - 我们创建了一个引用循环。list1 对象引用了 list1 对象本身。当函数返回对象 list1 时,对象 list1 的内存没有被释放。所以引用计数不适合解决引用循环问题。但是,我们可以通过改变垃圾回收器或垃圾回收器的性能来解决它。 为了实现这一点,我们将使用 gc 模块的 gc.collect() 函数。 上面的代码将给出被收集和释放的对象数量。 我们可以使用两种方法执行手动垃圾回收——基于时间的或基于事件的垃圾回收。 gc.collect() 方法用于执行基于时间的垃圾回收。该方法在固定的时间间隔后调用,以执行基于时间的垃圾回收。 在基于事件的垃圾回收中,gc.collect() 函数在事件发生后调用。让我们看下面的例子。 示例 - 输出 Here, we are creating garbage... Collecting the object... Number of unreachable objects collected by GC: 10 Uncollectable garbage: [] 在上面的代码中,我们创建了由 list 变量引用的 list1 对象。列表对象的第一个元素引用其自身。即使在程序中被删除或超出作用域,列表对象的引用计数也总是大于零。 C Python 内存管理在本节中,我们将详细讨论 C Python 的内存架构。 正如我们之前讨论的,从物理硬件到 Python 有一个抽象层。各种应用程序或 Python 访问由操作系统创建的虚拟内存。 Python 使用一部分内存用于内部使用和非对象内存。另一部分内存用于 Python 对象,如 int、dict、list 等。 CPython 包含一个对象分配器,它在对象区域内分配内存。每当新对象需要空间时,都会调用对象分配器。该分配器主要为少量数据设计,因为 Python 一次不涉及太多数据。它在绝对需要时才分配内存。 CPython 内存分配策略有三个主要组成部分。 Arena - 它是最大的内存块,在内存中按页边界对齐。操作系统使用页边界,即固定长度连续内存块的边缘。Python 假设系统的页大小为 256KB。 Pools - 它由单一大小的类组成。相同大小的池管理一个双向链表。一个池必须是 - used(使用中)、full(已满)或 empty(空)。一个 used 池包含用于存储数据的内存块。一个 full 池的所有内存块都已分配并包含数据。一个 empty 池没有任何数据,可以在需要时分配任何大小类的块。 Blocks - 池包含一个指向其“空闲”内存块的指针。在池中,有一个指针指示空闲的内存块。分配器在实际需要之前不会接触这些块。 降低空间复杂度的常用方法我们可以遵循一些最佳实践来降低空间复杂度。这些技术应该可以节省相当大的空间并使程序高效。以下是 Python 内存分配器的一些实践。
当我们在 Python 中定义一个列表时,内存分配器会根据列表索引分别在堆上分配内存。假设我们需要一个给定列表的子列表,那么我们执行列表切片。这是从原始列表中获取子列表的直接方法。在某种程度上,它适用于少量数据,但不适用于大量数据。 因此,列表切片会生成列表中对象的副本。它只是复制对它们的引用。结果,Python 内存分配器会创建对象的副本并为其分配内存。所以我们需要避免列表切片。 避免这种情况的最佳方法是,开发者应该尝试使用单独的变量来跟踪索引,而不是对列表进行切片。
开发者应该尝试使用 "for item in array" 而不是 "for index in range(len(array))" 来节省空间和时间。如果我们的程序不需要列表元素的索引,那就不要使用它。
字符串拼接不适合节省空间和时间复杂度。在可能的情况下,我们应该避免使用 '+' 进行字符串拼接,因为字符串是不可变的。当我们将新字符串添加到现有字符串时,Python 会创建新字符串并将其分配到新地址。 每个字符串根据字符及其长度需要固定大小的内存。当我们更改字符串时,它需要不同数量的内存并需要重新分配。 让我们运行下面的例子。 输出 Mango Mango Ice-cream 它将创建变量 a 来引用字符串对象,即字符串值信息。 然后我们使用 '+' 运算符在其中添加新字符串。Python 会根据其大小和长度在内存中重新分配新字符串。假设原始字符串的内存大小为 n 字节,那么新字符串将是 m 字节。 除了使用字符串拼接,我们可以使用 ".join(iterable_object)" 或 format 或 %。这对节省内存和时间有巨大影响。
在处理大量数据时,迭代器对于时间和内存都非常有帮助。处理大型数据集时,我们需要立即进行数据处理,不能等待程序先处理完整个数据集。 生成器是用于创建迭代器函数的特殊函数。 在下面的例子中,我们实现一个调用特殊生成器函数的迭代器。yield 关键字返回当前值,仅在循环的下一次迭代时才移至下一个值。 示例 -
如果我们使用 Python 库中已经预定义的方法,那么就导入相应的库。这将节省大量空间和时间。我们也可以创建一个模块来定义函数,并将其导入到当前工作的程序中。 结论在本教程中,我们讨论了 Python 内部内存的工作原理。我们学习了 Python 如何管理内存,并讨论了默认的 Python 实现。CPython 是用 C 编程语言编写的。Python 是一种动态类型语言,使用堆数据结构来存储内存。 下一个主题用于数据可视化的 Python 库 |
我们请求您订阅我们的新闻通讯以获取最新更新。