2. 变量引用

先提出一个爆论

在 Python 中, 是不存在赋值操作的.

那么问题来了, 当执行 a = 1 时, 这难道不是赋值操作吗?

确实不是赋值, 事实上 Python 在执行 a = 1 时做了两个操作:

  • 创建一个整数 1 对象,

  • 给这个对象起一个名字 a.

而并不是:

  • 创建一个整数变量 a,

  • 然后将 a 赋值成 1.

请读者细细体会这两种描述的区别, 这也是 Python 有别于其他语言的一个很重要的特征. 请看以下代码, 并判断最终的输出是什么.

Listing 2.1 assignment_of_int_1.py
1
2
3
4
a = 1
b = 1

print(a is b)

按照我们的之前的解释, 在Listing 2.1 中:

  • 创建了一个 int 变量 1,

  • 给这个变量添加一个名字 a,

  • 创建了一个 int 变量 1,

  • 给这个变量添加一个名字 b.

由于 ab 是两个不同的对象, 因此 ab 的地址是不同的. 所以最终的输出应该是 a is not b. 我们执行一下Listing 2.1, 其输出如 Bash 2.1 示.

Bash 2.1 assignment_of_int_1.py 执行结果
$ python3 examples/object/reference/assignment_of_int_1.py
True

好像哪里不对劲, 跟我们之前分析的有一些出入. 这是由于 Python 会将一些常用的整数缓存起来, 不会每次都重新构造一个新的对象. 笔者使用的 Python 版本为 3.8.6, 操作系统为 Linux, 在该版本中, 范围在 \([-5, 256]\) 之间的整数都会被缓存到内存中, 为了验证我们的结论, 我们将Listing 2.1 稍加改动, 执行结果就完全不一样了.

Listing 2.2 assignment_of_int_257.py
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 运行的结果是什么.

Listing 2.3 change_reference_value.py
a = 1
b = a

a = 2
print('a =', a)
print('b =', b)

由于 ab 指向同一个对象的地址, 修改 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 的输出结果是什么.

Listing 2.4 change_list.py
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, 此时 ba 的地址是相同的.

当执行 a[0] = 'he' 时:

  • 修改了 a 所指对象中元素的值,

  • 由于 b 也指的同一个对象, 因此 b[0] 的值也发生了变化.

至此, 我们回头看Section 1 中最后的疑问, 是不是对如下代码有了更深的理解.

a = '1'
a = 1 + 3

在上述代码中, 并不是变量 a 的类型发生了变化, 而是:

  • 创建字符串对象 '1',

  • 将其起名为 a,

  • 创建整数对象 4,

  • 将其起名为 a,

整个过程中, 对象的类型没有任何隐式或者显式的转换. 因此, 再次重申: Python 是一门强类型语言.

有人要问, 你讲了这么多, 有什么证据吗? 有的, 我们通过分析 Python 字节码的反汇编就可以看出 Python 底层的运行逻辑. 以下两段代码分别是 Python 源代码以及对应的反汇编.

Listing 2.5 assignments.py
1
2
3
4
a = 1
b = 1
c = a
pass
Python Disassembly 2.1 assignments.py
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 中, 可以定义一个常量吗? 即只可以被定义, 不可以被修改的变量.