2. 变量引用
先提出一个爆论
在 Python 中, 是不存在赋值操作的.
那么问题来了, 当执行 a = 1
时, 这难道不是赋值操作吗?
确实不是赋值, 事实上 Python 在执行 a = 1
时做了两个操作:
创建一个整数
1
对象,给这个对象起一个名字
a
.
而并不是:
创建一个整数变量
a
,然后将
a
赋值成1
.
请读者细细体会这两种描述的区别, 这也是 Python 有别于其他语言的一个很重要的特征. 请看以下代码, 并判断最终的输出是什么.
1 2 3 4 | a = 1 b = 1 print(a is b) |
按照我们的之前的解释, 在Listing 2.1 中:
创建了一个
int
变量1
,给这个变量添加一个名字
a
,创建了一个
int
变量1
,给这个变量添加一个名字
b
.
由于 a
和 b
是两个不同的对象, 因此 a
和 b
的地址是不同的. 所以最终的输出应该是 a is not b
. 我们执行一下Listing 2.1, 其输出如 Bash 2.1 示.
$ python3 examples/object/reference/assignment_of_int_1.py
True
好像哪里不对劲, 跟我们之前分析的有一些出入. 这是由于 Python 会将一些常用的整数缓存起来, 不会每次都重新构造一个新的对象. 笔者使用的 Python 版本为 3.8.6, 操作系统为 Linux, 在该版本中, 范围在 \([-5, 256]\) 之间的整数都会被缓存到内存中, 为了验证我们的结论, 我们将Listing 2.1 稍加改动, 执行结果就完全不一样了.
a = 257
b = 257
print(a is b)
我们执行一下Listing 2.2.
$ python3 examples/object/reference/assignment_of_int_257.py
True
额, 结果非常出乎我们的意料. 读者们, 请听我解释, 是酱紫的. Python 在运行脚本的时候, 已经拿到了所有的信息, 会做一些性能上的优化工作. 于是两个 257
就被优化成了一个对象. 如果用 Python 的交互模式来执行这段代码, 结果就不一样了.
$ python
Python 3.8.6 (default, Oct 19 2020, 15:10:29)
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 1
>>> b = 1
>>> print(a is b)
True
>>>
>>> a = 257
>>> b = 257
>>> print(a is b)
False
>>>
>>> exit()
如果 a
的地址和 b
的地址是一样的, 我们修改 a
的值, 那么 b
会随着变化吗? 读者可以思考一下Listing 2.3 运行的结果是什么.
a = 1
b = a
a = 2
print('a =', a)
print('b =', b)
由于 a
和 b
指向同一个对象的地址, 修改 a
的值, 那么 b
的值也一定会发生更改, 因此, 此时输出 b
的值应该是 2
.
$ python3 examples/object/reference/change_reference_value.py
a = 2
b = 1
然而事实上, 执行结果显示 b
的值并没有发生变化. 这好像跟之前说的不太一样? 这个结果跟之前表述的观点并不矛盾, 原因在于, 当执行 a = 2
时, 不是将 a
所指的对象的值改为 2
, 而创建了一个对象 2
, 然后给这个对象起名为 a
, 此时 a
不再是对象 1
的名字, 但是 b
仍然是对象 1
的名字, 当我们访问 b
的值, 当然还是 1
.
知道这个有什么用吗? 我套用 C, C++ 等语言的赋值语句来理解 Python 的赋值语句不可以吗? 答案是: 可以, 但不完全可以. 读者可以思考一下Listing 2.4 的输出结果是什么.
a = [1, 2, 3]
b = a
a[0] = 'he'
print('a =', a)
print('b =', b)
输出结果如下所示, 有没有跟你想的不一样呢?
$ python3 examples/object/reference/change_list.py
a = ['he', 2, 3]
b = ['he', 2, 3]
你会有这种疑问吗?
a[0]
的值指向了一个新的字符串, 为什么 b[0]
的值也跟着变化了?
Hint
当执行 a = [1, 2, 3]
时:
创建了一个列表
[1, 2, 3]
,给这个列表起一个名字
a
.
当执行 b = a
时:
等号的右边是一个已存在的对象, 直接加载这个对象, 而不是重新构造,
给这个对象起了另一个名字
b
, 此时b
和a
的地址是相同的.
当执行 a[0] = 'he'
时:
修改了
a
所指对象中元素的值,由于
b
也指的同一个对象, 因此b[0]
的值也发生了变化.
至此, 我们回头看Section 1 中最后的疑问, 是不是对如下代码有了更深的理解.
a = '1'
a = 1 + 3
在上述代码中, 并不是变量 a
的类型发生了变化, 而是:
创建字符串对象
'1'
,将其起名为
a
,创建整数对象
4
,将其起名为
a
,
整个过程中, 对象的类型没有任何隐式或者显式的转换. 因此, 再次重申: Python 是一门强类型语言.
有人要问, 你讲了这么多, 有什么证据吗? 有的, 我们通过分析 Python 字节码的反汇编就可以看出 Python 底层的运行逻辑. 以下两段代码分别是 Python 源代码以及对应的反汇编.
1 2 3 4 | a = 1 b = 1 c = a pass |
1 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (a)
2 4 LOAD_CONST 0 (1)
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 0 (a)
10 STORE_NAME 2 (c)
4 12 LOAD_CONST 1 (None)
14 RETURN_VALUE
其中:
LOAD_CONST
用于构造一个整数,
思考题
在 Python 中, 可以定义一个常量吗? 即只可以被定义, 不可以被修改的变量.