在这个教程中,我们将学习 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  

144-1.png

当我们给 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")  

144-2.png

在上面的代码中,我们分配了 y = x,这意味着 y 对象将引用相同的对象,因为如果对象已经存在并且具有相同的值,则 Python 将新变量引用指向相同的对象。

现在,看另一个例子。

144-3.png

示例 -

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 代码。

标签: Tkinter教程, Tkinter安装, Tkinter库, Tkinter入门, Tkinter学习, Tkinter入门教程, Tkinter, Tkinter进阶, Tkinter指南, Tkinter学习指南, Tkinter进阶教程, Tkinter编程