解释型语言和编译型语言的区别在于:源代码是“边翻译边执行”,还是“翻译好了再执行”。
1. 解释型语言(Interpreted Language)
原理:源代码不是直接由处理器执行,而是由解释器逐行读取、转换并立即执行。
- 工作流程:它不产生机器码,而是由Python 解释器内部的 编译器产生中间代码(字节码)即__pycache__ 文件夹下的 .pyc 文件,字节码比源代码更接近机器语言,但又是平台无关的。这样下次运行同一份代码时,如果源码没改,Python 就可以跳过编译步骤,直接加载 .pyc 文件,从而加快启动速度,字节码并不能直接在你的 CPU 上跑,它需要一个“运行环境”,这就是 Python 虚拟机 (简称 PVM)。PVM 是 Python 解释器的核心部分。它维持着一个巨大的循环,不停地读取字节码指令,并将其映射到具体的 C 语言函数调用,最终由 CPU 执行。
- 典型代表:Python、JavaScript、Ruby、PHP。
- 特点:跨平台性强;运行速度相对较慢。
2. 编译型语言(Compiled Language)
原理:运行前由编译器一次性把源码转换成机器可执行文件,再执行该文件。
- 工作流程:编译器直接将源代码翻译成针对特定 CPU 架构(如 x86, ARM)和操作系统(Windows, Linux)的二进制机器码
- 典型代表:C、C++、Go、Rust、Swift。
- 特点:平台依赖更强;改动后通常需要重新编译。
3. 为什么解释型语言运行速度慢
- 额外的开销:解释器在执行每一行代码时,都要经历:读取 → 解析 → 转换 → 执行。而编译型语言在运行阶段只有“执行”这一个动作。
- 动态检查:Python 等解释型语言通常是动态类型的。这意味着在运行时,解释器必须不断检查变量是什么类型(是数字还是字符串?),能不能做加法?这种“边跑边想”的过程极其耗时。
- 无法深度优化:编译器在编译阶段可以通览全局,进行复杂的逻辑优化(比如删掉永远不会运行的代码,或者合并重复计算)。解释器“走一步看一步”,很难进行这种全局层面的极限优化。
4. 为什么编译型语言“平台依赖强”?
- 硬件绑定:不同 CPU 识别的指令集不同(如 x86 与 ARM)。
- 系统绑定:不同操作系统的系统调用接口不同(Windows 与 Linux 机制差异明显)。
- 结果:编译时硬件与系统细节已被写入二进制文件,因此 Windows 的
.exe不能直接在 Linux 上运行。
5. 为什么编译型语言“改动后通常要重新编译”?
核心逻辑:编译产物是整体性、静态关联的结果。
- 整体流程:预处理 → 编译 → 汇编 → 链接,编译器要统一处理源码与依赖并建立地址映射。
- 静态关联:函数调用关系和符号地址在构建阶段已确定。
- 蝴蝶效应:即使只改很小一处,也可能引发内存偏移、符号表与链接关系变化,因此需要重新生成可执行文件。
6. 现代趋势:JIT(即时编译)的中间路线
- 启动阶段:先解释执行,保留跨平台和开发效率优势。
- 热点识别:PVM 不再仅仅是死板地逐行解释字节码。如果它发现某段字节码被反复执行(比如一个循环),JIT 编译器会介入,把这段热点字节码直接编译成 机器码。
1. 核心特性(Core Features)
A. 语法与编程范式
- 简洁易读:强制缩进定义代码块,提升可读性并降低维护成本。
- 多范式支持:支持面向对象与结构化编程,并融合函数式(
lambda、map、filter、itertools)与元编程。 - 动态类型与内存管理:动态类型 + 延迟绑定;引用计数与增量 GC(3.14 优化)自动管理内存。
B. 性能与底层优化
- 实验性 JIT:3.13 引入、3.14 持续优化,提升部分计算密集型任务性能。
- 自由线程模式:逐步支持关闭 GIL,实现多核并行。
2. list 和 tuple 的区别?
1. 核心区别对比
- 可变性:
list可变 ;tuple不可变。 - 语法:
list使用[],tuple使用()。 - 内存占用:
list通常更大(为扩容预留空间);tuple更紧凑。 - 哈希性:
list不可哈希,不能做字典键;tuple在元素可哈希时可作为键 。
总结:元组想作为字典键,里面的每个元素都必须是不可变(可哈希)的。
1. 什么是可哈希(Hashable)?
- 不可变:对象生命周期内值不会变化(如
int、str、float、bool)。 - 可计算哈希:支持
__hash__(),可生成稳定哈希值供字典定位。 - 元组本身不可变,但若内部元素可变(如
list),整个元组就不可哈希。
2. 常见类型可变性对比
| 数据类型 | 是否可变 |
|---|---|
int / float / complex | 否(不可变) |
str | 否(不可变) |
tuple | 否(不可变) |
frozenset | 否(不可变) |
bytes | 否(不可变) |
list | 是(可变) |
dict | 是(可变) |
set | 是(可变) |
bytearray | 是(可变) |
不可变对象一旦创建,值不能原地修改;“修改”本质上是创建新对象。可变对象可以原地修改内容,通常对象 id 不变。
3. 经典陷阱:可变对象作为默认参数
下面代码会在多次调用间共享同一个列表对象:
def add_item(item, box=[]):
box.append(item)
return box
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] 不是 [2]
正确写法通常是默认值使用 None,在函数内部再初始化。
1. 增
append(obj):在列表末尾添加一个元素。示例:ls.append(4)→[1, 2, 3, 4]。insert(index, obj):在指定索引位置插入元素,原位置及之后的元素后移。示例:ls.insert(1, 'a')→[1, 'a', 2, 3]。extend(iterable):将另一个序列内容合并到当前列表末尾(相当于+=)。示例:ls.extend([5, 6])→[1, 2, 3, 5, 6]。
2. 删
pop([index]):删除并返回指定索引的元素(默认最后一个)。示例:item = ls.pop()。remove(obj):删除列表中第一个匹配到的值(不匹配会报ValueError)。示例:ls.remove(2)。clear():清空整个列表,使其变成[]。del语句:可以按索引/切片删除,也可删除变量。示例:del ls[0]、del ls[1:3]。
3. 查
- 索引访问
ls[i]:通过下标获取元素(支持负数索引,如-1为最后一个)。 - 切片
ls[start:stop:step]:获取子列表。示例:ls[1:3]。 index(obj):返回第一个匹配值的索引。count(obj):统计某个元素出现次数。in关键字:判断元素是否存在。示例:if 2 in ls: print("Found")。
4. 改
- 直接赋值:通过索引覆盖原有值。示例:
ls[0] = 100。 - 切片赋值:一次性替换一段范围元素。示例:
ls[1:3] = [7, 8]。 reverse():反转列表中的元素顺序。sort():对列表进行原地排序(升序或自定义规则)。
2. 为什么元组常更快、更省内存?
2.1 结构精简:减少间接寻址
- list:为支持动态扩容,通常维护可增长数组并采用 over-allocation(预留容量)。
- tuple:长度固定,布局紧凑,元素槽位与对象结构绑定更稳定。
- 迭代差异:
list在访问路径上常有更多间接跳转;tuple偏移关系更固定
2.2 缓存与复用:不可变对象更容易优化
- 空元组单例:
()全局复用。 - 常量池友好:常量元组可在编译阶段进入常量池。
- 分配复用潜力:不可变语义使某些场景下对象管理更高效;而
list因可变性通常需要保持独立实例语义。
2.5 底层结构示意(SVG)
3. 场景适配?
使用 List 的场景
- 长度不固定:需要频繁增删元素。
- 原地修改:需要
.sort()、.reverse()等操作。
4. 列表中所有元素类型必须相同吗?
不必须。Python 的 list可以同时装入不同类型的对象。
4.1 为什么 Python 允许异构列表?
- 万物皆对象:Python 中每个值本质都是对象。
- list 存的是引用:CPython 的列表底层可理解为“对象指针数组”(保存对象地址)。
- 间接访问:列表不关心元素具体类型,只保存地址;取值时再根据地址找到真实对象并按其类型处理。
这与 C/Java 传统数组不同:后者通常强调同类型、固定布局,便于按偏移量快速定位。
4.2 混合类型的代价
- 逻辑风险:循环里做统一运算(如
item + 1)时,若混入字符串会触发TypeError。 - 性能损耗:类型不统一会降低解释器优化空间,不利于向量化与批量数值计算。
4.3 什么时候应该保持类型统一?
- 数值计算:建议使用
array.array或NumPy(同构类型,计算效率高)。 - 结构化异构数据:如“姓名、年龄、体重”,更推荐
dict、dataclass,语义更清晰。
使用 Tuple 的场景
- 记录型数据:表达不可拆分结构单元,如经纬度
(39.9, 116.4)。 - 安全传递:避免函数内部意外修改。
- 字典键:组合键必须是可哈希对象,元组更合适。
- 函数多返回值:Python 默认会将多返回值打包为元组。
3. == 和 is 的区别?
1. 核心区别:
==(相等运算符):调用__eq__(),判断两个对象的值是否相等 。is(身份运算符):比较对象内存地址(id),判断是否是同一个对象。
结论:== 是否包含类型判断,取决于对象内部如何实现 __eq__()。
1. 默认行为
若不同类没有自定义 __eq__,通常按对象身份比较,结果一般为 False。
2. 内置类型常见实现
很多内置容器会先看类型是否匹配,再比较值。例如:
print([1, 2] == (1, 2)) # False
3. 数值类型特例(跨类型)
print(1 == 1.0) # True
print(1 == (1+0j)) # True
数字比较更偏向“数学值是否相等”,而非类型必须一致。
4. 自定义类建议
推荐在 __eq__ 里使用 isinstance() 做类型检查,避免意外比较错误。
class User:
def __init__(self, id):
self.id = id
def __eq__(self, other):
if not isinstance(other, User):
return False
return self.id == other.id
2. is bug?
Python 对小整数和部分常量做缓存复用(例如小整数对象池),可能导致两个变量指向同一对象,从而 is 为 True。因此不能把这类现象当作通用规律。
4. 最佳实践:
- 单例判断用
is:尤其是None(推荐if x is None:)。 - 布尔判断优先真值语义:通常直接写
if x:/if not x:。
4. PEP 8 是什么?
PEP 8 是 Python 官方《代码风格指南》,属于 PEP体系中专门规范“代码风格”的核心文档。
1. PEP 8 的核心规范
代码布局
- 缩进:使用 4 个空格,避免混用 Tab 与空格。
- 行宽:建议每行不超过 79 个字符。
- 空行:顶级函数/类之间 2 行;类内方法之间 1 行。
命名规范
- 函数与变量:
snake_case,如calculate_total_price。 - 类名:
PascalCase,如UserProfile。 - 常量:全大写,如
MAX_RETRIES。
表达式与语句
- 空格规则:二元运算符两侧加空格;括号内避免多余空格。
- 导入顺序:标准库 → 第三方库 → 本地库,并分组分行。
5. self、__init__、__new__、*args、**kwargs、推导式、pass / break / continue
1. __new__ vs __init__:对象诞生流程
__new__(创建者):(第一个参数是 cls),负责创建实例,先于__init__执行,必须返回实例对象。__init__(初始化者):(第一个参数是 self),在实例创建后执行,负责设置初始状态(如self.name = name),返回值应为None。
2. self 到底是什么?
- 本质:
self是实例对象本身的引用。 - 作用:通过
self访问实例属性与实例方法。 - 补充:
self不是关键字;定义方法时需显式写出,调用时由解释器自动传入实例。
3. *args 与 **kwargs:动态参数
*args:接收多余位置参数,打包为tuple。**kwargs:接收多余关键字参数,打包为dict。
4. 推导式、pass / break / continue
推导式用于从迭代对象快速构建新序列,语法更紧凑,通常比显式循环更 Pythonic。
常见推导式类型
- 列表推导式:
[x**2 for x in range(5)]→[0, 1, 4, 9, 16] - 字典推导式:
{x: x**2 for x in range(3)}→{0: 0, 1: 1, 2: 4} - 集合推导式:
{x for x in 'abracadabra' if x not in 'abc'}→{'r', 'd'} - 生成器表达式:
(x**2 for x in range(5)),返回迭代器,惰性求值、内存更友好 。
核心价值:惰性求值
生成器不存储完整数据,它只存储“如何生成下一个数据”的算法与状态。
1. 内存差异
- 列表/元组:会把所有结果一次性放进内存;数据量极大时容易撑爆内存。
- 生成器:只占用很小且相对固定的空间,按需产出(每次
next()产出一个值)。
2. 时间差异
- 生成器:即用即生;你只要前 10 个结果,它生成到第 10 个就可停。
- 列表/元组:通常需要先构建完整结果,再进入后续消费逻辑。
3. 为什么没有“元组推导式”语法?
- 元组不可变,若要得到元组,通常写成
tuple(表达式 for ...),即“先按迭代生成,再转元组”。
4. 关键应用:作为函数参数
生成器表达式最常见、最优雅的用法是直接喂给聚合函数:
# 边生成边累加,不创建巨大的中间列表/元组
total = sum(x**2 for x in range(1000000))
这样 sum 可边消费边计算,通常更省内存、更快进入有效计算。
循环控制关键字
A. break
- 动作:立即结束当前最内层循环。直接跳到循环之后的语句。
B. continue
- 动作:结束本轮迭代,进入下一轮循环。本轮
continue后的代码不再执行。
C. pass
- 动作:不执行任何操作,仅保持语法完整。空函数、空类、待补实现代码块。
5. lambda 和普通 def 的区别
- 共同点:本质都能创建函数对象,都可被调用、传参、返回。
- 差异 1:
lambda是匿名函数;def是具名函数。 - 差异 2:
lambda只能写单个表达式;def可写多行逻辑(分支/循环/异常处理/注释/文档字符串)。 - 后端常用:排序、过滤、映射等轻量场景。
users = [{"name": "Tom", "age": 20}, {"name": "Amy", "age": 18}]
# 排序:按年龄升序
print(sorted(users, key=lambda x: x["age"]))
# 过滤:仅保留成年
print(list(filter(lambda x: x["age"] >= 18, users)))
# 映射:抽取姓名
print(list(map(lambda x: x["name"], users)))
6. 实例方法、@staticmethod、@classmethod 的区别
- 实例方法:自动传入
self,用于读写实例状态。 @staticmethod:不自动传入self/cls,适合工具函数。@classmethod:自动传入cls,常用于工厂方法。
class User:
role = "user"
def __init__(self, name):
self.name = name
def rename(self, new_name): # 实例方法
self.name = new_name
@staticmethod
def is_valid_name(name):
return isinstance(name, str) and len(name) > 0
@classmethod
def from_dict(cls, data): # 类方法(工厂)
return cls(data["name"])
# 1) 调用实例方法(需要先有实例)
u1 = User("Alice")
u1.rename("Alicia")
print(u1.name) # Alicia
# 2) 调用静态方法(可由类或实例调用)
print(User.is_valid_name("Tom")) # True
print(u1.is_valid_name("")) # False
# 3) 调用类方法(通常由类调用,用于构造实例)
u2 = User.from_dict({"name": "Bob"})
print(u2.name) # Bob
6. Python 内存管理
Python 内存管理由 引用计数 主导,配合 分代垃圾回收 处理循环引用,并借助 Pymalloc 与对象池优化分配性能。
1. 第一道防线:引用计数
- 核心机制:每个对象维护引用计数。
- 计数增加:赋值、参数传递、放入容器(
list/dict)等场景。 - 计数减少:
del、变量重绑定、离开作用域。 - 立即回收:引用计数降为
0时,对象可被及时释放。
一句话简述:两个或多个对象通过属性互相引用,形成闭合引用环,导致它们的引用计数永远无法降为 0。
1. 典型代码示例
import gc
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
def __del__(self):
print(f"内存释放: {self.name} 已被销毁")
# 1. 创建两个对象
parent = Node("父亲")
child = Node("儿子")
# 2. 建立循环引用
parent.children.append(child) # 父亲 引用 儿子
child.parent = parent # 儿子 引用 父亲
# 3. 删除外部变量引用
print("删除外部变量 parent 和 child...")
del parent
del child
print("外部变量已删除,但销毁消息并未立即出现!")
2. 发生了什么?
- 初始状态:
parent和child的引用计数都为 1(变量名引用)。 - 互相绑定后:
parent的引用计数变为 2(变量名parent+child.parent)。child的引用计数变为 2(变量名child+parent.children)。
- 执行
del后:你删除了变量名,引用计数各减 1,但依然剩 1。 - 尴尬局面:引用计数不为 0,基础回收机制不会销毁;对象已与外界失联,变成内存“孤岛”。
3. Python 如何解决?
虽然引用计数失效了,但 Python 还有垃圾回收器(GC)。GC 会定期扫描对象图,识别这些“外部不可达但内部互相引用”的孤立环,并强制回收它们。
4. 两种处理方案
方案 A:手动干预(不推荐常态使用)
import gc
gc.collect() # 强制触发回收,此时可能打印:内存释放: 父亲 已被销毁 ...
方案 B:弱引用 weakref(最佳实践)
让子节点对父节点使用弱引用。弱引用不会增加引用计数,这样当外部引用删除后,对象更容易及时释放,从根源上降低循环引用风险。
2. 第二道防线:分代垃圾回收
为处理循环引用,GC 会定期扫描对象图,识别这些“外部不可达但内部互相引用”的孤立环,并强制回收它们。
2.1 先看对象范围:只重点追踪“容器对象”
- 容器对象(如
list、dict、set、部分tuple、自定义类实例)可能持有其他对象引用,才可能形成引用环。 - 非容器对象(如
int、float、str)通常不参与引用环,GC 不会把主要精力放在它们上面。 - 这样可以显著缩小扫描范围,减少回收开销。
2.2 代际划分与晋升机制
- 第 0 代:新对象先进入 0 代,回收最频繁。
- 第 1 代:在 0 代回收后仍存活的对象,会晋升到 1 代。
- 第 2 代:在 1 代回收后仍存活的对象,再晋升到 2 代。
对象“越老”越稳定,因此高代扫描频率更低,减少不必要的全量扫描。
2.3 每次回收内部做什么:
- 标记阶段:从根对象出发,沿引用链标记“可达对象”。
- 清除阶段:未被标记的对象被判定为不可达垃圾,进行回收。
- 关键点:循环引用本身不是问题,“与根对象断开连接”才是垃圾判定依据。
2.4 触发条件(阈值)与代际联动
- GC 维护每代计数与阈值。
- 当低代达到阈值时优先触发低代回收;低代回收累计到一定次数后,会触发更高代回收。
- 扫描高代时会连带扫描低代,因为对象可能存在跨代引用,必须保证可达性分析链条完整。
2.5 常用调优/排查接口
gc.collect(0/1/2):手动触发指定代回收。gc.get_threshold()/gc.set_threshold(...):查看/调整阈值。gc.get_count():查看当前代计数。gc.garbage:查看难以自动回收的对象(排障时有用)。
3. 内存分配器:Pymalloc
先向操作系统批量拿内存,再在进程内部按小块快速分发给对象。
3.1 为什么需要它
- 系统调用昂贵:如果每个小对象都直接走 OS 的
malloc,会产生频繁用户态/内核态切换,成本很高。 - 小对象极多:Python 运行时会创建大量微小对象(数字、短字符串、临时容器等)。
- 碎片问题:大量零散小块会让通用分配器账本复杂,降低长期分配效率。
3.2 三层结构(大仓库 → 货架 → 零件盒)
- Arena(256KB):Pymalloc 向 OS 申请的大块内存单位。
- Pool(4KB):Arena 切分后的中层管理单位,通常对应一个内存页。
- Block(固定尺寸):真正存放对象的小块,通常按 8 字节步长划分(8、16、24 ... 到 512)。
3.3 为什么 512B 是分界线
- ≤ 512B:由 Pymalloc 处理,走内部快路径。
- > 512B:回退到系统分配器(如
malloc)。
4. 对象池与驻留
- 小整数池:常见小整数(如
-5~256)会被缓存复用。 - 字符串驻留:部分短字符串/标识符可共享存储,减少重复分配。
5. 总结
- 引用计数:主机制,处理绝大部分回收。
- 分代 GC:专治循环引用。
- Pymalloc:优化小对象分配效率。
7. 浅拷贝 vs 深拷贝
浅拷贝与深拷贝的本质区别是:你复制的是“外层容器本身”,还是“连同内部子对象一起递归复制”。这在嵌套 list/dict 场景中非常关键。
1. 概念核心区别
浅拷贝(Shallow Copy)
- 定义:创建新对象,但内部子对象仍引用原对象中的同一批子对象。
- 影响:若子对象可变,修改子对象会“联动”原件与副本。
- 常见方式:
list()、dict.copy()、切片[:]、copy.copy()。
深拷贝(Deep Copy)
- 定义:递归复制所有层级,生成完全独立的新对象树。
- 影响:修改副本任意层级都不会影响原对象。
- 标准方式:
copy.deepcopy()。
3. 为什么不总用 deepcopy?
- 性能成本高:递归遍历整个对象图,耗时与内存开销明显。
- 资源对象不适合:文件句柄、socket、单例对象通常不应深拷贝。
4. 结论
- 直接赋值(
a = b):不创建新对象,仅增加引用。 - 浅拷贝:顶层独立,嵌套可变子对象共享。
- 深拷贝:顶层与嵌套层都独立。
int/str/tuple),copy 与 deepcopy 常返回原对象本身,这是正常优化行为 。核心原因:原件和副本都不可变,逻辑上等价,让它们指向同一块内存地址(返回原对象)是安全且高效的优化。
元组的特殊情况
元组本身不可变,但只有当其内部元素也都不可变时,deepcopy 才可能直接返回自身。
全不可变元组:
import copy
a = (1, 2)
b = copy.deepcopy(a)
print(a is b) # True(返回原对象)
包含可变元素的元组:
import copy
a = (1, [2, 3])
b = copy.deepcopy(a)
print(a is b) # False(需要新副本)
延伸:字符串驻留
Python 对部分不可变对象会做对象复用优化(如短字符串、小整数等)。因此即使不是通过 copy 得到,也可能指向同一对象:
s1 = "hello"
s2 = "hello"
print(s1 is s2) # 常见为 True
这属于解释器层面的对象复用策略,目的是减少重复对象创建,提升性能并节省内存。
8. 装饰器、生成器、上下文管理器
这三个概念是 Python 后端进阶高频:装饰器解决能力复用,生成器解决大数据量内存压力,上下文管理器解决资源安全释放。
1. 装饰器(Decorator)
- 本质:接收函数并返回新函数的高阶函数,
@decorator只是func = decorator(func)的语法糖。 - 实现关键:使用
functools.wraps保留原函数元信息(函数名、注释、签名)。 - 生产场景:鉴权、日志埋点、缓存、限流、重试、权限校验。
from functools import wraps
def log(prefix):
# 第1层:接收装饰器参数
def decorator(func):
# 第2层:接收被装饰函数
@wraps(func)
def wrapper(*args, **kwargs):
# 第3层:真正执行函数逻辑
print(f"{prefix} -> 函数开始执行: {func.__name__}")
result = func(*args, **kwargs)
print(f"{prefix} -> 函数执行结束: {func.__name__}")
return result
return wrapper
return decorator
@log("DEBUG")
def add(a, b):
"""返回两数之和"""
return a + b
@log("DEBUG") → log("DEBUG") 返回 decorator → decorator(add) 返回 wrapper。本质上就是 add = log("DEBUG")(add)。其中 prefix 是装饰器参数,*args, **kwargs 是被装饰函数参数。@wraps(func) 的核心作用:把原函数元信息复制到 wrapper 上,避免装饰后“函数身份丢失”。其中:-
__name__:函数名-
__doc__:文档字符串-
__module__:函数所属模块(文件)名。-
__annotations__:类型注解信息(参数/返回值类型)__wrapped__:其本质是 wrapper.__wrapped__ = func,即“装饰后的函数对象上保留一个指向原函数的引用”。最直观理解:
@decorator 修饰后本质是 test = decorator(test),外部拿到的是包装函数。但有了
__wrapped__,你仍可通过 test.__wrapped__ 拿到被装饰前的原函数。2. 生成器(Generator)与迭代器(Iterator)
- 关系:生成器是特殊的迭代器(Generator ⊂ Iterator)。
yield的作用:返回值给外部并暂停函数,保留现场,下一次next()从暂停处继续执行。- 核心价值:惰性求值,按需产出,显著节省内存。
- 生产场景:大文件逐行处理、分页拉取、流式响应、消息消费。
def read_big_file(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
3. 上下文管理器(Context Manager)
- with 协议:进入时调用
__enter__(),退出时无论是否异常都会调用__exit__()。 - 核心价值:等价于更安全、可复用的
try/finally,确保资源闭环。 - 生产场景:文件句柄、数据库连接/事务、锁、临时资源管理。
class MyResource:
def __enter__(self):
print("acquire resource")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("release resource")
return False
4. 总结
- 装饰器:不改原函数,统一增强能力。
- 生成器:
yield+ 惰性迭代,处理大数据更稳。 - 上下文管理器:
with保证资源正确释放。
串联:接口鉴权可用装饰器;大报表导出可用生成器流式输出;DB 连接与事务可用上下文管理器兜底。
9. 设计模式
1. 装饰器模式
- 定义:在不修改原函数代码的前提下,动态增强能力。
- 核心价值:把日志、鉴权、耗时统计等横切逻辑与业务逻辑解耦
2. 工厂模式
- 定义:把对象创建过程集中管理,调用方只关心“要什么对象”,不关心“怎么创建”。
- Python 常见写法:工厂函数 +
match-case。
class JsonParser:
def parse(self, text):
return f"json parsed: {text}"
class XmlParser:
def parse(self, text):
return f"xml parsed: {text}"
def parser_factory(parser_type):
match parser_type:
case "json":
return JsonParser()
case "xml":
return XmlParser()
case _:
raise ValueError("unsupported parser type")
parser = parser_factory("json")
print(parser.parse('{"a":1}'))
3. 策略模式
- 定义:将一组可互换算法封装起来,运行时自由切换。
- Python 优势:函数是一等公民,策略可直接传函数。
def normal_discount(price):
return price
def vip_discount(price):
return price * 0.8
def execute_checkout(price, strategy):
return strategy(price)
print(execute_checkout(100, normal_discount)) # 100
print(execute_checkout(100, vip_discount)) # 80.0
4. 单例模式
- 定义:确保全局只有一个实例。
- Pythonic 实现:优先使用模块级单例(推荐);也可用
__new__控制实例化。
# 推荐:模块级单例(最常用、最 Pythonic)
# config.py
class Config:
def __init__(self):
# 全局配置初始化一次
self.env = "prod"
self.timeout = 30
# 模块级全局对象:天然单例
config = Config()
# main.py / other.py 中都这样用:
# from config import config
# print(config.env)
# 多处 import 共享同一个 config 对象
总结
| 模式 | 核心思想 |
|---|---|
| 装饰器模式 | 不改原函数,动态增强能力 |
| 工厂模式 | 统一对象创建,解耦创建与使用 |
| 策略模式 | 算法可替换,运行时自由切换 |
| 单例模式 | 保证全局唯一实例 |
10. LEGB
1. LEGB 规则是什么?
2.1 四层命名空间
- L - Local:当前函数调用帧里的局部变量表,查找最快。
- E - Enclosing:外层函数帧中被闭包捕获的变量。
- G - Global:当前模块对象的
__dict__。 - B - Built-in:内建命名空间(通常来自
builtins模块),如len、str。
2.2 解释器视角:
- 先近后远:优先当前栈帧,避免跨作用域查找带来的额外开销。
- 闭包可用:函数定义时记录自由变量,运行时可从 enclosing cells 回溯取值。
- 模块级兜底:函数内部若没有,就查模块全局字典。
- 最后 builtins:再找不到才去内建命名空间;仍找不到抛
NameError。
2.3 结合代码看命中路径
x = "G" # Global
len = "shadowed" # 故意遮蔽 builtins.len(仅演示,不推荐)
def outer():
x = "E" # Enclosing
def inner(flag):
x = "L" # Local
if flag == 1:
return x # 命中 Local
if flag == 2:
return y # Local 无 y -> Enclosing 命中 y
if flag == 3:
return z # Local/Enclosing 无 z -> Global 命中 z
return abs(-3) # 前三层无 abs -> Built-in 命中 abs
y = "E-y"
return inner
z = "G-z"
f = outer()
print(f(1)) # L
print(f(2)) # E-y
print(f(3)) # G-z
print(f(4)) # 3
2.4 作用域可视化(SVG)
2.5 常见易错点(面试加分)
- 赋值即局部:在函数体内对变量赋值,会让该名字默认变为 Local;若读取发生在赋值前,常见
UnboundLocalError。 global:显式声明后,函数内赋值会写入模块全局命名空间。nonlocal:显式声明后,函数内赋值会写入最近一层 Enclosing 作用域。
11. MRO
MRO 是 Python 在多继承下的方法查找顺序 。Python 3 采用 C3 线性化算法,用于稳定解决菱形继承 。
什么是多继承?
多继承是指:一个子类可以同时继承多个父类,从多个父类中复用属性和方法。
- 单继承:
class B(A),只有一个父类。 - 多继承:
class D(B, C),同时有多个父类。 - 优势:可组合不同父类能力,减少重复代码。
- 挑战:若多个父类有同名方法,会出现“到底调用谁”的歧义。
- Python 解决方式:通过 MRO(C3 线性化)给出唯一、可预测的查找顺序。
什么是菱形问题?
菱形问题是多继承中的经典冲突:子类通过两条路径继承到同一个祖先类,形成“菱形结构”。
- 典型结构:
A在最上层,B(A)与C(A)在中间,D(B, C)在底部。 - 冲突点 1:若
B和C都重写了同名方法,D调用时可能产生歧义。 - 冲突点 2:若调用链设计不当,祖先
A的方法可能被重复调用或漏调。 - Python 的解法:使用 C3 线性化生成唯一 MRO,并配合协作式
super(),保证调用顺序一致且可预测。
1. 为什么“到底调用哪个父类”会不确定?
- 在多继承里,子类可能同时从多个父类得到同名方法。
- 若没有统一规则,解释器可能出现多条可行路径,行为会变得不可预测。
- MRO 的作用:为每个类预先计算一条唯一线性顺序(
__mro__),方法查找严格按这条顺序进行,命中即停止。
class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
这意味着:D 查找方法时顺序固定为 D → B → C → A → object,不会再“摇摆”。
2. C3 线性化如何生成这个唯一顺序
C3 的核心是“合并多个有序列表”,并在每一步选择一个合法头元素,直到列表耗尽:
- 输入:父类各自的 MRO + 父类声明列表(如
[B, C])。 - 选择规则:选某列表头元素
X时,X不能出现在其他列表的尾部(tail)里。 - 输出保证:
- 局部优先级顺序:尽量保持
class D(B, C)中B在C前。 - 单调性:父类已有顺序在子类中不会被打乱。
- 无歧义:得到唯一可预测线性序列;若无法满足则类创建直接报错。
- 局部优先级顺序:尽量保持
3. 菱形继承下的 C3 推导
class A:
def f(self):
print("A")
class B(A):
def f(self):
print("B")
super().f()
class C(A):
def f(self):
print("C")
super().f()
class D(B, C):
def f(self):
print("D")
super().f()
print(D.__mro__) # D, B, C, A, object
D().f() # 输出: D B C A
关键机制:super() 不是“跳到某个固定父类”,而是“沿当前实例所属类的 MRO,调用下一个类的方法”。因此:
4. 可视化:
super(),并保持每层方法签名兼容;这样 MRO 才能形成稳定、可组合的调用链。12. 描述符
一句话定义:Python 描述符是“实现了描述符协议方法的对象”。当它被放在类属性位置时,能接管该属性的读取/写入/删除过程。
1. 描述符协议到底是什么
__get__(self, instance, owner):读取属性时触发。__set__(self, instance, value):写入属性时触发。__delete__(self, instance):删除属性时触发。
只要实现上面任意一个方法,就是描述符对象;但它通常要作为“类属性”出现,协议才会在属性访问时被自动触发。
2. __get__(self, instance, owner) 参数详解(重点)
最好记忆法:owner 看“定义归属”,instance 看“谁在调用”。
owner:描述符所属类(定义它的类)。instance:发起本次属性访问的实例对象;如果通过类访问,则为None。
class Descriptor:
def __get__(self, instance, owner):
print(f"instance: {instance}")
print(f"owner: {owner}")
return "value"
class Person:
name = Descriptor() # 描述符作为类属性
p = Person()
print(p.name) # __get__(instance=p, owner=Person)
print(Person.name) # __get__(instance=None, owner=Person)
3. 两种调用场景
A. 通过实例访问:p.name
instance = powner = Person- 最常用:用于读写这个具体对象的数据(常结合
instance.__dict__)。
B. 通过类访问:Person.name
instance = Noneowner = Person- 常见写法是返回描述符对象自身,方便类级访问和调试。
class Field:
def __init__(self, key):
self.key = key
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.key)
def __set__(self, instance, value):
instance.__dict__[self.key] = value
class User:
age = Field("age")
u = User()
u.age = 18
print(u.age) # 18
print(User.age) # <__main__.Field object at ...>
5. 数据描述符 vs 非数据描述符(优先级)
__dict__ 是什么:实例对象自己的属性,默认都存在 实例.__dict__ 里。
class User:
pass
u = User()
u.name = "Tom"
print(u.__dict__) # {'name': 'Tom'}
当你访问 u.x 时,解释器要决定“先看描述符,还是先看 u.__dict__”,这就涉及优先级:
- 数据描述符(实现了
__set__或__delete__:优先级高于实例__dict__。 - 非数据描述符(只有
__get__):优先级低于实例__dict__。
5.2 最小示例
class NonDataDesc:
def __get__(self, instance, owner):
return "from non-data descriptor"
class A:
x = NonDataDesc()
a = A()
a.__dict__["x"] = "from instance dict"
print(a.x) # from instance dict(实例 __dict__ 覆盖了非数据描述符)
13. 元类
元类(Metaclass)就是“创建类的类”。如果把“对象”比作车,“类”就是车的图纸;那“元类”就是生产图纸的工厂设备。
1. 核心逻辑:类也是对象
- 在 Python 里,一切皆对象,类(class)本身也是对象。
- 既然类是对象,就一定是由某个“创建者”创建出来的。
- 默认这个创建者就是
type,也就是 Python 默认元类。
class MyClass:
pass
print(type(MyClass)) # <class 'type'>
2. 为什么要用元类
元类最大的价值是:拦截类的创建过程。在类对象正式生成前,可以做校验、改写和自动注入。
- 校验规范:如强制类名、方法名、字段格式。
- 自动注入:给类加默认属性、注册信息、辅助方法。
- 统一管控:框架层对大量子类进行一致化约束。
3. 示例:强制类名必须大写开头
# 1) 定义元类(继承 type)
class UpperCaseMeta(type):
# __new__ 在“类对象创建之前”执行
def __new__(cls, name, bases, attrs):
# name: 类名
# bases: 父类元组
# attrs: 类体中的属性字典
if not name[0].isupper():
raise TypeError(f"类名 '{name}' 必须以大写字母开头!")
print(f"正在创建合规的类: {name}")
return super().__new__(cls, name, bases, attrs)
# 2) 使用元类创建类
class User(metaclass=UpperCaseMeta):
pass # ✅ 正常
# 3) 尝试创建违规类
try:
class student(metaclass=UpperCaseMeta):
pass
except TypeError as e:
print(f"拦截成功: {e}") # ❌ 报错
14. __slots__
- 限制实例可用属性:只能使用 __slots__ 里声明的字段。
- 节省内存:不再默认创建实例 __dict__,适合大量同结构对象。
- 减少属性拼写错误:给未声明属性赋值会直接报 AttributeError。
15. Python 的 pickle 和 json
在 Python 开发中,pickle 和 json 都能做序列化(把内存对象转成可存储/传输的格式),但定位完全不同:json 是跨语言“通用语言”,pickle 是 Python 内部“私有格式”。
1. 核心区别
- 可读性:
json是文本,可读可改;pickle是二进制,人类不可读。 - 跨语言:
json几乎所有语言都支持;pickle基本只适合 Python。 - 类型支持:
json主要支持基础类型(str/int/float/bool/list/dict/None);pickle可处理大量 Python 对象(含自定义类实例等)。 - 安全性:
json相对安全;pickle反序列化可能执行恶意构造逻辑,不能加载不可信来源数据。 - 性能:复杂 Python 对象场景下,
pickle往往更省心更快;但跨系统协作时json更稳妥。
2. 该怎么选
- 优先 JSON:Web API、前后端通信、配置文件、跨语言系统集成。
- 考虑 Pickle:纯 Python 可信边界内,且需要保存复杂对象状态(本地缓存、临时快照、多进程对象传递)。
json。只有在“纯 Python + 可信数据源 + 复杂对象状态保存”这类场景,再考虑 pickle。16. async/await + asyncio并发
import asyncio
import random
from dataclasses import dataclass
# ========== Step 1: 定义任务数据结构(模拟“入队”的任务项) ==========
@dataclass(slots=True)
class Job:
job_id: int
user_id: int
# ========== Step 2: 模拟两个异步 I/O 服务 ==========
async def fetch_user(user_id: int) -> dict:
"""模拟用户服务:异步网络请求"""
# await 时当前协程挂起,事件循环去调度其他协程
await asyncio.sleep(random.uniform(0.2, 0.6))
return {"user_id": user_id, "name": f"user-{user_id}"}
async def fetch_orders(user_id: int) -> list[dict]:
"""模拟订单服务:异步网络请求"""
await asyncio.sleep(random.uniform(0.2, 0.6))
return [{"order_id": 101, "amount": 99}, {"order_id": 102, "amount": 199}]
# ========== Step 3: 单个任务的全链路处理(创建子任务 + 并发等待) ==========
async def process_one_job(job: Job) -> dict:
# 3.1 创建并发子任务:用户请求和订单请求同时发起
user_task = asyncio.create_task(fetch_user(job.user_id), name=f"user-{job.job_id}")
orders_task = asyncio.create_task(fetch_orders(job.user_id), name=f"orders-{job.job_id}")
# 3.2 并发等待:用 gather 一次性等待多个任务
user, orders = await asyncio.gather(user_task, orders_task)
# 3.3 聚合结果
total_amount = sum(o["amount"] for o in orders)
return {
"job_id": job.job_id,
"user": user,
"orders": orders,
"total_amount": total_amount,
}
# ========== Step 4: 队列生产者(入队) ==========
async def producer(queue: asyncio.Queue[Job], total: int) -> None:
for i in range(1, total + 1):
# put 是异步入队:队列满时会自动等待,不阻塞线程
await queue.put(Job(job_id=i, user_id=i))
# ========== Step 5: 队列消费者(出队 + 执行 + 结果回收) ==========
async def consumer(
queue: asyncio.Queue[Job],
results: list[dict],
stop_event: asyncio.Event,
) -> None:
while True:
# 5.1 退出条件:收到停止信号且队列已空
if stop_event.is_set() and queue.empty():
return
try:
# 5.2 从队列取任务
# asyncio.wait_for 用来给一个可等待对象加“超时限制”
# queue.get:从队列里取出一个元素(通常是队头)
job = await asyncio.wait_for(queue.get(), timeout=0.2)
except asyncio.TimeoutError:
continue
try:
# 5.3 执行单任务全链路
result = await process_one_job(job)
results.append(result)
finally:
# 5.4 标记该任务已处理完成
queue.task_done()
# ========== Step 6: 主流程(创建任务、并发执行、等待结果) ==========
async def main() -> None:
# maxsize=100 表示队列最多缓存 100 个任务,超过后 put 会等待,避免无限堆积。
queue: asyncio.Queue[Job] = asyncio.Queue(maxsize=100)
# 结果容器:所有消费者处理完的结果统一追加到这里,主流程最后集中输出。
results: list[dict] = []
# 停止信号:主流程在 queue.join() 后 set,通知消费者“可优雅退出”。
stop_event = asyncio.Event()
# 总任务数:本次要处理多少个 Job
total_jobs = 5
# 消费者并发数:同时启动 3 个消费者协程并行处理队列任务。
consumer_count = 3
# 6.1 使用 TaskGroup 统一管理并发任务生命周期
async with asyncio.TaskGroup() as tg:
# 6.2 创建生产者任务(负责入队)
tg.create_task(producer(queue, total_jobs), name="producer")
# 6.3 创建多个消费者任务(并行处理队列)
for idx in range(consumer_count):
tg.create_task(consumer(queue, results, stop_event), name=f"consumer-{idx+1}")
# 6.4 等待“所有入队任务”被处理完成
# queue.put(item):每放入一个任务,内部“未完成任务数” (+1)
# queue.task_done():每处理一个任务,内部“未完成任务数” (-1)
# queue.join():阻塞等待,直到未完成任务数变成 0 才返回
# queue.empty():表示“此刻队列里暂时没有元素”,但不代表消费者已经处理完(可能刚拿走还在处理)
await queue.join()
# 6.5 通知消费者:可以优雅退出了
stop_event.set()
# 6.6 TaskGroup 退出后,说明所有任务都已结束
print("全部任务处理完成,结果如下:")
for item in results:
print(item)
if __name__ == "__main__":
# Step 7: 创建事件循环并运行主协程
asyncio.run(main())
| 步骤 | 内部动作 | 目的 |
|---|---|---|
| 1. 检查 | 确认当前线程没有正在运行的循环 | 防止循环嵌套冲突 |
| 2. 初始化 | 实例化一个新的 EventLoop | 准备任务调度器 |
| 3. 注入 | 将 main() 包装成一个 Task |
让协程变为可执行单位 |
| 4. 循环 | 启动循环直到 main() 返回结果 |
维持程序运行 |
| 5. 终结 | 取消所有存活任务并关闭循环 | 优雅地释放内存 |
17. Python 类型注解
Q1:Python 类型注解是什么?
- 定义:类型注解(Type Hints)是 Python 的可选语法,用于声明变量、参数、返回值的类型。
- 核心价值:提升编辑器补全、静态检查、重构安全性与团队协作可读性。
- 关键点:注解主要服务于工具链,不会像静态语言那样默认阻止运行时执行。
Q2:类型注解会改变函数运行逻辑吗?
- 通常不会:仅添加注解,不会自动做运行时强校验。
- 但会增强开发阶段质量:编辑器能更早暴露明显问题(例如把
int拼接到字符串)。
def get_name_with_age(name: str, age: int) -> str:
# 推荐显式转换,避免隐式类型错误
return f"{name} is this old: {age}"
Q3:函数参数和返回值如何声明基础类型?
def get_items(
item_a: str,
item_b: int,
item_c: float,
item_d: bool,
item_e: bytes,
) -> tuple[str, int, float, bool, bytes]:
return item_a, item_b, item_c, item_d, item_e
Q4:什么是泛型类型?
- 泛型类型:容器类型 + 内部元素类型参数,如
list[str]。 - Python 3.13 推荐:直接使用内置泛型语法(
list[str]、dict[str, float]),避免旧式typing.List。
def process_items(
names: list[str],
prices: dict[str, float],
fixed: tuple[int, int, str],
blobs: set[bytes],
) -> None:
for name in names:
print(name.title())
for item_name, item_price in prices.items():
print(item_name, item_price)
print(fixed, blobs)
Q5:Union、Optional、| 有什么区别?
- 等价关系:
str | None等价于Union[str, None],也等价于Optional[str]。 - Python 3.13 推荐:优先使用
|语法,最简洁直观。 - 语义提醒:可为
None不等于参数可省略;是否可省略由是否有默认值决定。
def print_item(item: int | str) -> None:
print(item)
def say_hi(name: str | None = None) -> None:
if name is None:
print("Hello World")
else:
print(f"Hey {name}!")
Q6:为什么“可为 None”和“可选参数”经常被混淆?
name: str | None:表示值类型允许None。- 只有写了默认值(如
= None)才是调用时可不传。
def required_but_nullable(name: str | None) -> None:
# 调用时必须传参,但可以传 None
print(name)
required_but_nullable(None) # 合法
# required_but_nullable() # 不合法:缺少参数
Q7:类本身可以作为类型吗?
- 可以:注解为某个类,表示参数是该类实例。
class Person:
def __init__(self, name: str) -> None:
self.name = name
def get_person_name(one_person: Person) -> str:
return one_person.name
Q8:Pydantic 模型与类型注解是什么关系?
- Pydantic 基于类型注解定义数据结构、校验与转换规则。
- FastAPI 基于 Pydantic + 类型注解实现请求体校验、错误响应与文档生成。
- Python 3.13 实践建议:优先不可变默认值;可变字段用
Field(default_factory=...)。
from datetime import datetime
from pydantic import BaseModel, Field
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
# 在 Python 的底层机制中,如果你在类定义时直接写 [],这个列表在内存中只会被创建一次。
# default_factory=list 的意思是:每当创建一个新对象时,请调用一次 list() 函数来生成一个新的、干净的列表。
# 防止掉入“共享变量”的陷阱。
friends: list[int] = Field(default_factory=list)
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
# 字典解包 ** 运算符会将字典中的键值对拆开,作为关键字参数传递给构造函数。
user = User(**external_data)
print(user.id) # 123
print(user.friends) # [1, 2, 3]
Q9:Annotated 是什么?在 FastAPI 里有什么价值?
- 定义:
Annotated[T, ...meta],第一个参数是“真实类型”,后面是附加元数据。 - Python 本身:不强制处理元数据。
- FastAPI:会读取这些元数据驱动参数校验、文档描述等行为。
from typing import Annotated
def say_hello(name: Annotated[str, "display name"]) -> str:
return f"Hello {name}"
Q10:FastAPI 到底如何利用类型注解?
| 能力 | 类型注解带来的结果 |
|---|---|
| 编辑体验 | 自动补全、签名提示、重构更稳 |
| 请求数据校验 | 自动解析并验证参数与请求体 |
| 错误反馈 | 输入不合法时返回结构化错误 |
| OpenAPI 文档 | 自动生成接口文档和交互式页面 |
| 联合语法;2)优先内置泛型 list[str];3)函数必须写返回类型;4)模型中避免可变默认值;5)需要额外语义时优先 Annotated。