欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > 【Python学习手册(第四版)】学习笔记06-Python动态类型-赋值模型详解

【Python学习手册(第四版)】学习笔记06-Python动态类型-赋值模型详解

2024/11/30 10:30:31 来源:https://blog.csdn.net/qq_39803142/article/details/140661191  浏览:    关键词:【Python学习手册(第四版)】学习笔记06-Python动态类型-赋值模型详解

个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。


主要介绍Python的动态类型(也就是Python自动为跟踪对象的类型,不需要在脚本中编写声明语句),Python中变量和对象是如何通过引用关联,垃圾收集的概念,对象共享引用是如何影响多个变量的,Python中引用是如何影响相等的概念。

目录

缺少类型声明

变量、对象和引用

变量创建

变量类型

变量使用

总结

举例

类型属于对象,而不是变量

对象的垃圾收集

共享引用

共享引用和在原处修改

共享引用和相等

动态类型随处可见

习题


动态类型以及由它提供的多态性,这些概念是Python语言简洁性和灵活性的基础。

在Python中,我们并不会声明脚本中使用的对象的确切类型。事实上,程序甚至可以不在意特定的类型;相反地,它们能够自然地适用于更广泛的场景下。因为动态类型是Python语言灵活性的根源。

缺少类型声明

如果你有学习静态编译类型语言C、C++或Java的背景,学到这里,你也许会有些困惑。到现在为止,我们使用变量时,都没有声明变量的存在和类型,但变量还可以工作。

例如,在交互会话模式或是程序文件中,当输入a=3时,Python怎么知道那代表了一个整数呢?在这种情况下,Python怎么知道a是什么?

在Python中,类型是在运行过程中自动决定的,而不是通过代码声明。没有必要事先声明变量,对变量、对象和它们之间的关系都适用。

变量、对象和引用

在Python中运行赋值语句a=3时,即使没有告诉Python将a作为一个变量来使用,或者没有告诉它a应该作为一个整数类型对象,但一样也能工作。

这是一种非常自然的方式:

变量创建

一个变量(也就是变量名),就像a,当代码第一次给它赋值时就创建了它。之后的赋值将会改变已创建的变量名的值。从技术上来讲,Python在代码运行之前先检测变量名,可以当成是最初的赋值创建变量。

变量类型

变量永远不会有任何的和它关联的类型信息或约束。类型的概念是存在于对象中而不是变量名中。变量原本是通用的,它只是在一个特定的时间点,简单地引用了一个特定的对象而已。

变量使用

当变量出现在表达式中时,它会马上被当前引用的对象所代替,无论这个对象是什么类型。

此外,所有的变量必须在其使用前明确地赋值,使用未赋值的变量会产生错误。

总结

变量在赋值的时候才创建,它可以引用任何类型的对象,并且必须在引用之前赋值。这意味着,不需要通过脚本声明所要使用的名字,但是,必须初始化名字然后才能更新它们;例如,必须把计数器初始化为0,然后才能增加它。

举例

刚入门时,如果清楚地将变量名和对象划分开来,动态类型是很容易理解的。比如:

>>> a = 3

从概念上来说,Python将会执行三个不同的步骤去完成这个请求。这些步骤反映了Python语言中所有赋值的操作:

1.创建一个对象来代表值3。

2.创建一个变量a,如果它还没有创建的话。

3.将变量与新的对象3相连接。

实际的效果是如图6-1所示的一个在Python中的内部结构。

如图6-1所示,变量和对象保存在内存中的不同部分,并通过连接相关联(这个连接在图6-1中显示为一个箭头)。变量总是连接到对象,并且绝不会连接到其他变量上,但是更大的对象可能连接到其他的对象(例如,一个列表对象能够连接到它所包含的对象)。

这里的变量名和对象,在运行a=3后。变量a变成对象3的一个引用。在内部,变量事实上是到对象内存空间(通过运行常量表达式3而创建)的一个指针。

在Python中从变量到对象的连接称作引用。也就是说,引用是一种关系,以内存中的指针的形式实现。

Python的引用类似于C的指针(内存地址)。事实上,引用是以指针的形式实现的,通常也扮演着相同的角色,尤其是那些在原处修改的对象。然而由于在使用引用时会自动解除引用,没有办法拿引用来做些什么:这种功能避免了许多C可能出现的Bug。

一旦变量被使用(也就是说被引用),Python自动跟随这个变量到对象的连接。

具体的术语来讲:

·变量是一个系统表的元素,拥有指向对象的连接的空间。

·对象是分配的一块内存,有足够的空间去表示它们所代表的值。

·引用是自动形成的从变量到对象的指针。

从概念上讲,在脚本中,每一次通过运行一个表达式生成一个新的值,Python都创建了一个新的对象(换言之,一块内存)去表示这个值。

从内部来看,作为一种优化,Python缓存了不变的对象并对其进行复用,例如,小的整数和字符串(每一个0都不是一块真正的、新的内存块,稍后会介绍这种缓存行为)。

但是,从逻辑的角度看,这工作起来就像每一个表达式结果的值都是一个不同的对象,而每一个对象都是不同的内存。

从技术上来讲,对象有更复杂的结构而不仅仅是有足够的空间表示它的值那么简单。每一个对象都有两个标准的头部信息:一个类型标志符去标识这个对象的类型,以及一个引用的计数器,用来决定是不是可以回收这个对象。


类型属于对象,而不是变量

为了理解对象类型是如何使用的,看对一个变量进行多次赋值后的结果:

>>> a = 3    #整数
>>> a = 'life'    #字符串
>>> a
'life'
>>> a = 1.23    #浮点
>>> a
1.23

a刚开始是一个整数,然后变成一个字符串,最后变成一个浮点数。

在Python中,情况很简单:变量名没有类型。就像前面说的,类型属于对象,而不是变量名。

就例子而言,只是把a修改为对不同的对象的引用。因为变量没有类型,我们实际上并没有改变变量a的类型,只是让变量引用了不同类型的对象而已。实际上,Python的变量就是在特定的时间引用了一个特定的对象。

从另一方面讲,对象知道自己的类型。每个对象都包含了一个头部信息,其中标记了这个对象的类型。

例如,整数对象3,包含了值3以及一个头部信息,告诉Python,这是一个整数对象[从严格意义上讲,一个指向int(整数类型的名称)的对象的指针)]。'life'字符串的对象的标志符指向了一个字符串类型(叫做str)。因为对象记录了它们的类型,变量就没有必要了

注意Python中的类型是与对象相关联的,而不是和变量关联。在典型的代码中,一个给定的变量往往只会引用一种类型的对象。但是这并不是必须的,你会发现Python代码比你通常惯用的代码更加灵活:如果正确的使用Python,代码能够自动以多种类型进行工作。

这里提到的这个代码有两个头部信息,一个是类型标志符,另一个是引用计数器


对象的垃圾收集

在上一节的例子中,我们把变量a赋值给了不同类型的对象。但是当重新给变量a赋值时,它前一个引用值发生了什么变化?

>>> a = 3    #整数
>>> a = 'life'    #字符串

答案是,在Python中,每当一个变量名被赋予了一个新的对象,之前的那个对象占用的空间就会被回收(如果它没有被其他的变量名或对象所引用的话)。这种自动回收对象空间的技术叫做垃圾收集

下面的例子,其中每个语句,把变量名x赋值给了不同的对象:

>>> x = 452
>>> id(x)
2585863375408
>>> x = 'beautiful'
>>> id(x)
2585863606128
>>> x = 3.1415
>>> id(x)
2585863375728
>>> x = [1,2,3,4]
>>> id(x)
2585863298944

这里用到了id函数, id() 函数返回对象的内存地址。

首先注意x每次被设置为不同类型的对象。再者,尽管这并不是真正的情况,效果却是x的类型每次都在改变。在Python中,类型属于对象,而不是变量名。由于变量名只是引用对象而已,这种代码自然行得通。

第二,注意对象的引用值在此过程中逐个丢弃。每一次x被赋值给一个新的对象,Python都回收了对象的空间。例如,当它赋值为字符串'beautiful'时,对象452马上被回收(假设它没有被其他对象引用):对象的空间自动放入自由内存空间池,等待后来的对象使用。

在内部,Python是这样来实现这一功能的:它在每个对象中保持了一个计数器,计数器记录了当前指向该对象的引用的数目。一旦(并精确在同一时间)这个计数器被设置为零,这个对象的内存空间就会自动回收。在前面的介绍中,假设每次x都被赋值给一个新的对象,而前一个对象的引用计数器变为零,就会导致它的空间被回收。

垃圾收集最直接的、可感受到的好处就是,这意味着可以在脚本中任意使用对象而不需要考虑释放内存空间。在程序运行时,Python将会清理那些不再使用的空间。实际上,与C和C++这样的底层语言相比,省去了大量的基础代码。

注意:从技术上讲,Python的垃圾收集主要基于引用计数器。但是,它也有一部分功能可以及时地检测并回收带有循环引用的对象。功能默认可用

由于引用实现为指针,一个对象有可能会引用自身,或者引用另一个引用了自身的对象。

比如把一个列表的引用嵌入其自身中,从而创建一个循环。对来自用户定义的类的对象的属性赋值的时候,会产生同样的现象。尽管相对很少,由于这样的对象的引用计数器不会清除为0,必须特别对待它们。

要了解Python的循环检测器的更多细节,参见Python库手册中gc模块的文档。还要注意,Python的垃圾收集器的介绍只适用于标准的Cpython、Jython和IronPython且可能使用不同的方案,直接效果都是类似的,都是未使用的空间又自动重新申请。


共享引用

我们已经看到了单个变量被赋值引用了多个对象的情况。现在,在交互模式下,引入另一个变量,并看一下变量名和对象的变化:

>>> b = a

输入这两行语句后,生成如图6-2所示的结果。就像往常一样,第二行会使Python创建变量b。变量a正在使用,并且它在这里没有被赋值,所以它被替换成其引用的对象3,从而b也成为这个对象的一个引用。

实际的效果就是变量a和b都引用了相同的对象(也就是说,指向了相同的内存空间)。这在Python中叫做共享引用——多个变量名引用了同一个对象

运行赋值语句b=a之后的变量名和对象。变量b成为对象3的一个引用。在内部,变量实际上是一个指针指向了对象的内存空间,该内存空间是通过运行常量表达式3创建的。

下一步,假设运行另一个语句扩展了这样的情况:

>>> a = 3
>>> b = a
>>> a = 'spam'

这条语句简单地创建了一个新的对象(代表字符串值'spam'),并设置a对这个新的对象进行引用。尽管这样,这并不会改变b的值,b仍然引用原始的对象——整数3。

>>> a
'spam'
>>> b
3

最终的引用结构如图所示

最终运行完赋值语句a='spam'后的变量名和对象。变量a引用了由常量表达式'spam'所创建的新对象(例如,内存空间),但是变量b仍然引用原始的对象3。因为这个赋值运算改变的不是对象3,仅仅改变了变量a,变量b并没有发生改变。

如果我们把变量b改成'spam'的话,也会发生同样的事情:赋值只会改变b,不会对a有影响。发生这种现象,跟没有类型差异一样。

再比如下面的3条语句:

>>> a = 3
>>> b = a
>>> a = a + 2

在这里,产生了同样的结果:Python让变量a引用对象3,让b引用与a相同的对象。之前,最后的那个赋值将a设置为一个完全不同的对象(在这种情况下,整数5是表达式“+”的计算结果)。

>>> a
5
>>> b
3

这并不会产生改变了b的引用。事实上,是没有办法改变对象3的值的:就像之前介绍过的,整数是不可变的,因此没有方法在原处修改它。

认识这种现象的一种方法就是,不像其他的一些语言,在Python中,变量总是一个指向对象的指针,而不是可改变的内存区域的标签给一个变量赋一个新的值,并不是替换了原始的对象,而是让这个变量去引用完全不同的一个对象。

实际的效果就是对一个变量赋值,仅仅会影响那个被赋值的变量。

当可变的对象以及原处的改变进入这个场景,那么这个情形会有某种改变。下面会讲到

共享引用和在原处修改

下面你会看到,有一些对象和操作确实会在原处改变对象。

例如,在一个列表中对一个偏移进行赋值确实会改变这个列表对象,而不是生成一个新的列表对象。对于支持这种在原处修改的对象,共享引用时的确需要加倍的小心,因为对一个变量名的修改会影响其他的变量。

>>> l1 = [2,3,4]
>>> l2 = l1

L1是一个包含了对象2、3和4的列表。在列表中的元素是通过它们的位置进行读取的,所以L1[0]引用对象2,它是列表L1中的第一个元素。当然,列表自身也是对象,就像整数和字符串一样。在运行之前的两个赋值后,L1和L2引用了相同的对象,就像我们之前例子中的a和b一样。

如果我们现在像下面这样去扩展这个交互:

>>> l1 = 24

L1直接设置为一个不同的对象,L2仍是引用最初的列表。

>>> l1
24
>>> l2
[2, 3, 4]

如果我们稍稍改变一下这个语句的内容,就会有明显不同的效果。

>>> l1 = [2,3,4]
>>> l2 = l1
>>> l1[0] = 24
>>> l1
[24, 3, 4]
>>> l2
[24, 3, 4]

在这里,没有改变L1,改变了L1所引用的对象的一个元素。这类修改会覆盖列表对象中的某部分。

因为这个列表对象是与其他对象共享的(被其他对象引用),那么一个像这样在原处的改变不仅仅会对L1有影响。也就是说,必须意识到当做了这样的修改,它会影响程序的其他部分。

在这个例子中,也会对L2产生影响,因为它与L1都引用了相同的对象。另外,我们实际上并没有改变L2,但是它的值将发生变化,因为它已经被修改了。

这种行为通常来说就是你所想要的,应该了解它是如何运作的,让它按照预期去工作。这也是默认的。

如果你不想要这样的现象发生,需要Python拷贝对象,而不是创建引用。有很多种拷贝一个列表的办法,包括内置列表函数以及标准库的copy模块。也许最常用的办法就是从头到尾的分片。后面会讲到。

>>> l1 = [2,3,4]
>>> l2 = l1[:]
>>> l1[0] = 24
>>> l1
[24, 3, 4]
>>> l2
[2, 3, 4

这里,对L1的修改不会影响L2,因为L2引用的是L1所引用对象的一个拷贝也就是说,两个变量指向了不同的内存区域。

注意这种分片技术不会应用在其他的可变的核心类型(字典和集合,因为它们不是序列)上,复制一个字典或集合应该使用X.copy()方法调用

注意标准库中的copy模块有一个通用的复制任意对象类型的调用,也有一个拷贝嵌套对象结构(例如,嵌套了列表的一个字典)的调用

这里在其他文章会讲到。

记住有些对象是可以在原处改变的(即可变的对象),这种对象往往对这些现象总是很开放。在Python中,这种对象包括了列表、字典以及一些通过class语句定义的对象。

如果不是你期望的现象,可以根据需要直接拷贝对象。

共享引用和相等

前面介绍的垃圾收集的行为与常量相比,某些类型需要更多地思考。比如下面的:

>>> x = 42
>>> x = 'life'    #现在会回收42吗?

Python缓存并复用了小的整数和小的字符串,就像前文提到的那样,这里的对象42也许并不像我们所说的被回收;相反地,它将可能仍被保存在一个系统表中,等待下一次你的代码生成另一个42来重复利用。

尽管这样,大多数种类的对象都会在不再引用时马上回收;对于那些不会被回收的,缓存机制与代码并没有什么关系。

例如,由于Python的引用模型,在Python程序中有两种不同的方法去检查是否相等。创建一个共享引用来说明:

>>> l = [1,2,3]
>>> m = l
>>> l == m
True
>>> l is m
True

第一种技术“==操作符”,测试两个被引用的对象是否有相同的值。这种方法往往在Python中用作相等的检查。

第二种方法“is操作符”,是在检查对象的同一性。如果两个变量名精确地指向同一个对象,它会返回True,所以这是一种更严格形式的相等测试。

实际上,is只是比较实现引用的指针,所以如果必要的话是代码中检测共享引用的一种办法。

如果变量名引用值相等,但是是不同的对象,它的返回值将是False,正如当我们运行两个不同的常量表达式时:

>>> l = [1,2,3]
>>> m = [1,2,3]
>>> l == m
True
>>> l is m
False
>>> id(l)
2035276862528
>>> id(m)
2035276864832

可以看到引用的对象不是同一个。

当我们对小的数字采用同样的操作时的结果:

>>> x = 42
>>> y = 42
>>> x == y
True
>>> x is y
True

在这次交互中,X和Y应该是==的(具有相同的值),但不是is的(同一个对象),因为我们运行了两个不同的常量表达式。但是因为小的整数和字符串被缓存并复用了,所以is告诉我们X和Y是引用了一个相同的对象。

如果你想刨根问底的话,你能够向Python查询对一个对象引用的次数:在sys模块中的getrefcount函数会返回对象的引用次数。

例如,在IDLE GUI中查询整数对象42时,它会报告这个对象有8次重复引用(绝大多数都是IDLE系统代码所使用的):

>>> import sts
>>> sys.getrefcount(42)
8

这种对象缓存和复用的机制与代码是没有关系的(除非你运行这个检查)。因为不能改变数字和字符串,所以无论对同一个对象有多少个引用都没有关系。然而,这种现象也反映了Python为了执行速度而采用的优化其模块的众多方法中的一种。


动态类型随处可见

在使用Python的过程中,真的没有必要去用圆圈和箭头画变量名/对象的框图

尽管在刚入门的时候,它会帮助你跟踪它们的引用结构,理解不常见的情况。例如,如果在程序中,当传递过程中一个可变的对象发生了改变时。

尽管目前来说动态类型看起来有些抽象,你最终还是需要关注它的。因为在Python中,任何东西看起来都是通过赋值和引用工作的,对这个模型的基本了解在不同的场合都是很有帮助的。

就像将会看到的那样,它也会在赋值语句,变量参数,for循环变量,模块导入等,类属性等很多场合发挥作用。值得高兴的是这是Python中唯一的赋值模型。一旦你对动态类型上手了,将会发现它在这门语言中任何地方都有效。

从最实际的角度来说,动态类型意味着你将写更少的代码。

同等重要的是,动态类型也是Python中多态的根本。

在Python代码中没有对类型进行约束,它具备了高度的灵活性。如果使用正确的话,动态类型和多态产生的代码,可以自动地适应系统的新需求。


练习

1、它们会改变A打印出的值吗?

>>> a = 'life'
>>> b = a
>>> b = 'he'

 答:不会。

>>> a
'life'
>>> b
'he'

当B赋值为字符串时,发生的只是变量B被重新设置为指向了新的字符串对象。A和B最初共享(即引用或指向)了同一个字符串对象"life",但是在Python中这两个变量名从未连接在一起。设置B为另一个不同的对象对A没有影响。

永远都不会在原处覆盖一个字符串(数字或元组),因为字符串是不可变的。


2、它们会改变A打印出的值吗?

>>> a = ['life']
>>> b = a
>>> b[0] = 'he'

 答:会。

>>> a
['he']
>>> b
['he']

我们既没有改变A也没有改变B,但是改变的是这两个变量共同引用(指向)的对象的一部分,通过变量B在原处覆盖了这个对象的一部分内容。

A像B一样引用了同一个对象,这个改变也会对A产生影响。


3、它们会改变A打印出的值吗?

>>> a = ['life']
>>> b = a[:]
>>> b[0] = 'he'

 答:不会。

>>> a
['life']
>>> b
['he']

由于分片表达式语句会在被赋值给B前创建一个拷贝,这次对B在原处赋值就不会有影响了。在第二个赋值语句后,就有了两个拥有相同值的不同列表对象了(它们是==的,却不是is的)。

第三条赋值语句会改变指向B的列表对象,而不会改变指向A的列表对象。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com