一、Python变量及赋值
变量是什么?变量是指在程序运行过程中,值会发生变化的量。变量就是存储在内存中的值,这就意味着在创建变量时会在内存中开辟一个空间。基于变量的数据类型,Python 解释器会分配指定内存,并决定什么数据可以被存储在内存中。因此,变量可以指定不同的数据类型,这些变量可以存储整数,小数或字符。
除了经常听到变量外,可能还会经常听到常量这个词,常量是指在程序运行过程中,值不会发生变化的量。无论是变量还是常量,在创建时都会在内存中开辟一块空间,用于保存它的值。
- 变量定义
Python 中的变量不需要声明,变量的赋值操作既是变量声明和定义的过程,当我们定义一个变量时,Python 解释器会根据语法和操作数决定对象的类型。如果了解 C 语言的同学应该知道,C 语言中变量的定义需要提前此变量的类型后才可被赋值。每个变量在内存中创建,都包括变量的标识,名称和数据这些信息。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建,且才可以被使用。
变量在程序中就是用一个变量名表示了,变量名必须是大小写英文、数字和_的组合,且不能用数字开头。另外不能使用Python关键字作为变量名。
普通变量赋值
1 2 3 |
var = 10 type(var) <type 'int'> |
定义一个变量,变量名为 “var”,赋值了一个整数,我们使用 type 方法查看此变量的类型,可以看出是一个整数类型。
1 2 3 |
var = 10.1 type(var) <type 'float'> |
此时变量 var 是一个浮点型。
1 2 3 |
var = "school" type(var) <type 'str'> |
此时变量 var 是一个字符串。
1 2 3 |
var = True type(var) <type 'bool'> |
此时变量 var 是一个布尔值。
在 Python 中,等号 “=” 是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量,如上操作就是把一个变量进行了多次赋值,以最后一次赋值为最终结果。
这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配就会报错;比如 C 语言和 Go 都是静态语言,赋值语句如下:
1 2 |
var a int = 10; // 声明变量 a 是一个整数并赋值 var a string = "10"; // 声明变量 a 是一个字符串并赋值 |
和静态语言相比,动态语言更灵活,就是这个原因。另外请不要把赋值语句的等号等同于数学的等号。比如下面的代码:
1 2 3 4 |
x = 10 x = x + 2 print(x) 12 |
如果从数学上理解 x = x + 2 那无论如何是不成立的,在程序中,赋值语句先计算右侧的表达式 x + 2,得到结果 12,再赋给变量 x。由于 x 之前的值是 10,重新赋值后,x 的值变成 12。
- 变量赋值过程
Python 中的变量赋值,不是真正意义上的修改内存的值,而是将变量名和在内存中的值做了一个绑定,称之为“引用”。
当我们创建一个变量时:
1 |
a = 1 |
这时,Python 解释器干了两件事情:
- 在内存中创建了一个对象 1
- 在内存中创建了一个名为 a 的变量并指向对象 1
也可以把一个变量 a 赋值给另一个变量 b,这个操作实际上是把变量 b 指向变量 a 所指向的数据,例如下面的代码:
1 2 |
a = 1 b = a |
这里要注意,Python 里的对象可以被多个变量所指向或引用。
再看下面这个示例:
1 2 3 |
a = 1 b = a a = a + 1 |
这里首先将 1 赋值于 a,即 a 指向了 1 这个对象。接着 b = a 则表示,让变量 b 也同时指向 1 这个对象。
最后执行 a = a + 1。需要注意的是,Python 的数据类型,例如整型(int)、字符串(string)等等,是不可变的。所以,a = a + 1,并不是让 a 的值增加 1,而是表示重新创建了一个新的值为 2 的对象,并让 a 指向它。但是 b 仍然不变,仍然指向 1 这个对象。
因此,最后的结果是,a 的值变成了 2,而 b 的值不变仍然是 1。
通过这个例子你可以看到,这里的 a 和 b,开始只是两个指向同一个对象的变量而已,或者你也可以把它们想象成同一个对象的两个名字。简单的赋值 b = a,并不表示重新创建了新对象,只是让同一个对象被多个变量指向或引用。
同时,指向同一个对象,也并不意味着两个变量就被绑定到了一起。如果你给其中一个变量重新赋值,并不会影响其他变量的值。
明白了这个基本的变量赋值例子,我们再来看一个列表的例子:
1 2 3 4 5 6 7 |
l1 = [1, 2, 3] l2 = l1 l1.append(4) l1 [1, 2, 3, 4] l2 [1, 2, 3, 4] |
同样的,我们首先让列表 l1 和 l2 同时指向了 [1, 2, 3] 这个对象。
由于列表是可变的,所以 l1.append(4) 不会创建新的列表,只是在原列表的末尾插入了元素 4,变成 [1, 2, 3, 4]。由于 l1 和 l2 同时指向这个列表,所以列表的变化会同时反映在 l1 和 l2 这两个变量上,那么,l1 和 l2 的值就同时变为了 [1, 2, 3, 4]。
另外,需要注意的是,Python 里的变量可以被删除,但是对象无法被删除。比如下面的代码:
1 2 |
l = [1, 2, 3] del l |
del l 删除了 l 这个变量,从此以后你无法访问 l,但是对象 [1, 2, 3] 仍然存在。Python 程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果 [1, 2, 3] 除了 l 外,还在其他地方被引用,那就不会被回收,反之则会被回收。
由此可见,在 Python 中:
- 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向。
- 可变对象(列表,字典,集合等等)的改变,会影响所有指向该对象的变量。
- 对于不可变对象(字符串,整型,元祖等等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作(+= 等等)更新不可变对象的值时,会返回一个新的对象。
- 变量可以被删除,但是对象无法被删除。
对象引用的优缺点:
- 优点,节省内存空间,多个变量可以引用同一个对象。
- 缺点,如果修改变量的变量值在内存中不存在,需要重新申请内存,绑定变量名和地址,降低了执行效率。
我们看一下下面代码的赋值过程:
1 2 3 4 5 6 7 8 |
x = 2 y = 3 id(x) 32771152 id(y) 32771128 |
当你创建一个变量 x,值等于 2 时;Python 解释器会先找内存中有没有2的值,如果有直接做绑定,然后2这个值得引用计数加1即可。如果没有那么 Python 解释器就需要开辟一段内存空间,创建一个2的值,我们使用 id(x) 可以看出变量 x 当前引用的内存地址。同样变量 y 也是这样的操作。
此时,我们如果修改变量 x = 3,那么 Python 解释器就会把变量 x 的与内存地址 “32771128” 做绑定,然后把对象 3 的引用加 1,如下操作:
1 2 3 4 |
x = 3 id(x) 32771128 |
那么此时,内存中的整数值 “2” 就没有被引用了,当 Python 解释器检查到 “2” 的引用为 0 时就会启动内存回收机制把此内存空间给释放掉了。
不过,需要注意,对于整型数字来说,以上结论只适用于 -5 到 256 范围内的数字。比如下面这个例子:
1 2 3 4 5 6 7 8 |
a = 257 b = 257 id(a) 4473417552 id(b) 4473417584 |
这里我们把 257 同时赋值给了 a 和 b,可以看到 a == b 仍然返回 True,因为 a 和 b 指向的值相等。但奇怪的是,a is b 返回了 false,并且我们发现,a 和 b 的 ID 不一样了,这是为什么呢?
事实上,出于对性能优化的考虑,Python 内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。这样,每次你试图创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的引用,而不是重新开辟一块新的内存空间。
但是,如果整型数字超过了这个范围,比如上述例子中的 257,Python 则会为两个 257 开辟两块内存区域,因此 a 和 b 的 ID 不一样。
二、Python解释器内部机制
- Python引用计数机制
要保持追踪内存中的对象,Python 使用了引用计数这一简单计数。也就是说 Python 内部记录着所有使用中的对象各有多少引用。一个内部跟踪变量也称为引用计数器。每个对象各有多少个引用简称引用计数;当对象被创建时,就创建了一个引用计数,当这个对象不再需要时,也就是说这个对象的引用计数变为 0 时它会被当垃圾回收。
- Python垃圾回收机制
在 Python 中不再使用的内存会被一种称为垃圾收集的机制释放。像上面说的,虽然解释器跟踪对象的引用计数,但垃圾收集器负责释放内存。垃圾收集器是一块独立代码,它用来寻找引用计数为 0 的对象。它也负责检查哪些虽然应用计数大于 0 但也应该被销毁的对象。特定情形会导致循环引用。
三、变量赋值的方法
1. 普通变量赋值
1 2 |
x = 1 y = 'school' |
2. 增量赋值
1 2 3 4 5 |
x = 2 x += 2 x 4 |
3. 多重赋值
1 2 3 4 5 6 7 8 9 10 |
y = x = z = 1 y 1 x 1 z 1 |
4. 多元赋值
1 2 3 4 5 6 7 |
x, y = 1, 'str' x 1 y 'str' |
1 2 3 4 5 6 7 8 9 |
x = 10 y = 20 x, y = y, x x 20 y 10 |
5. 分解赋值
元祖和列表分解赋值时当赋值符号(*)的左侧为元祖或列表时、Python 会按照位置把右边的对象和左边的目标自左而右逐一进行配对;个数不同时会触发异常,此时可以切片的方式进行.
1 2 3 4 5 |
one = (1,2,3) x, y, z = one x 1 |
四、变量的作用域
作用域简单说就是一个变量的命名空间。代码中变量被赋值的位置,就决定了哪些范围的对象可以访问这个变量,这个范围就是命名空间。Python 赋值时生成了变量名,当然作用域也包括在内。
Python 变量作用域遵循 LEGB 原则:
- L (Local) 局部作用域
- E (Enclosing) 闭包函数外的函数中
- G (Global) 全局作用域
- B (Built-in) 内建作用域
以 L –> E –> G –>B 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。
在def/class/lambda
内定义的变量名,只能被函数内部引用,不能在函数外引用这个变量名,这个变量的作用域就是局部的,也叫它为局部变量。在def/class/lambda
外,一段代码最始开所赋值的变量,它可以被多个函数引用,这就是全局变量。局部作用域会覆盖全局作用域,但不会影响全局作用域。
如果函数内的变量名与函数外的变量名相同,也不会发生冲突。好比下面这种情况:
1 2 3 4 |
x = 100 def func(): x = 55 |
x = 100 这个赋值语句所创建的变量 X,作用域为全局变量。
x = 55 这个赋值语句所创建的变量 X,它的作用域则为局部变量,只能在函数 func() 内使用。
尽管这两个变量名是相同的,但它的作用域为它们做了区分。作用域在某种程度上也可以起到防止程序中变量名冲突的作用。
每次对函数的调用都会创建一个新的本地作用域,赋值的变量除非使用global申明为全局变量否则均为本地变量。
内置的由__builtin__
模块提供。
<参考>