考了 99 分的潘石屹肯定不懂的 Python 知识点
在 Python 中,当创建变量时,不用像 C 语言那样在前面加入变量类型,如下图所示:
对比发现在 Python 中定义变量时,不需要声明其数据类型,因此 Python 属于动态类型(dynamic typed)语言。读者可能会认为 Python 不够严谨,怎么定义变量都不带变量类型呢?
原因是 Python 中的变量只是一个名字而已,就像下图的 x 存在变量名一样,它的作用仅仅是“指向”引用对象(PyObject)。PyObject 是计算机分配的一块内存,其下有类型、大小和引用计数等属性。引用计数是说多少个变量名“指向”该对象,当引用计数为零时,意味着没有任何变量名引用,因此可以被回收。
为什么 x 能“轻易地”指向不同变量类型?这要深挖 Python 内部机制是如何运行下面四条语句的。
定义整数 x 并赋值 1031
给 x 赋予一个新值 1032
创建一个新变量 y 并等于 x
将 y 值增加 1
定义整数 x 并赋值 1031
表面上是敲入 x = 1031,实际发生的是:
创建一个新对象 PyObject
将该 PyObject 的类型属性设为 int
将该 PyObject 的值属性设为 1031
创建一个变量名,叫做 x
将 x 指向新对象 PyObject
将 PyObject 里的引用计数加 1
给 x 赋予一个新值1032
表面上是敲入 x =1032,实际发生的是:
创建一个新对象 PyObject
将该 PyObject 的类型属性设为 int
将该 PyObject 的值属性设为 1032
将 x 指向新对象 PyObject
将新对象 PyObject 里的引用计数加 1
将旧对象 PyObject 里的引用计数减 1
旧对象“颜色变灰退出舞台”,代表着它随时会被清理。
创建一个新变量 y 并等于 x
表面上是敲入 y = x 时,实际发生的是:
将 y 和 x 指向同样的对象 PyObject
将该对象 PyObject 里的引用计数加 1
注意:在上面过程中没有创建任何新对象 PyObject
将 y 值增加 1
表面上是敲入 y += 1,实际发生的是:
创建一个新对象 PyObject
将该 PyObject 的引用计数设为 int
将该 PyObject 的值属性设为 1033
将 y 指向新对象 PyObject
将新对象 PyObject (即 y 指向的对象) 里的引用计数加 1
将旧对象 PyObject (即 x 指向的对象) 里的引用计数减 1
由上图可知,在 Python 中,即便对于一个简单的整数,它不单单包含其值,还包含其类型、大小和引用计数,封装成 PyObject。根据不同的变量值会生成不同的 PyObject,而变量名可以随意指向 PyObject。
让人迷惑是第三步,当 x 和 y 同时指向值为 1032 的 PyObject,但在第四步将 y 加 1,x 却保持不变。虽然迷惑但是合理,要不然改变 y 也改 x 会造成很多麻烦。但为什么改变 y 而不是改变 x 呢?原因在于改变 y 时新建了一个值为1033 的 PyObject,并将 y 指向它,而 x 还是指向原来值为 1032 的 PyObject。
从上面描述可以侧面推出整数是不可修改(immutable)的,因为更改变量值不是在原来的 PyObject 里改,而是新创建一个 PyObject。
判断变量 x 是否可修改,用 id(x) 函数,该函数打印出变量 x 的地址。
如果 x 可修改,那么更新其值前后的地址一样
如果 x 不可修改,那么更新其值前后的地址不一样
x = 1031
id(x)
2479057898512
更新 x 的值,地址变了,因此 x 不可修改
x = 1032
id(x)
2479067931376
y = x
print( id(x) )
print( id(y) )
2479067931376
2479067931376
y += 1
print( id(x) )
print( id(y) )
2479067931376
2479067931440
结论:整型变量是不可修改的。
再回到上面动态类型的例子,当变量 x 定义为整数 1、字符串 'one' 和布尔值 True 时,实际上变量名 x 轮流指向三个 PyObject,因此它们的内存地址也不一样。
配着上面的解释再回顾一下引言里的图,现在都明白了吧。
Python 中的整数变量是不可修改的,而列表是可修改的。虽然还没介绍列表,可把它当成一个存储元素的容器,创建一个存储 1, 10.31 和'Python' 的列表,起名为 l,它在内存中的示意图如下:
和上面整数变量一样,表面上是敲入l = [1, 10.31, 'Python'],实际发生的是:
创建一个新对象 PyObject(列表的)
将该 PyObject的类型属性设为 list
将该 PyObject的值属性指向三个地址
创建三个新对象 PyObjects(列表元素的)
将它们的类型属性设为 int, float, str
将它们的值属性设为 1, 10.31, 'Python'
将三个地址分别指向 PyObjects
创建一个变量名,叫做 l
将 l 指向新对象 PyObject
将 PyObject 里的引用计数加 1
根据上述流程,当更改列表中的元素,只是新创建其元素的 PyObject,而没有新创建列表本身的 PyObject。因此列表是可修改的,可用 id() 函数来验证更改列表前后的地址是一样的。
创建 l 并打印出地址
l = [1, 2, 3]
id(l)
2233189737736
更新 l 第一个元素值,地址没变,因此 l 可修改
l[0] = 10000
id(l)
2233189737736
和列表不同,元组是不可修改的。创建一个元组 t,注意里面还包含一个列表 [1, 2]。
t = (1, [1, 2], 'Python')
它在内存中的示意图如下(注意第二个列表元素又指向两个整型 PyObject):
由于元组不可修改,直接给元组元素赋值会报错。
t[1] = [1, 2, 3]
TypeError: 'tuple' object does not support item assignment
但只要元组中的元素可修改,比如列表,那么可以更改它,注意这跟赋值其元素不同。
t[1].append(3)
t
(1, [1, 2, 3], 'Python')
这也好理解,由于列表可修改,因此在[1, 2] 后面加个 3 不会改变列表的内存地址 0x210640,因此元组的内存地址也没有改变。但如果将整个列表重新赋值,那么要新创建一个列表赋给元组第二个元素,列表的地址肯定改变了,那么元组的内存地址也改变了,这样就违背了元组不可修改的特性,所以会报错。
记住整数和元组不可修改、列表可修改一点也不难。
知道用 id() 函数来验证一个变量是否可修改也不难。
难的是要知道为什么,知其然还要知其所以然!
-END-
扫码添加早小起
1. 回复「进群」进入Python技术交流群
2. 回复「Python」获得Python技术图书
3. 回复「习题」领取Python数据处理200题