在传统的编程语言中,变量通常会被认为是被命名的内存位置。如果把这个想法应用于Python的话,你可能就会认为Python里的变量是某种小型的、与计算机内存中可以存储对象的位置相对应的东西。这种思维方式对于简单的程序来说非常有效,但对于Python实际管理事物的方式来说,这并不是一个非常准确的表述。因此,为了避免和其他语言相混淆,一些人更喜欢在Python里用名称(name)来代表,而不是使用传统的术语——变量(variable)

Python里的名称始终指的是存储在内存中的某个对象。当把Python的名称分配给一个对象时,Python解释器将在内部使用字典,将这个名称映射到存储对象的实际内存位置。这个维护从名称到对象的映射的字典称为命名空间。如果在之后把这个名称分配给另一个对象,命名空间字典就会被修改,从而能够将这个名称映射到新的内存位置。我们将通过一个交互式的案例来演示“幕后发生的事情”。我知道,这个细节有些单调无聊,但当你完全理解这些知识之后,你将会更容易地理解后面将会讨论的许多主题。

让我们从几个简单的赋值语句开始:

执行语句d='Dave'时,Python会分配一个包含Dave的字符串对象。赋值语句j=d则会使名称j引用到与名称d相同的对象。它不会去创建新的字符串对象。一个很好的类比是:

赋值可以被视为将一个带有名称的便签放在对象上。

这个时候,数据对象Dave上会有两个粘着的便笺:一个名称为d,另一个名称为j。

图4.1所示应该有助于你理解正在发生的事情。在这个图里,我们使用箭头作为直观的方式来表示“值”。而计算机实际存储的数字是箭头所指向的地址

当然,Python解释器并不是用的便笺纸,而是使用命名空间字典,在内部跟踪这些关联。我们其实可以用名为本地(locals)的内置函数来访问这个命名空间字典:

d='Dave'
j=d
print(locals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001FD219908B0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:/pythonProject/main.py', '__cached__': None, 'd': 'Dave', 'j': 'Dave'}

在这个例子里,你可以看到本地字典包含着一些你可能认识的Python的特殊名称:builtinsname__以及__doc。在这里,我们并不需要去关心这些特殊名称。然而,应该注意的是,我们的赋值语句将两个名称d和j添加到了字典里。可以看出,在输出这个字典的时候,Python会把名称显示为键,并将实际数据对象的存储形式显示为值。请记住,命名空间字典实际上存储的是对象的内存地址(也称为对象的引用)。但是,由于我们通常关心的都是数据,而不是它的内存地址,因此Python解释器会自动向我们显示存储在这个内存地址中的内容,而不是这个地址本身。

当然,如果好奇的话,还是会想要知道一个对象的实际地址。我们是可以做到这一点的,Python里的id函数将会返回每个对象的唯一标识符。而在大多数的Python版本里,id函数会返回存储对象的内存地址:

print(id(d))
print(id(j))
2336958556848
2336958556848

就像你可以通过id函数的输出所看到的那样,在赋值语句j=d之后,名称j和d都引用着相同的数据对象。在内部,Python解释器会持续地跟踪,现在有两个引用指向了Dave字符串对象。这通常被称为对象的引用计数(reference count).

让我们写更多的命令来继续这个例子:

j='John'
print(id(d))
print(id(j))d='Smith'
print(id(d))
print(id(j))2336958556848
2336958556272
2336958556784
2336958556272

当我们执行j='John’的时候,就会创建一个包含John的新字符串对象。还是用我们的便笺纸来作为类比,相当于我们把便笺j移动到了另一个包含字符串John的新创建的数据对象上。可以看到,语句j='John’之后的id函数的输出显示,名称d仍然引用之前相同的对象,但名称j现在引用了另一个不同的内存位置的对象。在这个时候,两个字符串对象中每个字符串对象的引用计数将会是1。

之后的语句d='Smith’将会让名称d引用包含Smith的新字符串对象。要注意看的是,字符串对象Smith的地址和字符串对象Dave的地址并不相同。同样,当名称被分配给不同的对象的时候,名称所映射到的地址也会更改。这是一个需要注意的重点:赋值会改变变量引用的对象,但是它对对象本身没有任何影响。在这种情况下,字符串Dave并不会被修改为字符串Smith,而是会创建一个包含Smith的新字符串对象。

在这个时候,由于没有任何名称在引用字符串Dave,因此它的引用计数现在是零。因为无法再访问包含Dave的字符串对象,Python解释器会自动释放这个地方的内存。通过释放无法再被访问的对象(当它们的引用计数变为零时),Python解释器就能够在以后为新对象重新使用相同的内存位置。此过程被称为垃圾回收(garbage collection)。垃圾回收会增加Python解释器的开销,从而会减慢执行速度。但是它所能提供的好处是:它减轻了程序员对内存分配和释放的相关负担的担心。这个过程在没有自动内存管理的语言中会非常地糟糕且容易出错。

当然,程序员也可以显式删除给定名称的映射:

语句del d将会从命名空间字典里删除名称d,这样你就再也不能访问它了。如果你再去尝试执行语句print d的话,就好像我们从没有把对象分配给d一样,会导致抛出NameError异常。删除这个名称会把字符串Smith的引用计数从1减少到0,因此,它也会被垃圾回收。

Python的这个内存模型有很多好处
因为变量只会包含对象的引用,所以所有变量的大小都相同(计算机的标准地址大小通常为4字节或8字节)。而且,数据类型信息会和对象一起存储,这一技术的术语叫作动态类型(dynamic typing)。这也就意味着同一个名称可以在程序执行的时候引用不同的类型,而且名称能够被重新分配。这也就使得诸如列表、元组和字典这类的容器非常容易实现对异质(包含多种类型)的支持,因为它们也只是去维护它所包含对象的(地址)的引用。Python的这个内存模型也使得赋值操作非常高效。Python里的赋值表达式将会始终被用来操作对某个对象的引用。将结果分配给名称只需要把名称这个四字节或八字节的引用添加到命名空间字典(如果它还不存在的话)。在像j=d这样的简单赋值中,它的效果就是将d的引用复制到j的命名空间条目而已。

现在我们可以很清晰地知道,Python的内存模型能够使多个名称引用完全相同的对象,而且这个操作(通常来说)非常简单易行。这称为别名(aliasing),这个别名可能会导致一些有趣的情况。例如,当多个名称引用同一对象的时候,通过其中一个名称对对象进行修改将会修改所有名称引用的数据。在这个时候,访问其他名称也可以看到使用这个名称对数据的更改。这里有一个使用列表来做的简单说明:

lst1=[1,2,3]
lst2=lst1
lst2.append(4)
print(lst1) #[1, 2, 3, 4]

由于lst1和lst2引用了同一个对象,因此向lst2追加4也会影响到lst1。除非你理解了这里潜在的语义,否则lst1就好像是“神奇地”被改变了一样,因为从交互式操作的第一行到最后一行之间,我们并没有对lst1进行任何修改。当然,只有共享的对象是可以被改变(非贫血类型)的时候,这些令人惊讶的混叠结果才会出现。字符串(string)、整数(int)和浮点数(float)这类的变量根本无法改变,所以对于这些类型,用别名不会产生问题。

想要避免使用别名时所产生的副作用,就需要获得一个对象的独立副本,从而能够在不影响其他副本的情况下对这个副本进行更改。当然,诸如列表之类的复杂对象,它本身可能也会包含对其他对象的引用,因此我们必须要斟酌如何在复制过程中处理这些引用。有两种不同类型的复制,它们分别被称为浅拷贝(shallowcopy)深拷贝(deep copy)。浅拷贝的副本会有自己单独的引用,但这个引用所引用的对象是和原始对象相同的对象。深拷贝的副本则会是一个完全独立的副本,它可以被用来创建一个新的对象引用,并且创建每一级所有的新的数据对象。Python的复制(copy)模块包含非常有用的函数,能够被用来复制任意Python对象。下面是一个使用列表来进行演示的交互式操作示例:

import copy
b=[1,2,[3,4],6]
c=b
d=copy.copy(b)# 浅拷贝
e=copy.deepcopy(b)
print(b is c) #true
print(b==c) #true
print( b is d) #false
print(b==d) #true
print(b is e) # false
print(b==e)#true

在这段代码里,c和b是同一个引用,d是浅拷贝,e则是深拷贝。顺便说一下,有很多方法都能够获得Python列表的浅拷贝副本。比如,我们也可以使用切片(d=b[:])或者列表的构造函数(d=list(b))来创建浅拷贝副本。那么,这段代码的输出都代表着什么呢?Python的“是(is)”运算符会检测左右两个表达式是否引用的是完全相同的一个对象,而Python的“比较()”运算符则会检测两个表达式是否有相同的数据。这就是说:a is b可以得出ab,但是,反之并不成立。

在这段示例里可以看出,赋值并不会创建新的对象,因为在初始赋值后b is c。但是,通过切片来创建的浅拷贝d以及深拷贝得到的e都是包含与b相等数据的不同的新对象。虽然这些副本都包含着相同的数据,但它们的内部结构并不相同。
浅拷贝只会包含列表最顶层引用的副本,而深拷贝则会包含所有级别里的可变部分的结构的副本,如图4.2所示。

要注意的是,深拷贝并不需要去复制不可变的数据项,因为就像之前提到过的一样,不可变对象的别名并不会引起任何特殊的问题。

由于在浅拷贝中还残留有数据共享,我们仍然会得到混叠副作用。可以考虑一下,当我们开始修改其中一些列表时会发生什么:


根据图4.2,你应该能够理解这里的输出结果是怎么产生的了。修改b引用的列表的顶级结构会导致对象c的改变,这是因为它引用了同一个对象。而这个对顶级结构的更改对d或e则没有影响,因为它们引用的是一个从b拷贝出来的独立的副本这一单独对象。


然而,当我们修改c的子列表[3, 4]的时候,事情就有趣起来了。很明显,b也会得到这些变化(因为b和c是同一个对象)。但是,现在d也看到了这些变化,这是因为这个子列表仍然是浅拷贝里的共享子结构。与此同时,深拷贝e并没有看到任何和这些相关的修改,这是由于所有可变结构都已经在每个级别上被复制,因此b所引用的对象的修改并不会影响到它。图4.3显示了内存在这个例子结束时的样子。


在最后,我们在这一节内容中所使用的完整的、基于引用的图表会占用大量空间,以至于有些时候变得非常难以解释。而由于引用和值之间的区别在不可变对象的情况下并不重要,为了能够使这个图表尽可能简单,当它们被包含在另一个对象里的时候,我们通常不会将不可变对象作为单独的数据对象进行绘制。图4.4以更紧凑的方式绘制了这个例子在结束时候的内存的样子,显示了与图4.3相同的情况。

python的内存模型相关推荐

  1. python对象内存模型

    原文:Python的垃圾回收机制(二)之内存模型 1. Python对象内存模型 首先介绍一下Python对象的内存模型,如下图1所示: 图1. PyObject对象内存模型 上图可以看到,一个PyO ...

  2. python内存模型_内存篇3:CPython的内存管理架构-L2-块

    本篇用到了C/C++的内存对齐的基础知识,我已经假定你有C/C++内存管理的相关基础. 我们在前一篇的流程图中留下了两个黑箱子,会涉及到内存模型第一层以上的其他话题,回顾下面关于第一层面向类型的内存A ...

  3. Python内存模型

    在Python种,一切皆是对象.对象的存储放在堆内存中. 1.内存表(memory table)和变量表(variable table) 在Python中内存使用内存表来表示.内存表告诉我们数据在堆内 ...

  4. 深入理解Java内存模型(四)——volatile

    2019独角兽企业重金招聘Python工程师标准>>> volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好 ...

  5. JAVA学习笔记--4.多线程编程 part1.背景知识和内存模型

    2019独角兽企业重金招聘Python工程师标准>>> 背景知识 CPU Cache 如上简易图所示,在当前主流的CPU中,每个CPU有多个核组成的.每个核拥有自己的寄存器,L1,L ...

  6. Java 内存模型 与 高效并发

    2019独角兽企业重金招聘Python工程师标准>>> 1. 并发解决了什么问题 多任务处理. 在处理器(CPU)运算速度 与 存储设备 (内存).通讯设备 的速度差距极大的前提下, ...

  7. python共享内存

    python共享内存 共享内存(Shared Memory)是最简单的进程间通信方式,它允许多个进程访问相同的内存,一个进程改变其中的数据后,其他的进程都可以看到数据的变化. 共享内存是进程间最快速的 ...

  8. python gc内存_禁用 Python GC,Instagram 性能提升10%

    通过关闭 Python 垃圾收集(GC)机制,该机制通过收集和释放未使用的数据来回收内存,Instagram 的运行效率提高了 10 %.是的,你没听错!通过禁用 GC,我们可以减少内存占用并提高 C ...

  9. JVM学习 - 体系结构 内存模型

    2019独角兽企业重金招聘Python工程师标准>>> 一:Java技术体系模块图 二:JVM内存区域模型 1.方法区 也称"永久代" ."非堆&quo ...

最新文章

  1. 谨慎的Waymo CEO:未来几十年,自动驾驶无法做到无处不在
  2. HTML做frame跳转设置响应头,X-Frame-Options header响应头如何配置
  3. UA MATH567 高维统计IV Lipschitz组合9 矩阵函数、半正定序与迹不等式
  4. 李永乐线性代数手写笔记-向量
  5. c++STL容器的Vector
  6. gj6 深入python的set和dict
  7. SQL语句中 as 的作用
  8. 为什么TypedReference在幕后
  9. Linux ubuntu 切换阿里更新源
  10. 干货,AES破解路程-生意参谋举例
  11. Unity3d 周分享(22期 2019.8.30 )
  12. PyTorch-1.10(十三)--torch.optim基本用法
  13. vscode code runner配置
  14. java计算机毕业设计课堂考勤系统MyBatis+系统+LW文档+源码+调试部署
  15. 高分毕业论文答辩自述稿(附注意事项及模板)
  16. 数学建模论文写作学习——数模论文概述
  17. oracle定时任务实例
  18. .Net4.0 任务(Task)
  19. Matrix矩阵的图像处理
  20. VTK重建CT图像,写入和读取STL格式文件

热门文章

  1. 雷电3和Type C区别
  2. pythonopencv算法_OpenCV3-Python基于Kalman和CAMShift算法应用
  3. The application could not be installed: INSTALL_FAILED_INSUFFICIENT_STORAGE
  4. 夏天这四件事会耗干你的阳气,尤其是第三件!
  5. cesium 圆形波纹
  6. 大容量nc文件解析_分布式文件系统浅谈
  7. python程序中结束while循环的两种方法是_Python中while循环
  8. js radio 获值
  9. tomcat 占用 dos
  10. css未生效,css文件引入后出现某些标签生效某些不生效