重点总结:
- Python 如何执行代码
- Python 如何管理内存
- Python 垃圾回收的工作原理
- int 类型是否有最大值,为什么 float 有上限?
1 | Python 代码如何执行❓
Python 不会将代码编译为机器指令(如C或C++),而是翻译为一种中间字节码(或 p-code),然后再由 Python 解释器执行:
- 解析:检查是否存在语法错误或其他异常
- 编译
compile()
:将代码编译为 bytecode 类型 - 将编译后代码执行
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()
等
类型代码 | 名称 | 是否可变 | 示例 | |
---|---|---|---|---|
1 | int | 整数 | ❌ | |
2 | float | 浮点数 | ❌ | |
3 | complex | 复数 | ❌ | |
4 | str | 文本字符串 | ❌ | |
5 | bool | 布尔值 | ❌ | True , False |
6 | list | 列表 | ⭕ | ['Winken', 'Blinken', 'Nod'] |
7 | tuple | 元组 | ❌ | |
8 | dict | 字典 | ⭕ | |
9 | set | 集合 | ⭕ | |
10 | frozenset | 冻结集合 | ❌ | frozenset(['Elsa', 'Otto']) |
11 | bytes | 字节序列 | ❌ | b'ab\xff' |
12 | bytearray | 字节数组 | ⭕ | 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
中
- 数字:
-
区分大小写:
thing
、Thing
和THING
是不同的名称 -
不能是 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
类型 ,即-2147483648
至2147483648
-
可存 64 位,
long
类型,即-9 223 372 036 854 775 808
至9 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 × 795 − 72 − 272205 = 283815 − 72 − 272205 = 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:序列一定是可迭代的,可迭代的不一定是序列
- 容器序列:可存放复杂类型的元素,也可嵌套,相当于存放对象的引用,如
list
、tuple
和collections.deque
- 扁平序列:可存放简单类型的元素,相当于存放值而不是引用,实质是连续的内存空间,如
str
,bytes
和array.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
大部分支持以下方法:
Operation | Result | list | tuple | range | str |
---|---|---|---|---|---|
x in s | True if an item of s is equal to x, else False | ⭕ | ⭕ | ⭕ | ⭕ |
x not in s | False 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])
背后原因:
- Python 先计算
[30, 40]+=[50, 60]
,成功执行 - 再将结果赋值给元组,报错 TypeError
小结
-
不要在元组中存放可变的项
-
增量赋值不是原子操作
-
用
l[2].extend([50, 60])
避免这种极端况的出现
16 | 专门化自适应解释器
specializing adaptive interpreter
工作原理
3.10 版本开始实施专门化自适应解释器,该解释器在执行过程中”改编“代码(bytecode)以优化重复执行的相同操作
执行步骤:
-
Quickening:解释器找到多次执行的 bytecode,作为专门化的候选
-
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