Python 学习笔记——9. 变量
变量作为Python中最基础的概念,但是我却很难捋清楚它的概念,因此放在了笔记的后面。
Python的变量使用非常灵活方便,但是这也导致了Python变量的底层原理不易理解,这一点与C/C++的变量完全不同。
在这篇笔记中,重点区分几个概念:
- 字面量:字面值是内置数据类型常量值的表示法
- 对象:对象具有类型,类型实例化得到对象
- 变量:可以指向一个任何类型对象的指针
关于字面量和字面量集的表示语法在前面已经介绍过,这里不再重复。
对象
在 Python 中,一切皆为对象,函数也是对象。
每一个对象都具有类型,对象是类型的实例,这些是面向对象的通用概念,在Python中也一样成立。
我们不需要像 C++一样手动管理内存中对象的创建与销毁,而是由 Python 虚拟机的负责(本质还是通过 C 的 malloc 之类的函数以及系统调用),内存回收机制 GC 负责管理和自动销毁没有被变量直接指向或间接使用的对象。GC的判定方法可以理解为对每一个对象都维持一个引用计数,如果计数为零则删除,和 C++的智能指针类似。事实上,我们无法在Python中显式地销毁内存中的任何对象。
基本数据类型可以分成两类:
- 不可变对象类型,主要包括:
- 整数类型 int
- 浮点数类型 float(虽然名为float,实质上就是双精度浮点数,相当于C语言的double)
- 字符串类型 str
- 元组 tuple
- 可变对象类型,通常包括:
- 列表 list
- 集合 set
- 字典 dict
这些基础的数据类型都有相应的字面量进行对应,可以通过字面量或字面量集直接创建相应的对象。可变与不可变类型的区别体现在修改对象时的行为,具体见下文。除了这些最基础的类型,还有可调用类型,自定义类型等等。
每个对象有独立的标识,关于标识有如下特点:
- 对象一旦被创建后,它的标识就不会改变
- 标识是一个整数,可以将其理解为该对象在内存中的地址(CPython就是这么实现的)
- 可以通过
id(x)获取变量x指向的对象的标识 - 如果变量
x和y分别指向两个对象,可以使用is运算符比较两个对象是否是同一个,两个同时存在的对象不可能具有相同的内存地址
1 | x is y |
- 两个生命周期不重合的对象可能具有相同的标识符,因为它们可以先后被分配在内存中的同一位置
每一个对象都有自己的类型,如果变量x指向一个对象,那么可以使用type(x)获取对象的类型信息,例如
1 | s = 'abc' |
Python的对象除了最简单的基本对象,还有很多对象是存在从属关系的,一个对象可能拥有其它对象的指针/引用,不妨称为复合对象,
复合对象和可变对象是很相似的概念,只是从不同角度进行的定义,例如一个列表[1,2,3],首先它是一个列表对象,它拥有三个指针,分别指向三个值为1,2,3的整数对象。
Python的类型也可以分为基本的类型和用户自定义的类型。在 Python3 中所有的类型都继承自 object 类型,因此都有一些公共的属性和方法。(但是在 Python2 中自定义类是否继承 object 有细微区别)
变量
在Python中,变量就是指向一个可以指向任何类型对象的指针,变量没有固定的类型,查看变量的类型就是它当前指向的对象的类型。
变量并不代表内存层面上的数据,Python 不支持常量(不可修改的变量)。
Python的变量在使用时非常自由,使用之前不需要进行声明或者定义,可以认为Python维护了一个记录所有变量的数据库(这里先忽略变量的作用域问题,最后再讨论变量作用域),其中每一个变量都正在指向一个对象,关于变量的基本使用有如下特点:
- 如果需要添加新的变量
x,首先需要直接将一个对象赋值给它,例如x=2,x就会被添加进入数据库中 - 直接将不在数据库中的名称作为变量使用,会报错未定义
1 | Traceback (most recent call last): |
- 可以使用
del语句删除变量,此时会将变量从数据库中移除,删除变量并不意味着它指向的对象的销毁,只是把指向这个对象的指针销毁了而已,对象的销毁判断和时机完全由GC控制
1 | del a |
- 删除的变量仍然可以再次添加到数据库中进行使用,只需要重新赋值即可
关于变量的赋值操作,有如下的便捷形式:
- 支持连续对多个变量同时赋值
1 | x = y = z = 1 |
- 支持多个变量分别赋值,实质是元组的解包:将右侧打包为元组
(1,2,3)分别赋值到左侧的对应变量
1 | x,y,z = 1,2,3 |
- 可以使用如下语句直接交换两个变量
1 | x,y = y,x |
Python并不支持不可变的变量(常量),因为Python的变量的实质只是指向对象的指针,通常约定全大写的变量为常量,例如
1 | PI=3.1415926 |
也可以采用某些特殊的处理来确保变量是不可变的,参考类的只读属性的实现。
Python只有变量没有常量,变量指向的对象类型则分为可变和不可变的。
底层原理浅析
变量赋值
考虑如下变量赋值语句的实质:
1 | x = 1 |
x=1会在内存空间创建了一个值为1的整数对象,然后将指向这个对象的指针交给 x,然后 x=2 则是创建了一个值为2的整数对象,并使x指向它。
实践中上述操作会被优化:Python在启动时就会在内存中直接创建所有的小整数对象,因为这些对象在Python语句执行中频繁使用。
并且由于整数类型是不可变的,因此Python会进一步使用优化策略:不再重复创建两个同样的整数对象,如果已经存在一个值为2整数对象,那么Python在执行z=1的时候就会让z直接指向它。
考虑如下变量赋值语句
1 | x = [1,2,3] |
首先创建一个值为[1,2,3]的列表对象,然后令x指向它。第二行的y=x不会涉及到对象的创建,只是让变量y和变量x指向同一个对象。
变量修改
对变量的修改有两种可能:
- 如果指向的对象类型是不可变的,修改变量这个指针自身,将变量指向新的对象,新对象的值根据原对象的值和修改语句确定
- 如果指向的对象是可变的,那么直接修改变量指向的对象的值
例一
1 | x = 100 |
由于整数类型是不可变的,因此首先x指向一个值为100的整数对象,修改会将x指向另一个值为200的整数对象,这一点通过不同的标识可以验证。
例二
1 | x={} |
由于字典类型是可变的,因此首先x指向一个值为空的字典对象,修改x指向的字典对象,向其中添加一个键值对,这个过程中x始终指向同一个对象,这一点通过相同的标识验证。
深复制与浅复制
考虑下面的语句
1 | x = [1,2,3] |
这里y=x只是让两个变量指向了同一个对象,并没有真正在内存中复制对象!我们关注如何在内存中复制对象。
值得注意的是,对于复合对象天然是存在两种复制操作的:
- 浅复制:只复制顶层对象,复制后两个顶层对象会指向同样的次级对象
- 深复制:复制顶层对象,然后递归地复制所有的子级对象,复制后两个顶层对象之间没有任何联系
Python提供了copy这个模块,可以分别实现任意对象的浅复制和深复制,下面给出两个例子进行分析
1 | import copy |
这里对x指向的列表对象[[1,2],3]执行一个浅复制,将y指向复制得到的新列表对象:
- 对
y指向的顶层对象的修改不会影响到x - 对
y指向的子级对象的修改会影响到x,因为x和y指向的两个列表对象共有子级对象
1 | import copy |
这里对x2指向的列表对象[[1,2],3]执行一个深复制,将y2指向复制得到的新列表对象,对y2指向的对象的任何层次的修改都不会影响到x2。
除了专门的copy模块,很多内置类型都提供了copy()方法,通常是浅复制,例如
1 | x = [[1,2],3] |
科学计算中常用的numpy数组也提供了copy()方法,**考虑到科学计算的实际需求,这里提供的是深复制,**例如
1 | import numpy as np |
注:
- 对于不可变对象,其实在内存中复制与否都是无所谓的:因为任何的修改尝试都会导致创建具有新的值的不可变对象,在这里提到的对于不可变对象的复制都会被Python解释器优化
- 对于非复合对象,相当于只有顶层对象,此时深复制和浅复制没有区别
变量作用域
在Python中所有的变量被分配到具体的不同作用域中,有如下特点:
- 默认有全局作用域和内置作用域,在具体的函数,类或者模块的内部会开辟对应的局部作用域,局部作用域是可以嵌套的,局部作用域与全局作用域也是嵌套关系
- 同一个作用域内的变量不会重名,不同作用域之间的变量可以重名
- 添加一个新变量时,新变量所属的作用域由当前位置决定
- 获取一个现有的变量时,会根据当前位置,按照优先级顺序从当前作用域依次向外层作用域查找,最后会去内置作用域查找
例一,不同的位置决定了不同的作用域
1 | var1 = 5 # var1 属于全局作用域 |
例二,在外部不能直接获取局部作用域中的变量
1 | def fun(): |
例三,可以在局部作用域中直接获取全局作用域的变量
1 | x = 10 # 创建全局作用域的变量x |
例四,在局部作用域中不能给全局作用域中的变量赋值或修改,而是会在局部作用域中创建一个新变量,并且此后,局部作用域中的变量会遮盖全局作用域的同名变量
1 | x = 0 # 创建全局作用域的变量x |
例五,可以使用global关键词来声明要使用和修改全局作用域中的变量,此后会抑制在当前局部作用域中创建同名变量,例如
1 | x = 0 # 创建全局作用域的变量x |
例六,下面的函数执行时会触发错误,a=a+1的语法解析有错误,不能对局部作用域的变量x在赋值之前获取
1 | x=1 # 创建全局作用域的变量x |
注:
- Python的作用域比C++少得多,后者在每一个
{}结构都会创建新的作用域,Python只会在函数,类和模块的内部创建局部作用域,在判断或循环结构中不会创建局部作用域 - 除了
global之外,还有作用相反的nolocal,它会使用上一层作用域中的变量,但是注意如果上层作用域中没有找到,会直接报错而不是继续向上查找。
