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中的引用计数

引用计数表示其他对象引用一个对象的次数。当给对象分配一个引用时,对象的计数增加1。当对象的引用被删除时,计数减1。当引用计数变为零时,Python内存管理器执行解分配。让我们简单理解一下。

示例

假设有两个或更多变量包含相同的值,那么Python虚拟机不会在私有堆中创建另一个相同值的对象。实际上,它将第二个变量指向私有堆中已经存在的值。

这对于保留内存非常有益,可以供另一个变量使用。

x = 20

Python内存管理

当我们给x赋值时,整数对象10被创建在堆内存中,并将其引用分配给x。

x = 20
y = x 
if id(x) == id(y): 
    print("The variables x and y are referring  to the same object")

Python内存管理

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

现在,看另一个示例。

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: []

在上面的代码中,我们创建了由列表变量引用的list1对象。列表对象的第一个元素指向它本身。即使在程序中删除或超出范围时,列表对象的引用计数始终大于零。

C Python内存管理

在这一部分中,我们将详细讨论C Python的内存架构。

正如我们之前讨论过的,存在从物理硬件到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会创建新字符串并将其分配给新地址。

每个字符串基于字符和长度都需要固定大小的内存。当我们改变字符串时,它需要不同大小的内存并需要重新分配。

让我们运行以下示例。

a = Mango
print(a)
a = a + " Ice-cream"
print (a)

输出:

Mango
Mango Ice-cream

它将创建变量 a 来引用字符串对象,该对象包含字符串值信息。

然后,我们使用”+”运算符在其中添加新的字符串。Python根据其大小和长度在内存中重新分配新的字符串。假设原始字符串的内存大小为n字节,则新字符串将为m字节。

我们可以使用” **.join(iterable_object)”、format或%来代替字符串连接。这对节省内存和时间产生了巨大影响。

  • 使用迭代器和生成器

迭代器在处理大量数据时非常有帮助,既可以节省时间又可以节省内存。在处理大型数据集时,我们需要立即进行数据处理,而不必等待程序先处理整个数据集。

生成器是用于创建迭代器函数的特殊函数。

在下面的示例中,我们实现了一个调用特殊生成器函数的迭代器。关键字 yield 返回当前值,仅在循环的下一次迭代时移动到下一个值。

示例

def __iter__(self):
    ''' This function allows are set to be iterable. Element can be looped over using the for loop'''
    return self. _generator()

def _generator(self):
    """ This function is used to implement the iterable. It stores the data we are currently on and gives the                      next item at each iteration of the loop."""
    for i in self.items():
        yield i
  • 尽可能使用内置库

如果我们使用已在Python库中预定义的方法,则导入相应的库。这样可以节省很多空间和时间。我们还可以创建一个模块来定义函数并将其导入到当前工作程序中。

结论

在本教程中,我们讨论了Python内部的内存工作原理。我们学习了Python如何管理内存,并讨论了默认的Python实现。CPython是用C编程语言编写的。Python是一种动态类型的语言,使用堆数据结构来存储内存。

Camera课程

Python教程

Java教程

Web教程

数据库教程

图形图像教程

办公软件教程

Linux教程

计算机教程

大数据教程

开发工具教程