Python教程-Python 内存管理

在这个教程中,我们将学习 Python 如何管理内存,或者说 Python 如何内部处理我们的数据。我们将深入探讨这个主题,以了解 Python 的内部工作方式以及它如何处理内存。
本教程将深入理解 Python 的内存管理。当我们执行 Python 脚本时,Python 内部会有许多逻辑运行,以使代码更加高效。
介绍
对于软件开发人员来说,内存管理对于有效地使用任何编程语言都非常重要。正如我们所知,Python 是一种著名且广泛使用的编程语言。它几乎在每个技术领域都有应用。与编程语言相反,内存管理与编写内存高效的代码相关。在实施大量数据时,我们不能忽视内存管理的重要性。不正确的内存管理会导致应用程序和服务器端组件变慢。它还会导致不正常的工作。如果内存处理不好,会在预处理数据时花费很多时间。
在 Python 中,内存由 Python 管理器管理,该管理器确定在内存中放置应用程序数据的位置。因此,我们必须了解 Python 内存管理器的知识,以编写高效且可维护的代码。
让我们假设内存看起来像一本空白的书,我们想在书的页面上写点什么。然后,我们写入数据,管理器会找到书中的空闲空间并将其提供给应用程序。向对象提供内存的过程称为分配。
另一方面,当数据不再使用时,Python 内存管理器可以删除它。但问题是,如何删除?内存从哪里来?
Python 内存分配
内存分配是开发人员内存管理的重要部分。此过程基本上为计算机的虚拟内存分配了空闲空间,执行程序时有两种类型的虚拟内存在工作。
- 静态内存分配
- 动态内存分配
静态内存分配 -
静态内存分配发生在编译时。例如 - 在 C/C++ 中,我们声明一个带有固定大小的静态数组。内存在编译时分配。但是,我们不能在以后的程序中再次使用该内存。
static int a=10;
堆栈分配
堆栈数据结构用于存储静态内存。它仅在特定函数或方法调用内部需要。每当我们调用它时,该函数将添加到程序的调用堆栈中。函数内部的变量分配在函数调用堆栈中临时存储;函数返回值,调用堆栈移动到文本任务。编译器处理所有这些过程,因此我们不需要担心它。
调用堆栈(堆栈数据结构)保存程序的操作数据,例如按照它们应该被调用的顺序的子例程或函数调用。这些函数在调用时从堆栈中弹出。
动态内存分配
与静态内存分配不同,动态内存在程序运行时分配给程序。例如 - 在 C/C++ 中,整数或浮点数据类型有预定义的大小,但数据类型没有预定义的大小。对象在运行时分配内存。我们使用堆 来实现动态内存管理。我们可以在整个程序中使用内存。
int *a;
p = new int;
正如我们所知,Python 中的一切都是对象,这意味着动态内存分配激发了 Python 内存管理。当对象不再使用时,Python 内存管理器会自动清除它。
堆内存分配
堆数据结构用于动态内存,与命名对应物无关。堆内存的最大优点之一是,如果对象不再使用或节点被删除,它将释放内存空间。
在下面的示例中,我们定义了如何在堆栈和堆中存储函数的变量。
默认的 Python 实现
Python 是一种开源、面向对象的编程语言,其默认实现是使用 C 编程语言编写的。这是一个非常有趣的事实 - 一种最流行的语言是用另一种语言编写的?但这并不完全是真的,但有点类似。
基本上,Python 语言是用英语编写的。但是,在参考手册中定义了一种不会自己使用的语言。因此,我们需要一个基于手册规则的解释器。
默认实现的好处是它在计算机上执行 Python 代码,并将我们的 Python 代码转换为指令。因此,我们可以说 Python 的默认实现同时满足了这两个要求。
注意 - 虚拟机不是物理计算机,而是在软件中启动的。
我们使用 Python 语言编写的程序首先会转换成计算机相关的指令字节码。虚拟机解释这个字节码。
Python 垃圾收集器
正如我们之前所解释的,Python 删除那些不再使用的对象,或者可以说它释放了内存空间。这个消除不必要对象内存空间的过程称为垃圾收集器。Python 垃圾收集器在程序启动时开始执行,并且在引用计数降为零时被激活。
当我们分配新名称或将其放入字典或元组等容器中时,引用计数会增加其值。如果重新分配对象的引用,引用计数会减少其值。当对象的引用超出作用域或对象被删除时,引用计数也会减少其值。
正如我们所知,Python使用动态内存分配,由堆数据结构管理。Python 内存管理器通过 API 函数来管理堆内存空间的分配或释放。
Python 内存中的对象
正如我们所知,Python 中的一切都是对象。对象可以是简单的(包含数字、字符串等)或容器(字典、列表或用户定义的类)。在 Python 中,我们不需要在程序中使用变量或它们的类型之前声明它们。
让我们理解以下示例。
示例 -
a= 10
print(a)
del a
print(a)
输出:
10
Traceback (most recent call last):
File "", line 1, in
print(x)
NameError : name 'a' is not defined
如上面的输出所示,我们给对象 x 分配了值,并打印了它。当我们删除对象 x 并尝试在后续代码中访问它时,将出现一个错误,称变量 x 未定义。
因此,Python 垃圾收集器会自动工作,程序员不需要担心它,不像 C 那样需要手动管理内存。
Python 中的引用计数
引用计数表示其他对象引用一个对象的次数。当给对象分配引用时,对象的计数会增加一次。当对象的引用被移除或删除时,对象的计数会减少。Python 内存管理器在引用计数变为零时执行释放内存。让我们简化理解。
示例 -
假设有两个或更多变量包含相同的值,因此 Python 虚拟机不会在私有堆中创建相同值的另一个对象。实际上,它使第二个变量指向私有堆中原始存在的值。
x = 20
当我们给 x 分配值时,Heap 内存中创建了整数对象 10,并将其引用分配给 x。
x = 20
y = x
if id(x) == id(y):
print("The variables x and y are referring to the same object")
在上面的代码中,我们分配了 y = x,这意味着 y 对象将引用相同的对象,因为如果对象已经存在并且具有相同的值,则 Python 将新变量引用指向相同的对象。
现在,看另一个例子。
示例 -
x = 20
y = x
x += 1
If id(x) == id(y):
print("x and y do not refer to the same object")
输出:
x and y do not refer to the same object
变量 x 和 y 不引用相同的对象,因为 x 增加了 1,x 创建了新的引用对象,而 y 仍然引用 10。
调整垃圾收集器
Python 垃圾收集器通过其生成对对象进行分类。Python 垃圾收集器有三代。当我们在程序中定义新对象时,垃圾收集器的第一代处理它的生命周期。如果对象在不同程序中有用,它将被提升到下一代。每一代都有一个阈值。
只有当分配数减去取消分配数的阈值超过时,垃圾收集器才会启动。
我们可以使用 GC 模块手动修改阈值值。此模块提供 get_threshold() 方法来检查垃圾收集器不同代的阈值值。让我们看以下示例。
示例 -
Import GC
print(GC.get_threshold())
输出:
(700, 10, 10)
在上面的输出中,阈值值 700 用于第一代,其他值用于第二代和第三代。
可以使用 set_threshold() 方法手动修改触发垃圾收集器的阈值值。
示例 - 2
import gc
gc.set_threshold(800, 20, 20)
在上面的示例中,提高了所有三代的阈值值。这将影响垃圾收集器运行的频率。程序员不需要担心垃圾收集器,但它在优化 Python 运行时的目标系统方面发挥着重要作用。
Python 垃圾收集器处理开发人员的低级细节。
手动执行垃圾收集的重要性
正如我们之前所讨论的,Python 解释器处理程序中使用的对象的引用。当引用计数变为零时,它会自动释放内存。这是一种经典的引用计数方法,但在程序引用循环时无法正常工作。引用循环发生在一个或多个对象相互引用的情况下。因此,引用计数永远不会变为零。
让我们理解以下示例 -
def cycle_create():
list1 = [18, 29, 15]
list1.append(list1)
return list1
cycle_create()
[18, 29, 15, [...]]
我们创建了一个引用循环。list1 对象引用它自己的对象。当函数返回对象 list1 时,对象 list1 的内存不会被释放。因此,引用计数不适用于解决引用循环。但是,我们可以通过修改垃圾收集器或改进垃圾收集器的性能来解决它。
为此,我们将使用 gc 模块的 gc.collect() 函数。
import gc
n = gc.collect()
print("Number of object:", n)
上面的代码将提供已收集和已释放的对象数量。
我们可以使用两种方法手动执行垃圾收集 - 基于时间或基于事件的垃圾收集。
gc.collect() 方法用于执行基于时间的垃圾收集。此方法在固定时间间隔后调用以执行基于时间的垃圾收集。
在基于事件的垃圾收集中,事件发生后调用 gc.collect() 函数。让我们看以下示例。
示例 -
import sys, gc
def cycle_create():
list1 = [18, 29, 15]
list1.append(list1)
def main():
print("Here we are creating garbage...")
for i in range(10):
cycle_create()
print("Collecting the object...")
num = gc.collect()
print("Number of unreachable objects collected by GC:", num)
print("Uncollectable garbage:", gc.garbage)
if __name__ == "__main__":
main()
sys.exit()
输出:
Here, we are creating garbage...
Collecting the object...
Number of unreachable objects collected by GC: 10
Uncollectable garbage: []
在上面的代码中,我们创建了由 list 变量引用的 list1 对象。列表对象的引用计数始终大于零,即使它被删除或在程序中超出作用域。
CPython 内存管理
在本节中,我们将详细讨论 CPython 内存架构。
正如我们之前所讨论的,从物理硬件到 Python 之间存在一层抽象。各种应用程序或 Python 访问操作系统创建的虚拟内存。
Python 使用内存的一部分用于内部使用和非对象内存。另一部分内存用于 Python 对象,例如 int、dict、list 等。
CPython 包含了分配对象的对象分配器。对象分配器每次新对象需要空间时都会得到调用。分配器主要用于小数据量,因为 Python 不会一次处理太多数据。只有在绝对需要时才会分配内存。
CPython 内存分配策略有三个主要组成部分。
Arena - 这是内存中最大的块,并且在内存中对齐到页边界。操作系统使用页边界,这是一块固定长度的连续内存块的边缘。Python 假设系统的页大小为 256 千字节。
Pools - 它由单个大小类组成。相同大小类的池管理一个双链列表。池可以是已使用、已满或空的。一个 已使用 的池包含用于存储数据的内存块。一个 已满 的池包含所有已分配的内存块并包含数据。一个空池没有任何数据,并且可以在需要时为块分配任何大小类。
块 - 池包含对它们的内存块的指针。在池中,有一个指针,指示内存块的空闲块。分配器在实际需要时不会触及这些块。
减少空间复杂性的常见方法
我们可以遵循一些最佳实践来减少空间复杂性。这些技巧旨在节省相当多的空间并使程序更高效。以下是 Python 中的一些内存分配器的最佳实践。
- 避免列表切片
在 Python 中定义列表时,内存分配器会根据列表索引分配堆内存,因此我们需要避免使用列表切片。这是从原始列表中获取子列表的简单方法。它对于小数据量来说很适用,但对于大数据量来说不适用。
因此,列表切片生成对象的副本。它只复制对它们的引用。结果,Python 内存分配器创建对象的副本并分配给新地址。因此,我们需要避免使用列表切片。
最好的方法是避免开发人员应尽量使用单独的变量来跟踪索引,而不是切片列表。
- 谨慎使用列表索引
开发人员应该尽量使用“for item in array”而不是“for index in range(len(array))”来节省空间和时间。如果程序不需要列表元素的索引,则不要使用它。
- 字符串连接
字符串连接不适合节省空间和时间复杂性。可能的情况下,我们应该避免使用 '+' 进行字符串连接,因为字符串是不可变的。当我们将新字符串添加到现有字符串时,Python 会创建新字符串并分配给新地址。
每个字符串需要根据字符及其长度的大小分配内存。因此,连接字符串的代价很高。
为了更好的性能,我们应该使用 join() 方法来连接字符串。join() 方法会将所有字符串连接到一个地方并一次分配内存。
- 避免不必要的全局变量
全局变量存储在堆中。它们会增加全局对象的引用计数,并且在程序退出时需要收集。
因此,我们需要避免使用不必要的全局变量。
- 使用列表推导式
列表推导式非常有用,因为它们可以在一行中创建列表。它是一种更高效的方式,因为它只分配一次内存。
结论
在 Python 中,内存管理是非常重要的,它会直接影响代码的性能和可维护性。Python 有一个内存管理器来处理对象的分配和释放,但了解内存管理的基本原理和最佳实践是编写高效 Python 代码的关键。
通过引用计数、垃圾收集器以及 CPython 内存分配器的内部工作原理,我们可以更好地理解 Python 的内存管理机制。同时,遵循一些内存管理的最佳实践可以帮助我们减少内存使用,提高代码性能。
希望这个教程能帮助你更好地理解 Python 的内存管理,使你能够编写更高效和可维护的 Python 代码。