查看原文
其他

听说你会玩 Python 系列 3 - 考了 99 分的潘石屹肯定不懂的知识点

王圣元 王的机器 2022-07-20




本文含 2734 字,12 图表截屏
建议阅读 15 分钟


本文是听说你会玩 Python 系列的第三篇





引言

在 Python 中,当创建变量时,不用像 C 语言那样在前面加入变量类型,如下图所示:



对比发现在 Python 中定义变量时,不需要声明其数据类型,因此 Python 属于动态类型(dynamic typed)语言。读者可能会认为 Python 不够严谨,怎么定义变量都不带变量类型呢?


原因是 Python 中的变量只是一个名字而已,就像下图的 x 存在变量名一样,它的作用仅仅是“指向”引用对象(PyObject)。PyObject 是计算机分配的一块内存,其下有类型大小引用计数等属性。引用计数是说多少个变量名“指向”该对象,当引用计数为零时,意味着没有任何变量名引用,因此可以被回收。



为什么 x 能“轻易地”指向不同变量类型?这要深挖 Python 内部机制是如何运行下面四条语句的。


  • 定义整数 x 并赋值 1031

  • x 赋予一个新值 1032

  • 创建一个新变量 y 并等于 x

  • y 值增加 1


不可修改的整数

定义整数 x 并赋值 1031

 

  1. 表面上是敲入 x = 1031,实际发生的是:

  2. 创建一个新对象 PyObject

  3. 将该 PyObject 类型属性设为 int

  4. 将该 PyObject 值属性设为 1031

  5. 创建一个变量名,叫做 x

  6. x 指向新对象 PyObject

  7. PyObject 里的引用计数 1





x 赋予一个新值1032

 

表面上是敲入 x =1032,实际发生的是:

 

  1. 创建一个新对象 PyObject

  2. 将该 PyObject 的类型属性设为 int

  3. 将该 PyObject 的值属性设为 1032

  4. 将 x 指向新对象 PyObject

  5. 将新对象 PyObject 里的引用计数加 1

  6. 将旧对象 PyObject 里的引用计数减 1

 

旧对象“颜色变灰退出舞台”,代表着它随时会被清理。





创建一个新变量 y 并等于 x

 

表面上是敲入 y = x 时,实际发生的是:


  1. y x 指向同样的对象 PyObject

  2. 将该对象 PyObject 里的引用计数 1


注意:在上面过程中没有创建任何新对象 PyObject





y 值增加 1

 

表面上是敲入 y += 1,实际发生的是:

 

  1. 创建一个新对象 PyObject

  2. 将该 PyObject 的引用计数设为 int

  3. 将该 PyObject 的值属性设为 1033

  4. 将 y 指向新对象 PyObject

  5. 将新对象 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 并打印出地址
x = 1031id(x) 2479057898512


更新 x 的值,地址变了,因此 x 不可修改

x = 1032id(x) 2479067931376


x 赋予 y,两个指向相同对象,地址相同
y = xprint( id(x) )print( id(y) )2479067931376
2479067931376


更新 y 的值,y 的地址变了,因此 y 不可修改
y += 1print( id(x) )print( id(y) )2479067931376
2479067931440



结论:整型变量是不可修改的。


再回到上面动态类型的例子,当变量 x 定义为整数 1、字符串 'one' 和布尔值 True 时,实际上变量名 x 轮流指向三个 PyObject,因此它们的内存地址也不一样。



配着上面的解释再回顾一下引言里的图,现在都明白了吧。




可修改的列表

Python 中的整数变量是不可修改的,而列表是可修改的。虽然还没介绍列表,可把它当成一个存储元素的容器,创建一个存储 1, 10.31 和'Python' 的列表,起名为 l,它在内存中的示意图如下:



和上面整数变量一样,表面上是敲入l = [1, 10.31, 'Python'],实际发生的是:


  1. 创建一个新对象 PyObject(列表的)

    1. 将该 PyObject的类型属性设为 list

    2. 将该 PyObject的值属性指向三个地址

  2. 创建三个新对象 PyObjects(列表元素的)

    1. 将它们的类型属性设为 int, float, str

    2. 将它们的值属性设为 1, 10.31, 'Python'

  3. 将三个地址分别指向 PyObjects

  4. 创建一个变量名,叫做 l

  5. 将 指向新对象 PyObject

  6. 将 PyObject 里的引用计数加 1

 

根据上述流程,当更改列表中的元素,只是新创建其元素的 PyObject,而没有新创建列表本身的 PyObject。因此列表是可修改的,可用 id() 函数来验证更改列表前后的地址是一样的。


创建 l 并打印出地址

l = [1, 2, 3]id(l) 
2233189737736


更新 l 第一个元素值,地址没变,因此 l 可修改

l[0] = 10000id(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() 函数来验证一个变量是否可修改也不难。


难的是要知道为什么,知其然还要知其所以然!



Stay Tuned!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存