Python 高频面试题

1. Python 是什么?
解释型 动态语义 面向对象 高层次 胶水语言

解释型语言编译型语言的区别在于:源代码是“边翻译边执行”,还是“翻译好了再执行”。

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. 语法与编程范式

  • 简洁易读:强制缩进定义代码块,提升可读性并降低维护成本。
  • 多范式支持:支持面向对象与结构化编程,并融合函数式(lambdamapfilteritertools)与元编程。
  • 动态类型与内存管理:动态类型 + 延迟绑定;引用计数与增量 GC(3.14 优化)自动管理内存。

B. 性能与底层优化

  • 实验性 JIT:3.13 引入、3.14 持续优化,提升部分计算密集型任务性能。
  • 自由线程模式:逐步支持关闭 GIL,实现多核并行。
2. list 和 tuple 的区别?

1. 核心区别对比

  • 可变性list 可变 tuple 不可变。
  • 语法list 使用 []tuple 使用 ()
  • 内存占用list 通常更大(为扩容预留空间);tuple 更紧凑。
  • 哈希性list 不可哈希,不能做字典键;tuple 在元素可哈希时可作为键

总结:元组想作为字典键,里面的每个元素都必须是不可变(可哈希)的。

1. 什么是可哈希(Hashable)?

  • 不可变:对象生命周期内值不会变化(如 intstrfloatbool)。
  • 可计算哈希:支持 __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)

list(动态数组,支持扩容) PyListObject 头部 长度、容量、指针等元信息 ob_item 指针 elem0* elem1* elem2* 预留 预留 tuple(定长、紧凑、不可变) PyTupleObject 头部 e0* e1* e2*

3. 场景适配?

使用 List 的场景

  • 长度不固定:需要频繁增删元素。
  • 原地修改:需要 .sort().reverse() 等操作。

4. 列表中所有元素类型必须相同吗?

不必须。Python 的 list可以同时装入不同类型的对象。

4.1 为什么 Python 允许异构列表?

  • 万物皆对象:Python 中每个值本质都是对象。
  • list 存的是引用:CPython 的列表底层可理解为“对象指针数组”(保存对象地址)。
  • 间接访问:列表不关心元素具体类型,只保存地址;取值时再根据地址找到真实对象并按其类型处理。

这与 C/Java 传统数组不同:后者通常强调同类型、固定布局,便于按偏移量快速定位。

4.2 混合类型的代价

  • 逻辑风险:循环里做统一运算(如 item + 1)时,若混入字符串会触发 TypeError
  • 性能损耗:类型不统一会降低解释器优化空间,不利于向量化与批量数值计算。

4.3 什么时候应该保持类型统一?

  • 数值计算:建议使用 array.arrayNumPy(同构类型,计算效率高)。
  • 结构化异构数据:如“姓名、年龄、体重”,更推荐 dictdataclass,语义更清晰。

使用 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 对小整数和部分常量做缓存复用(例如小整数对象池),可能导致两个变量指向同一对象,从而 isTrue。因此不能把这类现象当作通用规律。

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 的区别

  • 共同点:本质都能创建函数对象,都可被调用、传参、返回。
  • 差异 1lambda 是匿名函数;def 是具名函数。
  • 差异 2lambda 只能写单个表达式;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. 发生了什么?

  • 初始状态parentchild 的引用计数都为 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 先看对象范围:只重点追踪“容器对象”

  • 容器对象(如 listdictset、部分 tuple、自定义类实例)可能持有其他对象引用,才可能形成引用环。
  • 非容器对象(如 intfloatstr)通常不参与引用环,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:查看难以自动回收的对象(排障时有用)。
新容器对象创建 进入第 0 代(Young) 检查第 0 代计数是否超过阈值 未超阈值:继续分配;超阈值:触发 gc.collect(0) 第 0 代回收:标记-清除(可达性分析) 从根对象出发标记可达对象 未标记对象(含孤立循环引用)被清除 幸存对象晋升到第 1 代 0 代回收执行到一定次数后,触发 1 代回收 第 1 代回收(通常连带检查第 0 代) 考虑跨代引用,保证可达性链条完整 幸存对象继续晋升到第 2 代 第 2 代回收(全代参与) 触发频率最低、成本最高,但最全面 存活对象继续留在老年代,等待后续周期检查 循环往复:分代回收持续运行,平衡吞吐与停顿

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)。
一句话:Pymalloc 让 Python 不必为每个“小额内存请求”都惊动操作系统,从而显著提升小对象分配与回收效率。

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),copydeepcopy 常返回原对象本身,这是正常优化行为

核心原因:原件和副本都不可变,逻辑上等价,让它们指向同一块内存地址(返回原对象)是安全且高效的优化。

元组的特殊情况

元组本身不可变,但只有当其内部元素也都不可变时,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") 返回 decoratordecorator(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 模块),如 lenstr

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)

LEGB 名称解析顺序:L → E → G → B(命中即停止) L: Local inner() 当前帧 x = "L" E: Enclosing outer() 闭包环境 y = "E-y" G: Global module.__dict__ z = "G-z" B: Built-in builtins.__dict__ abs, str, len ... inner(flag) 查找演示 flag=1: return x -> L 命中 flag=2: return y -> L miss, E 命中 flag=3: return z -> L/E miss, G 命中 flag=4: return abs(-3) -> L/E/G miss, B 命中 如果四层都 miss:抛 NameError

2.5 常见易错点(面试加分)

  • 赋值即局部:在函数体内对变量赋值,会让该名字默认变为 Local;若读取发生在赋值前,常见 UnboundLocalError
  • global:显式声明后,函数内赋值会写入模块全局命名空间。
  • nonlocal:显式声明后,函数内赋值会写入最近一层 Enclosing 作用域。
面试:LEGB 本质是“运行时按作用域链解析名字”的机制;闭包能“记住外层变量”,是因为解释器把外层自由变量保存在 cell 中并在 E 层回溯命中。
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:若 BC 都重写了同名方法,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)BC 前。
    • 单调性:父类已有顺序在子类中不会被打乱。
    • 无歧义:得到唯一可预测线性序列;若无法满足则类创建直接报错。

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. 可视化:

菱形继承结构(左)与最终 MRO 线性顺序(右) A B(A) C(A) D(B, C) C3 结果(D 的 MRO) D → B → C → A → object 方法查找与 super() 链路都沿此顺序推进 调用链(D().f) 1) D.f 执行后 super() → B.f 2) B.f 中 super() → C.f 3) C.f 中 super() → A.f 4) A 只执行一次,避免重复调用
工程建议:在多继承体系里始终使用协作式 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 = p
  • owner = Person
  • 最常用:用于读写这个具体对象的数据(常结合 instance.__dict__)。

B. 通过类访问:Person.name

  • instance = None
  • owner = 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}")  # ❌ 报错
元类是深奥的魔法,99.9% 的用户都不需要它。如果你在考虑是否需要元类,那么你通常不需要。
14. __slots__
  • 限制实例可用属性:只能使用 __slots__ 里声明的字段。
  • 节省内存:不再默认创建实例 __dict__,适合大量同结构对象。
  • 减少属性拼写错误:给未声明属性赋值会直接报 AttributeError。
15. Python 的 picklejson

在 Python 开发中,picklejson 都能做序列化(把内存对象转成可存储/传输的格式),但定位完全不同: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())
async/await + asyncio 全链路并发流程(单进程/单线程,多协程并发) 关键语法全景:asyncio.run / TaskGroup / create_task / Queue / wait_for / gather / join / task_done / Event 入口:asyncio.run(main()) 创建并运行 EventLoop,驱动 main();main 结束后统一清理剩余任务并关闭循环 main() 初始化关键对象 queue = asyncio.Queue(maxsize=100);results = [];stop_event = asyncio.Event() maxsize 提供背压;Event 用于消费者优雅退出 并发编排:async with asyncio.TaskGroup() as tg tg.create_task(producer(...), name="producer") for idx in range(consumer_count): tg.create_task(consumer(...), name=f"consumer-{idx+1}") Producer(入队) for i in range(1, total + 1): await queue.put(Job(job_id=i, user_id=i)) 队列满时 put 会 await 挂起,事件循环转去调度其他协程(背压生效) Consumer(出队 + 处理 + 回收) while True: if stop_event.is_set() and queue.empty(): return try: job = await asyncio.wait_for(queue.get(), timeout=0.2) except asyncio.TimeoutError: continue try: result = await process_one_job(job); results.append(result) finally: queue.task_done() task_done 与 join 成对出现;缺失会导致 join 永久等待 process_one_job(job):单任务内并发子请求 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}") user, orders = await asyncio.gather(user_task, orders_task) I/O 协程(模拟) async def fetch_user(...): await asyncio.sleep(...) async def fetch_orders(...): await asyncio.sleep(...) 遇到 await 会让出执行权,实现协程级并发(非多线程并行) 主流程收敛与退出 await queue.join() # 等未完成任务计数归零 stop_event.set() → 消费者命中 is_set()+queue.empty() 后退出;TaskGroup 统一等待所有任务结束
步骤 内部动作 目的
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 文档 自动生成接口文档和交互式页面
Python 3.13 类型注解实践清单:1)优先使用 | 联合语法;2)优先内置泛型 list[str];3)函数必须写返回类型;4)模型中避免可变默认值;5)需要额外语义时优先 Annotated