Chapter-7 解释器

重点总结:

  • Python 如何执行代码
  • Python 如何管理内存
  • Python 垃圾回收的工作原理
  • int 类型是否有最大值,为什么 float 有上限?

1 | Python 代码如何执行❓

Python 不会将代码编译为机器指令(如C或C++),而是翻译为一种中间字节码(或 p-code),然后再由 Python 解释器执行:

  1. 解析:检查是否存在语法错误或其他异常
  2. 编译 compile():将代码编译为 bytecode 类型
  3. 将编译后代码执行

2 | Python 如何管理内存❓

  • Heap Space:维护所有的对象,一旦某个对象的引用为 0 ,垃圾回收器就会将它删除

  • Stack Space:存放所有堆空间中对象的引用名称,即变量名

  • 垃圾回收机制

3 | 垃圾回收机制

Garbage Collection

🔗 相关链接:Garbage collector design (python.org)

该机制与解释器底层实现有关,以下工作原理针对 Cpython:

以引用计数 Reference Counting 为主,分代回收器 cyclic garbage collector 为辅,分代回收器可通过标准库 gc 手动操控

3-1 | 引用计数

每个对象维护一个ob_refcnt 属性,记录对象被引用的”地方“(次数),

当对象引用时,引用次数增加:如被创建引用函数局部作用域中使用

当对象被取消引用时,引用次数减少:如 del名称赋给新对象对象所在容器销毁

引用计数归零就被立即回收,占用的内存被释放

💻 查看对象的引用计数 sys.getrefcount(var)

💻 引用对象但不增加引用计数

import sys
import weakref
class A:
   pass

>>> a = A()
>>> sys.getrefcount(a)
2

>>> b = weakref.ref(a)
>>> sys.getrefcount(a)
2  # weak reference 并不会增加引用计数

>>> c = a
>>> sys.getrefcount(a)
3

主要问题:对象引用自己(引用循环)即使被删除也不会使引用计数减少😂,因为对象依旧维护其内部引用

因此,普通的引用计数机制无法释放 reference cycle,需要额外机制清除(gc模块)

container = []
container.append(container)
del container

3-2 | 分代回收

处理 Reference Cycle,只专注回收容器型对象,主要逻辑写在 gc 模块中,

实现基础:基于实际编程中大部分对象生命周期很短的假设

分代规则

Python 根据对象的存活时间分为第0代 、第1代、第2代,

所有新创建的对象放入第0代,该代中对象数量超过临界值会触发垃圾回收,未被回收的对象会放入下一代,直到第2代,越靠后代被垃圾回收的频率越低

💻 查看每代对象数量上限

import gc
gc.get_threshold()
(700, 10, 10)

为何要给对象分代❓ 限制垃圾回收过程花费的时间

回收两阶段

标记阶段 Mark,遍历所有的对象,如果还有对象引用它,则标记该对象可达 reachable

清除阶段 Sweep,再次遍历对象,回收没有标记为可达的对象;

4 | 可变性

类型决定值的可变性,即是否能修改

  • 不可变对象 ----- 一个贴着封条的透明盒子,可以看到值,但无法改动,Read-only

    An object with a fixed value. Immutable objects include numbers, strings and tuples. Such an object cannot be altered. A new object has to be created if a different value has to be stored. They play an important role in places where a constant hash value is needed, for example as a key in a dictionary.

    ——官方文档 3.11

  • 可变对象 ----- 一个开盖儿的盒子,不仅能看到里面的值,还能修改,但是没法改变类型

    Mutable objects can change their value but keep their id()
    ——官方文档 3.11

可变与不可变对象的本质区别:可变对象的值发生改变但依旧保持相同的 id

>>> l = [1, 2, 3]
>>> id(l)
2965675544384
>>> l.pop()
3
>>> id(l)
2965675544384

💡提示修改可变对象本身的方法均默认返回 None,这是 Python 对于所有可变对象方法的设计原则之一,如 list 的方法 insert()

类型代码名称是否可变示例
1int整数
2float浮点数
3complex复数
4str文本字符串
5bool布尔值True, False
6list列表['Winken', 'Blinken', 'Nod']
7tuple元组
8dict字典
9set集合
10frozenset冻结集合frozenset(['Elsa', 'Otto'])
11bytes字节序列b'ab\xff'
12bytearray字节数组bytearray(...)

5 | 字面值

Python 中指定数值的方法:

  • 字面值 Literal:通过文字、数字等直接赋值

    Literals are notations for constant values of some built-in types.

  • 变量:将一个变量的值赋给另一个变量

6 | 识别器 Identifier

  • 长度无限制

  • 组成元素

    ASCII 中的部分字符(Python 2)

    • 数字:0 ~ 9,但不能在开头
    • 字母:小写 a ~ z 大写 A~Z
    • 下划线 _

    Unicode Character Database 中的字符(Python 3 开始引入 )

    • 所有字符:https://www.unicode.org/Public/14.0.0/ucd
    • 字符数据已内置在标准库 unicodedata
  • 区分大小写:thingThingTHING 是不同的名称

  • 不能是 Python 的保留字语法关键字

7 | 深浅复制

深浅复制的区别只与复合对象相关,即包含另一个对象的对象,如 list 、类实例

  • 浅复制构建一个新的复合对象,并引用源对象包含的所有对象
  • 深复制构建一个新的复合对象,并递归复制源对象包含的所有对象

The difference between shallow and deep copying is only relevant for compound objects (objects that contain other objects, like lists or class instances):

  • A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.
  • A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

标准库 copy 可用于深浅复制

🧲 一般情况:复制不可变对象,两个变量即使后续改为其他值也互不影响

a = 1
b = a
b = 2  # 修改 b 的值

>> a, b  
1, 2  # 二者相互独立

🧲一般情况:复制可变对象,如️list dict set ,如果赋值给两个变量,修改一个会改变另一个,因此它们内置方法 copy()

>>> a = [2, 4, 6]
>>> b = a
>>> a[0] = 1  # 修改 a 中一个值

>>> a, b
([1, 4, 6], [1, 4, 6])  # b 也跟着改变

🧲特殊情况:复合对象中包含复合对象

import copy
a = [1, 2, 3]
b = [4, 5, 6]
c = [a, b]

# 浅复制
d = copy.copy(c)
print(id(c) == id(d))  # False,c 与 d 已不同一对象
print(id(c[0]) == id(d[0]))  # True,但 a 依旧是同一个
d[0][1] = 4
print(d)  # [[1, 4, 3], [4, 5, 6]]
print(c)  # [[1, 4, 3], [4, 5, 6]]

e = copy.deepcopy(c)
print(id(c) == id(e))  # False,c 与 e 已不同一对象
print (id(c[0]) == id(e[0]))  # False, 递归复制了包含的对象 a
e[0][0] = 9
print(e)  # [[9, 4, 3], [4, 5, 6]]  二者不再联动,相互独立
print(c)  # [[1, 4, 3], [4, 5, 6]]

8 | int 最大值

整数溢出: 计算的数字或结果超出了计算机允许的存储空间,大部分编程语言会出现的问题,但 Python 不会

Python2:

  • 可存 32 位,int类型 ,即 -21474836482147483648

  • 可存 64 位,long 类型,即 -9 223 372 036 854 775 8089 223 372 036 854 775 807

Python3 中仅有int(实际类似于long类型),整数可存储任意大小,最大长度却决于内存

Python 的整数为何没有大小限制?

Python 的整数类型会根据其存储的值动态调整数字位数,C 与 Java 中数字的最大长度采用了 32 或 64 位

Python 中两个大数相乘时,会用到一种快速乘法,由俄罗斯数学家 Anatoly Karatsuba 于1960年发现:

# 计算 12345*6789,选择 B=10, m=3, 然后用得到的基(Bm = 1000)分解输入操作数:
2345 = 12 · 1000 + 345
6789 = 6 · 1000 + 789

# 只有三个乘法运算较小的整数,用于计算三个部分结果:
z2 = 12 × 6 = 72
z0 = 345 × 789 = 272205
z1 = (12 + 345) × (6 + 789) − z2 − z0 = 357 × 79572272205 = 28381572272205 = 11538

# 通过添加这三个部分结果得到结果,相应地移位(然后通过在输入操作数中分解基数1000中的这三个输入来考虑进位):
result = z2 · B + z1 · B + z0,
result = 72 · 1000*1000 + 11538 · 1000 + 272205 = 83810205

9 | float 最大/小值

Python 单个浮点数超过 1.8 ⨉ 10308 会转换为 inf

>>> 1.79e308
1.79e+308
>>> 1.8e308
inf

⚠️Python 单个浮点数接近 5.0 ⨉ 10-324 会转换为 -inf

>>> 5e-324
5e-324
>>> 1e-325
0.0

为何浮点数有上下限

根据 IEEE 754,大部分系统以 64 位双精度值作为 python 浮点数的实现

10 | iterable

list, set, sequence, generator, 或者其他每次遍历可返回一个元素的对象

11 | 序列 Sequence

An iterable which supports efficient element access using integer indices via the __getitem__() special method and defines a __len__() method that returns the length of the sequence

🆚iterable:序列一定是可迭代的,可迭代的不一定是序列

  • 容器序列:可存放复杂类型的元素,也可嵌套,相当于存放对象的引用,如listtuplecollections.deque
  • 扁平序列:可存放简单类型的元素,相当于存放值而不是引用,实质是连续的内存空间,如 str, bytesarray.array

按照可变性划分

  • 可变序列:list、bytearray、array.array、collections.deque
  • 不可变序列:tuple、str、byte

二者关系:可变继承不可变序列,并实现了更多方法

from collections import abc
>>> issubclass(tuple, abc.Sequence)
True
>>> issubclass(list, abc.MutableSequence)
True

11-1 | 为何序列取值时索引从 1 开始❓

由来
  • 1966 年,BCPL 语言的创始人 Martin Richards 首次提出以 0 作为数组的索引开始,以便计算内存地址取值
  • 1982 年,荷兰计算机科学家 Edsgar W. Dijkstra 也提倡这一方案,以及不包含最后一项,曾写过这量种设计原则的原因,原文连接:

Why numbering should start at zero (EWD 831)www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html

​ 大意是包含下限,但不包含上限的半开区间不会用到负数表示区间范围,还可以更好的表示空集

  • C、Python 等编程语言沿用了这一设计
好处

索引从 0 开始

  • 方便数组寻找内存地址(实际未必方便):只要在当前元素的内存地址上增加位数即可获取下一个元素的
假设机器在内存地址 any_pos 开始存数组 Array,则:
Array[0] 的地址为 any_pos + 0 * size
Array[1] 的地址为 any_pos + 1 * size
Array[2] 的地址为 any_pos + 2 * size
  • 方便计算长度:只有终止位置的索引时,可直观看出元素数量,如 range(10) 包含 10 个元素,arr[:5] 包含 5 个元素,从 1 开始则还需+1
  • 方便循环队列取值:假设循环队列 arr 长度为 N
第一个元素   arr[0]
第二个元素   arr[1]
....
第 N 个元素  arr[N-1],arr 的最后一个元素
第 N+1 个元素,其索引为 N % N = 0 , 正好回到列表开头

不包含最后一项

  • 方便计算长度:有起始位置与终止位置时,直接相减即是元素数量,包含最后一项则减完还要 + 1
  • 方便切片拼接: 如 arr[:i] + arr[i:],如果包含最后一项则拼接后多了一项 arr[i]
从 1 开始

某些编程语言从 1 开始:AWK, COBOL, Fortran, R, Julia, Lua, MATLAB, Smalltalk, Wolfram…

11-2 | 何为序列❓

3 种基础序列类型:list, tuple, range,附加了两种:str byte

大部分支持以下方法:

OperationResultlisttuplerangestr
x in sTrue if an item of s is equal to x, else False
x not in sFalse if an item of s is equal to x, else True
s + t相同类型相加
s * n or n * s乘上自己的 n
s[i]索引取值
s[i:j]slice of s from i to j
s[i:j:k]slice of s from i to j with step k
len(s)length of s
min(s)smallest item of s
max(s)largest item of s
s.index(x[, i[, j]])index of the first occurrence of x in s (at or after index i and before index j)
s.count(x)total number of occurrences of x in s

11-3 | 序列解构

Sequence Unpacking,PEP 3232

将一个包含 N 个元素的序列分解为 N 个单独的变量,也叫平行赋值 parallel assignment

# 解包元组
>>> s = (1, 2)
>>> a, b = s  # 本质是解包+打包的过程
>>> a  # 1
>>> b  # 2

# 二维序列亦可解构
>>> l = ['a', (1,2,3)]
>>> a, (b,c,d) = l

⚠️变量数要小于序列的元素数量,否则ValueError

l = [1,2,3]
>>> a,b,c,d = l
ValueError: not enough values to unpack (expected 4, got 3)

💡提示:避免上述 ValueError,可使用 *收集多余的数据,但数据类型可能有变化

>>> s = "abcdef"
>>> a, *b = s
>>> b           # ['b', 'c', 'd', 'e', 'f']  此时 b 不再是字符串!!

🛠️应用:获取指定位置值

l = [1,2,3,4]
>>> a, *b = l   # 只获取第一个
>>> a, b        # 1, [2, 3, 4]

11-4 | 数值比较

Sequence 对象通常可以相互比较,以数字大小字母先后顺序Unicode 字符位置元素数量决定大小,不同类型不能比较

# 以下结果均为 True
(1, 2, 3) < (1, 2, 4)
[1, 2, 3] < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4) < (1, 2, 4)
(1, 2) < (1, 2, -1)
(1, 2, 3) == (1.0, 2.0, 3.0)  # int 和 float 均有比较值相关的方法实现,因此可以相互比骄傲
(1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4)
"a" < "我"

(1,2,3) < [1,2,3]  # TypeError

12 | 为什么 len() 是一个独立方法?

对于Python 内置数据类型,Cpython 会直接从一个 C 结构体里读取对象的长度,不会调用任何方法,更高效

13 | 运算符重载机制

+ 调用 __add__,全部 Dunder 方法可参考文档的 Data Model 部分

  • 算术运算符:__add__, __mul__

  • 反向运算符:r 开头 __radd__,当两个运算的数值为不同类型,且左侧操作值 operand 底层未实现相关接口或相关接口返回 NotImplemented相当于实现数学中的交换律概念

    举例:x-y, 如果 type(x).__sub__(x,y) 返回 NotImplemented 则调用 type(y).__rsub__(y,x)

  • 增量赋值算数运算符:i 开头,__iadd__,可把中缀运算符变为赋值运算符, a = a*b 转为 a*=b ,相当于 a=a.__imul__(b)

14 | __repr____str__ 的区别

二者功能都是以字符串形式表示对象

__str__str() pinrt()使用的,返回的字符串对交互式命令行更友好

如果二者只实现一个,则用 __repr__

因为如果对象没有实现 __str__ 方法,但 Python 又需要调用时,会用 __repr__ 作为代替

15 | 增量运算之谜

增量加法 +=,增量乘法 *=报错又赋值成功的谜之情况

l = (1, 2, [30, 40])

>>> l[2] += [50, 60]
TypeError: 'tuple' object does not support item assignment

>>> l  # (1, 2, [30, 40, 50, 60])

背后原因

  1. Python 先计算 [30, 40]+=[50, 60] ,成功执行
  2. 再将结果赋值给元组,报错 TypeError

小结

  • 不要在元组中存放可变的项

  • 增量赋值不是原子操作

  • l[2].extend([50, 60]) 避免这种极端况的出现

16 | 专门化自适应解释器

specializing adaptive interpreter

工作原理

3.10 版本开始实施专门化自适应解释器,该解释器在执行过程中”改编“代码(bytecode)以优化重复执行的相同操作

执行步骤:

  1. Quickening:解释器找到多次执行的 bytecode,作为专门化的候选

  2. Specialization:解释器将普通的 bytecode 替换(改编)为专门化的 bytecode,如将浮点数相加的操作替换为普通的相加操作

3.12 版本的 Quickening 过程比 3.11 版本更快,而且能专门化更多的 bytecode

3.11 版本,相同类型参数的 bytecode 重复执行 8 次会触发专门化,但在 3.12 中 2 次即会触发

🧲示例:解释器如何专门化
在这里插入图片描述

import dis
from math import pi

def add(x:float, y:float) -> float:
	return x + y

dis.dis(add, adaptive=True)  # dis 代表 disassemble(分解)
add(1.0, 99.0)
add(1.0, 999.0)    

同一方法执行两次后,解释器认为之后的操作依旧是两种相同类型

行内推导式

Inline Comprehension

Python 3.12 版本解释器新增的机制,以往的推导式在执行时被编译为一个函数,会隔离被迭代的对象,运行时性能不佳,PEP 709 提议在编译时将 list dict set 推导式融入其所在函数,以提升执行性能

17 | 如何提速?

如果需要提高运行速度,除了默认的 CPython 还有其他底层实现:

  • PyPy:加入用于提速 Java 的技术,未来可能替代 Cpython
  • NumPy

第三方库 numba:可将 Python 代码动态编译成机器码,以提高运行速度

END

  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值