这份笔记共整理 90 道 Python 面试题,按“语言基础 → 容器与算法 → 函数与协议 → 面向对象 → 运行时与工程 → 并发异步 → 项目场景”逐层展开。适合 Python 开发、测试开发和自动化测试岗位复习。
面试热度说明: 星级表示结合常见面试考点与岗位实践作出的经验判断,不是招聘平台统计或实时流行度排名。面试官关注点会随业务、职级和技术栈变化。
| 标记 | 经验热度 | 复习目标 |
|---|---|---|
| ★★★★★ | 高频核心 | 能准确解释,能现场写出基本示例 |
| ★★★★☆ | 常见追问 | 能说明原理、边界和设计取舍 |
| ★★★☆☆ | 进阶区分项 | 能结合项目解释,避免只背术语 |
| ★★☆☆☆ | 专项深入 | 根据岗位和实际经历补充 |
答案使用方法: 先用一两句话给出结论,再解释原因,最后用代码或项目例子说明边界。不要把 CPython 的实现细节说成所有 Python 实现的语言保证。
版本约定: 基础示例尽量使用稳定的 Python 3 语法;TaskGroup、asyncio.timeout、ExceptionGroup 等特性标注为 Python 3.11+。自由线程、进程启动方式和注解等有版本变化的内容单独说明。独立代码块可单独执行;代码中的断言用于演示,不表示生产环境应依赖 assert 校验不可信输入。
一、入门:类型、变量和表达式(1—12)
1. Python 是动态类型还是弱类型?“解释型”意味着不编译吗?
热度:★★★★★|难度:入门
参考答案: Python 是动态类型语言,变量名在运行时绑定对象,对象本身具有类型;一般也称为强类型语言,不会无条件把不兼容的对象自动转换成可运算类型。例如字符串和整数直接相加会抛出 TypeError。
“动态”与“弱类型”是不同维度。变量名可以先绑定整数、再绑定字符串,不代表整数对象自身变成字符串。
追问: CPython 通常先把源码编译成代码对象和字节码,再由解释器执行。不能把“解释型”理解为完全没有编译步骤,也不能据此断言所有 Python 实现的执行方式完全相同。
2. Python 的变量与赋值是怎样工作的?函数参数是值传递还是引用传递?
热度:★★★★★|难度:基础
参考答案: 赋值建立名称与对象的绑定,不会默认复制对象。函数调用时形参绑定到传入的对象,常称为对象共享或传对象引用;用“值传递/引用传递”二选一容易遗漏重绑定与修改对象的区别。
|
|
加分点: 函数能否影响调用方,关键是是否修改共享对象,而不是“参数是不是局部变量”。需要隔离时由接口明确约定复制和所有权。
3. 可变与不可变对象有什么区别?元组里能放列表吗?
热度:★★★★★|难度:基础
参考答案: 列表、字典、集合通常可变;整数、字符串、元组等不可变。不可变指对象自身的结构或值不能原地修改,不代表它引用的所有对象都递归不可变。
|
|
元组可以引用列表,不能替换元组中的引用位置,但可以修改那个列表。这样的元组也不能用作字典键,因为它包含不可哈希成员。
易错点: 不可变、可哈希和线程安全不是同义词,要分别判断。
4. is 与 == 有什么区别?为什么不能用 is 比较字符串?
热度:★★★★★|难度:基础
参考答案: is 比较对象身份,== 比较相等关系,后者可由类型自定义。判断单例 None 通常使用 is None;比较业务值使用 ==。
|
|
CPython 的整数缓存、字符串驻留等优化可能让两个相等值碰巧共享身份,但不是一般业务逻辑可依赖的规则。对象存活期间身份稳定;对象销毁后,后续对象可能复用其 id,因此不能把 id() 当永久业务标识。
5. 哪些值是假?and、or 一定返回布尔值吗?
热度:★★★★★|难度:基础
参考答案: None、False、数值零、空容器等通常为假。自定义对象可以通过 __bool__ 或 __len__ 定义真值。and、or 会短路,并返回操作数,不保证返回 bool。
|
|
项目陷阱: timeout = supplied or 30 会把合法的 0 也当成缺失。若只有 None 表示未提供,应写 30 if supplied is None else supplied。同理,不能用 if value 混淆空字符串、零和不存在。
6. /、//、% 有什么区别?负数整除如何计算?
热度:★★★★☆|难度:基础
参考答案: / 是真除法;对于整数,// 向负无穷方向取整,而不是向零截断;整数余数遵循 a == (a // b) * b + a % b,除数不能为零。
|
|
非零整数余数与除数同号。若业务规则要求“向零截断”,必须明确实现,不能直接把 // 当成 C 等语言中所有整数除法行为的等价替代。
7. 如何区分“没传参数”与“显式传了 None”?
热度:★★★★☆|难度:基础
参考答案: 使用唯一哨兵对象,避免让一个值承担两种不同语义。这在配置更新、PATCH 接口和可选过滤条件中很常见。
|
|
边界: 哨兵按身份比较;如果需要跨进程序列化或在公共 API 中稳定表达缺失,应设计明确协议,不能假设普通 object() 在反序列化后仍保留身份。
8. 字符串格式化有哪些方式?f-string 有什么注意事项?
热度:★★★★☆|难度:入门
参考答案: 常见方式有 f-string、str.format 和 % 格式化。f-string 适合代码中直观地插入表达式和格式规范;大量片段拼接通常使用 join。
|
|
项目注意: 日志通常用参数化形式 logger.info("user=%s", user_id),便于推迟消息格式化,但传参表达式自身仍会先计算。f-string 不是 SQL 参数绑定工具,不应把不可信输入拼进 SQL 或命令字符串。
9. for...else 和 while...else 的 else 何时执行?
热度:★★★☆☆|难度:基础
参考答案: 循环正常结束且未被 break 提前退出时执行 else。它不表示“循环体一次都没执行”;空循环也会正常进入 else。return 和向外传播的异常也会离开整个结构。
|
|
它适合搜索、重试等需要表达“没有提前成功退出”的逻辑;如果团队不熟悉,使用显式返回也可以,优先保证可读性。
10. 列表推导式与生成器表达式有什么区别?
热度:★★★★★|难度:基础
参考答案: 列表推导式立即计算并保存结果,适合多次访问;生成器表达式按需产出,适合流式处理,但通常只能遍历一次。
|
|
生成器节省结果存储空间,不保证一定更快,也不是“完全不占内存”:它仍保存迭代状态及引用。不要用复杂嵌套推导式执行副作用;难以阅读时拆成普通循环。
11. str 与 bytes 的区别是什么?编码和解码方向是什么?
热度:★★★★★|难度:基础
参考答案: str 表示 Unicode 文本,bytes 表示字节序列。encode 将文本编码为字节,decode 将字节按编码解释为文本。
|
|
文件、网络和数据库边界应明确编码,不应默认所有字节都是 UTF-8。errors="ignore" 可能悄悄丢数据,只能在明确接受损失时使用。文本字符数量也不总等于屏幕上可见的字符簇数量,emoji 和组合字符尤其需要注意。参见Unicode 官方指南。
12. 为什么 0.1 + 0.2 != 0.3?金额怎样计算?
热度:★★★★★|难度:基础
参考答案: 常见二进制浮点数不能精确表示许多十进制小数,因此运算结果包含表示误差。科学计算可按业务容差比较;金额常使用最小货币单位整数或 Decimal,并明确精度和舍入规则。
|
|
从字符串构造 Decimal,避免先经过不精确的 float。Decimal 也受上下文精度、舍入和异常策略影响,并不意味着任意运算都自动符合财务规则。参见Decimal 文档。
二、常用容器:行为、复杂度与数据处理(13—24)
13. list、tuple、dict、set 如何选择?
热度:★★★★★|难度:基础
| 类型 | 主要特点 | 常见用途 |
|---|---|---|
| list | 有序、可变、可重复 | 顺序处理、动态结果集合 |
| tuple | 有序、不可变容器结构 | 固定结构记录、复合键候选 |
| dict | 键到值的映射,保留插入顺序 | 索引、配置、聚合 |
| set | 唯一成员集合,不提供业务顺序保证 | 去重、成员判断、集合运算 |
参考答案: 按访问方式和语义选类型,而不是按“哪个更快”机械选择。dict/set 的键或成员需要可哈希;元组只有成员都可哈希时才可哈希。
易错点: {} 是空字典,空集合要用 set()。set 的某次打印顺序稳定,不代表可以把它当有序序列。
14. 什么是可哈希?实现 __eq__ 后为什么可能不能做字典键?
热度:★★★★★|难度:进阶
参考答案: 可哈希对象需要在生命周期内保持稳定的哈希值,并满足“相等对象必须有相同哈希值”;相同哈希值不保证对象相等。
自定义类若实现值相等却没有合适的哈希策略,Python 通常将其设为不可哈希,以免键在容器中的行为不一致。不要让影响相等与哈希的字段在入字典后变化。
|
|
这里字段本身也可哈希。frozen=True 不是递归冻结,不能据此认为任意 dataclass 都可以安全当键。
15. dict 是有序的吗?OrderedDict 还有意义吗?
热度:★★★★☆|难度:基础
参考答案: Python 3.7 起,字典保留插入顺序是语言保证;这不等于自动按键排序。更新已有键通常不改变位置,删除后重新插入会出现在末尾。
|
|
OrderedDict 仍适合频繁重排位置、从特定一端弹出元素等需求,它与普通 dict 的相等比较语义也有差异。应先看所需操作,不必因为 dict 有序就认为 OrderedDict 完全无用。
16. list、dict、set 的常见操作复杂度是多少?
热度:★★★★★|难度:进阶
| 操作 | 常见复杂度 | 条件或注意事项 |
|---|---|---|
| list 按索引访问 | O(1) | 常见动态数组实现 |
| list 尾部 append | 均摊 O(1) | 扩容时单次可能为 O(n) |
| list 头部插入、删除 | O(n) | 需要移动其他元素 |
| list 成员判断 | O(n) | 逐项比较 |
| dict/set 查找、插入 | 平均 O(1) | 最坏情况可退化,哈希/比较也有成本 |
| 排序 | O(n log n) 最坏时间 | 已有顺序可能减少工作 |
参考答案: 必须说明平均、均摊和最坏情况。上表是常见 CPython 容器实现下的分析,不是所有操作在任何对象上都只花常数时间。例如长字符串的哈希或昂贵的 __eq__ 会影响实际成本。
17. 赋值、浅拷贝和深拷贝有什么区别?
热度:★★★★★|难度:基础
|
|
参考答案: 赋值共享同一对象;浅拷贝创建新外层容器,内部引用通常共享;深拷贝递归复制支持复制的子对象,并使用 memo 处理循环和共享关系。
项目边界: deepcopy 不是通用隔离方案。连接、锁、文件等资源可能不适合复制;不可变对象可能仍复用原对象。应先明确哪些状态需要独立,再决定复制或重新构造。参见copy 文档。
18. 切片和序列乘法有哪些共享引用陷阱?
热度:★★★★★|难度:基础
|
|
参考答案: 乘法重复的是元素引用,不是递归复制内部对象。items[:] 对普通 list 创建浅拷贝,仍不能隔离嵌套对象。
切片通常会复制所选引用并占用额外空间,不应把巨大列表的切片当成零成本视图;不同类型的切片行为可能不同,例如某些数组库会返回视图。回答时要注明对象类型。
19. 解包、*args 和 **kwargs 中的星号表示什么?
热度:★★★★☆|难度:基础
参考答案: 星号含义取决于上下文:赋值中收集剩余元素,调用中展开参数,函数定义中收集参数。** 用于关键字映射展开或收集。
|
|
重复传入同一个关键字会报错,不是自动覆盖。字典构造中的 {**left, **right} 则是另一种语境,右侧同名键覆盖左侧。不要把两种规则混为一谈。
20. sort 与 sorted 的区别是什么?如何多字段排序?
热度:★★★★★|难度:基础
参考答案: list.sort() 原地排序并返回 None;sorted() 接受可迭代对象并返回新列表。Python 排序稳定,相同排序键的元素保持原有相对顺序。
|
|
混合升降序可以构造合适的键,或利用稳定性分多轮排序。不要把 items = items.sort() 写成赋值,它会让 items 变成 None。排序键还应定义如何处理缺失值和类型不一致。
21. 如何去重并保持原顺序?对象不可哈希怎么办?
热度:★★★★★|难度:基础
参考答案: 可哈希值可以用 list(dict.fromkeys(values)) 保留第一次出现顺序。复杂对象可根据稳定业务键去重,同时说明保留首条还是末条。
|
|
任意转字符串作为去重键可能混淆类型或依赖不稳定表示;直接对列表做 x not in result 虽然可行,但大规模时可能退化为 O(n²)。
22. Counter、defaultdict、deque 分别适合什么场景?
热度:★★★★☆|难度:基础
|
|
参考答案: Counter 计数,defaultdict 简化分组,deque 适合双端队列,避免 list 头部删除的搬移成本。
追问: defaultdict[key] 访问缺失键会触发默认工厂,.get() 不会;deque(maxlen=n) 满时会丢弃另一端元素,不是阻塞队列。线程间通信需要同步语义时,应考虑 queue.Queue。参见collections 文档。
23. 为什么不建议遍历容器时直接增删元素?
热度:★★★★☆|难度:基础
参考答案: 修改列表可能导致索引移动、漏处理或重复处理;迭代字典时改变大小通常会抛异常。安全方法包括构造新结果、遍历快照,或在严格控制索引时原地修改。
|
|
快照会占额外空间,而且浅快照不意味着内部对象不变。并发修改问题还需要锁或所有权设计,不能只靠复制键列表解决。
24. 如何从大量数据中找 Top K?什么时候不必完整排序?
热度:★★★★☆|难度:进阶
参考答案: 当 K 远小于 n 时,可以维护大小为 K 的堆,时间约 O(n log K)、额外空间约 O(K);完整排序约 O(n log n)。若 K 很大或还要完整有序结果,排序可能更简单有效。
|
|
项目边界: 需定义并列、重复、排序方向和稳定规则。输入若已经在数据库,应考虑在数据源侧利用索引和 LIMIT,而不是先把所有记录拉进 Python。复杂度优化也要结合实际数据量和传输成本。
三、函数与协议:作用域、闭包、迭代和异常(25—36)
25. 函数参数有哪些种类?/ 和 * 的作用是什么?
热度:★★★★★|难度:基础
参考答案: 参数可以分为仅位置参数、位置或关键字参数、可变位置参数、仅关键字参数和可变关键字参数。/ 前是仅位置参数,单独 * 后或 *args 后是仅关键字参数。
|
|
这里 method 不能用关键字传,timeout 不能按位置传。合理限制传参方式可减少参数顺序错误,并为接口演进留余地;无必要时不要为了展示语法写得过度复杂。
26. 为什么默认参数不能随意写成列表或字典?
热度:★★★★★|难度:基础
参考答案: 默认值在函数定义执行时求值,不是每次调用重新创建。可变默认值可能被后续调用共享,造成跨调用污染。
|
|
不仅容器有问题,datetime.now() 等动态默认值也会只计算一次。若 None 是合法业务值,应使用独立哨兵。默认对象共享有时是刻意设计,但应显式说明,不要把它当隐形缓存。
27. LEGB 是什么?为什么会出现 UnboundLocalError?
热度:★★★★★|难度:进阶
参考答案: 常见名称查找依次考虑局部 Local、外层函数 Enclosing、模块 Global 和内置 Builtins。函数体内对某名称赋值,通常会让该名称被判断为局部变量,除非声明 global 或 nonlocal。
|
|
global 改变模块级绑定,nonlocal 修改外层函数中的绑定。只调用共享列表的 append 不属于重绑定名称,因此未必需要 nonlocal。不要把 LEGB 套用成所有类体和特殊作用域都完全相同的简化规则。
28. 什么是闭包?循环中的 lambda 为什么常返回同一个值?
热度:★★★★★|难度:进阶
参考答案: 闭包使内部函数能够访问外层函数的自由变量。常见陷阱是延迟绑定:调用时读取变量当前值,而不是自动保存定义时的每次循环值。
|
|
修复可以用默认参数绑定当前值,或使用工厂函数创建独立作用域。回调、线程任务和定时任务中同样会发生这个问题。
29. 装饰器是什么?为什么需要 functools.wraps?
热度:★★★★★|难度:进阶
参考答案: 装饰器接收可调用对象并返回替代对象。@decorator 通常等价于定义后执行 func = decorator(func),适合日志、计时、缓存等横切逻辑。
|
|
wraps 保留名称、文档等元信息,并设置 __wrapped__,便于内省。它不会让错误的同步包装器自动正确处理协程;异步函数通常需要异步 wrapper 并 await 原函数。
30. 带参数的装饰器如何实现?多个装饰器按什么顺序生效?
热度:★★★★☆|难度:进阶
参考答案: 带参数装饰器通常多一层工厂:先接收配置,再接收函数,最后在调用时执行业务包装。叠加时 @outer 在 @inner 外侧,相当于 outer(inner(func))。
|
|
项目追问: 缓存与认证、重试与事务的顺序可能改变安全和副作用语义。缓存若绕过身份校验,或重试包住不幂等事务,都可能引入严重问题;应先确定调用边界,再选择装饰顺序。
31. 生成器的执行时机是什么?yield 与 return 有什么区别?
热度:★★★★★|难度:进阶
参考答案: 含 yield 的普通函数调用后返回生成器,函数体通常在迭代推进时才开始执行。yield 暂停并产出值,后续推进从暂停处恢复;return 终止生成器。
|
|
追问: 生成器适合流式数据,但资源释放不能只依赖“以后会迭代完”。中途停止时应明确关闭或由外层上下文管理器控制资源。生成器内部也不应手动抛 StopIteration 来结束,使用 return。
32. 可迭代对象、迭代器和生成器是什么关系?
热度:★★★★★|难度:进阶
参考答案: 可迭代对象能够通过 iter() 获取迭代器;迭代器通过 next() 产生值,并在耗尽时抛 StopIteration;生成器是一种迭代器。列表可多次创建新迭代器,单个迭代器通常只能消费一次。
|
|
实际陷阱: 把迭代器先用于日志输出,再交给业务处理,业务可能得到空结果。函数接口应说明接收一般 Iterable 还是需要可重复访问的序列,避免暗中消费输入。
33. yield from、send 与异步 await 有什么联系和区别?
热度:★★★☆☆|难度:深入
参考答案: yield from 将迭代委托给子迭代器,并能传递生成器的 send、throw、close 等协议;对子生成器,还能取得其 return 值。send 可以向暂停在 yield 处的生成器传值,首次推进通常用 next 或 send(None)。
|
|
await 用于等待可等待对象,不能把任何普通可迭代对象随意 await。生成器与协程有历史联系,但现代 asyncio 项目应使用 async/await,不能把旧式生成器协程模板直接当成当前通用方案。
34. with 如何工作?如何编写上下文管理器?
热度:★★★★★|难度:进阶
参考答案: 上下文管理器通过 __enter__、__exit__ 管理进入和退出。成功进入后,即使代码块抛异常,也会调用退出逻辑;若进入本身失败,则不能依赖该次 __exit__ 清理未完全建立的资源。
|
|
__exit__ 返回真值会抑制异常,因此不能随便 return True。多个动态资源可用 ExitStack;异步资源使用异步上下文管理器。进程强杀等情况下,退出逻辑也不保证运行。参见contextlib。
35. 异常捕获、异常链和 finally 的正确使用方式是什么?
热度:★★★★★|难度:进阶
参考答案: 尽量只捕获预期异常,并缩小 try 范围。else 在 try 正常结束后执行,finally 用于必要清理。裸 except 可能捕获退出或中断信号;一般业务异常处理使用具体异常或 Exception,但也不能无条件吞掉。
|
|
raise ... from exc 保留异常原因;在 except 中直接 raise 可重抛当前异常。finally 中 return 或再抛异常可能覆盖原始结果或错误,应避免。Python 3.11+ 的 ExceptionGroup 与 except* 用于分别处理一组并发异常,不等于普通 except 的简单替代。参见异常处理。
36. lru_cache 有哪些限制?缓存线程安全等于只计算一次吗?
热度:★★★★☆|难度:进阶
|
|
参考答案: 参数需要可哈希,缓存会持有参数和结果的引用。应避免缓存有副作用、依赖未编码外部状态或返回可变共享结果的函数。
内部数据结构线程安全,不代表同一个 key 并发首次请求只计算一次,竞争时可能重复计算。需要 single-flight 语义时要额外协调。maxsize=None 或 cache 无界增长,还需考虑内存和失效策略。参见functools 缓存说明。
四、面向对象:属性、继承与数据模型(37—48)
37. Python 的类与实例是什么关系?为什么说函数也是对象?
热度:★★★★☆|难度:基础
参考答案: 类定义对象的行为和结构约定,实例保存具体状态。类本身也是对象,可以赋值、传参或动态创建;函数同样可以作为值传递、存储和返回,这支持回调、装饰器和策略模式。
|
|
从实例获取普通方法时,函数通常被绑定为方法并携带实例;因此 self 是约定名称,而实例绑定机制不是靠名称拼写触发的。
38. 类变量与实例变量有什么区别?共享可变类属性有什么坑?
热度:★★★★★|难度:基础
参考答案: 类属性通常被实例共享;实例赋值同名属性通常创建或更新自己的实例属性,但描述符可能改变行为。可变类属性若被原地修改,其他实例也能看到变化。
|
|
实例独有的业务状态放在构造或专门初始化逻辑中;全局配置、计数等共享状态则应明确并发和生命周期约束。
39. 普通方法、classmethod 和 staticmethod 如何选择?
热度:★★★★★|难度:基础
参考答案: 普通方法绑定实例,适合访问实例状态;classmethod 绑定调用时的类,适合备用构造器和支持继承的类级操作;staticmethod 不自动绑定实例或类,适合与类语义相关的纯工具逻辑。
|
|
备用构造器返回 cls(...),有利于子类扩展;如果函数与类没明显关联,直接写模块级函数往往更简单。
40. __new__ 与 __init__ 有什么区别?单例怎么评价?
热度:★★★★☆|难度:进阶
参考答案: __new__ 创建并返回实例,__init__ 初始化已创建实例,且应返回 None。不可变类型的定制常需要在 new 阶段处理。若 new 返回的不是该类实例,后续初始化行为也会不同。
单例追问: 可以通过 new、模块级实例或其他方式控制复用,但必须说明线程竞争、重复初始化、测试隔离和多进程边界。一个进程里的单例不等于整个部署全局唯一。
项目中连接池、客户端共享常通过依赖注入或明确生命周期管理实现,比把所有东西写成隐式全局单例更容易测试。只有确有唯一性约束时再讨论单例实现,不要把它当默认设计模式。
41. 多继承的 MRO 是什么?super() 一定调用父类吗?
热度:★★★★☆|难度:进阶
参考答案: MRO 是属性和方法查找顺序,Python 使用 C3 线性化处理多继承。super() 沿当前对象的 MRO 找下一个实现,不是简单写死“调用直接父类”。
|
|
协作式多继承需要方法签名和 super 调用约定一致。复杂业务通常优先组合,避免通过层层继承隐藏流程。
42. property 与描述符是什么关系?属性查找优先级如何理解?
热度:★★★★☆|难度:深入
参考答案: 描述符是实现 __get__、__set__ 或 __delete__ 等协议的对象,property、普通方法绑定等都与描述符机制有关。具有设置或删除协议的是数据描述符;只有读取协议的通常是非数据描述符。
默认实例属性查找中,数据描述符优先于实例字典,实例字典优先于非数据描述符及普通类属性;继承查找还受 MRO 影响,自定义属性访问可改变行为。
|
|
边界: property 不是安全隔离手段;验证应避免意外触发昂贵 I/O。参见描述符指南。
43. __getattr__ 与 __getattribute__ 的区别是什么?
热度:★★★☆☆|难度:深入
参考答案: __getattribute__ 参与实例的普通属性访问;__getattr__ 一般在正常查找失败后提供兜底。覆盖 getattribute 时应通过 object.__getattribute__ 或合适的 super 访问原始属性,避免无限递归。
|
|
兜底不能对所有拼写错误都随意返回 None,否则 hasattr、调试和调用方错误检测都会受影响。特殊方法的隐式调用还可能绕过普通实例属性查找路径,不能假定拦截所有情况。
44. 常见魔术方法有哪些?__repr__ 与 __str__ 的区别?
热度:★★★★☆|难度:进阶
参考答案: __repr__ 提供面向开发和调试的表示,__str__ 面向用户可读展示;此外有 __len__、__iter__、__getitem__、__call__ 等协议入口,让对象接入内置语法。
|
|
注意: repr 不应泄漏密码或 token,也不保证一定可被重新执行构造。运算符方法不支持某种操作数时,适当返回 NotImplemented,让 Python 尝试其他协议,而不是混淆它与 NotImplementedError。
45. dataclass 有什么价值?frozen=True 会递归冻结吗?
热度:★★★★☆|难度:进阶
参考答案: dataclass 根据字段声明生成初始化、表示、比较等方法,适合数据记录。它不自动完成业务校验或类型转换,frozen 通常只限制字段重新赋值,不会递归冻结字段指向的容器。
|
|
可变默认值用 default_factory,为每个实例构造独立对象。选择 frozen、eq、unsafe_hash 等参数前,要明确相等和哈希语义,不应只为了能放进 set 就强行生成哈希。
46. __slots__ 有什么用?一定省内存并禁止新增属性吗?
热度:★★★☆☆|难度:深入
参考答案: slots 声明实例属性槽位,在合适的类布局下可避免每个实例持有普通 __dict__,适合大量同结构对象,可能节省内存并改善属性访问。
|
|
继承层级中若父类或子类引入 __dict__,限制会变化;弱引用还可能需要相应槽位。slots 也不代表对象不可变。是否值得使用应通过目标解释器和真实对象数量测量。
47. 鸭子类型、抽象基类和 Protocol 有什么区别?
热度:★★★★☆|难度:进阶
参考答案: 鸭子类型关注对象是否提供所需行为;ABC 可以声明抽象接口并通过继承等机制表达约束;Protocol 主要服务静态结构化类型检查,让不继承同一基类的对象也能匹配约定。
|
|
类型注解本身不强制运行时校验。即使使用 runtime_checkable,检查能力也不等于全面验证方法签名和业务契约。选择接口机制应服务可替换性和可测试性。
48. 元类是什么?什么时候不需要元类?
热度:★★☆☆☆|难度:深入
参考答案: 元类控制类对象的创建,通常类的元类是 type。它可用于框架级字段收集、注册和类定义约束,但会增加理解、继承和调试成本。
如果只想在子类创建时登记信息,常可用 __init_subclass__;修改已创建类可用类装饰器;成员访问规则可用描述符。不要把简单需求都升级成元类设计。
|
|
追问: 注册表持有强引用,动态大量生成类时需考虑生命周期;模块重复加载和命名冲突也应有明确策略。
五、运行时与工程基础:内存、导入和类型(49—58)
49. CPython 如何执行代码?.pyc 是机器码吗?
热度:★★★★☆|难度:进阶
参考答案: CPython 将源码解析、编译为代码对象及字节码,再执行。.pyc 通常是用于加速后续导入的字节码缓存,不是通用原生机器码,也不保证跨 Python 版本兼容。
缓存是否有效要依据对应版本的校验方式判断。删除 pycache 通常不会改变程序语义,但会失去相应导入缓存;发布时也不应把字节码当成可靠的源码保密手段。
追问: CPython、PyPy 等实现的执行和优化策略不同。讨论 JIT、字节码指令或原子性时,需要先明确实现和版本,不能把某次反汇编结果当成长期语言保证。
50. CPython 如何管理内存?引用计数为什么不够?
热度:★★★★★|难度:进阶
参考答案: CPython 常以引用计数为基础管理对象生命周期,并辅以循环垃圾回收,处理容器之间互相引用但整体不可达的情况。仅靠引用计数,循环对象可能无法降到零。
|
|
内存分配器可能保留已释放的内存供后续使用,所以对象被回收不等于 RSS 立即下降。GC 的代际策略和自由线程实现会随版本变化,面试中应说明大方向,不要无条件背诵某个旧版阈值。参见gc 文档。
51. del 会立刻销毁对象吗?__del__ 能代替资源清理吗?
热度:★★★★☆|难度:进阶
参考答案: del name 删除名称绑定;若还有其他引用,对象继续存在。del container[key] 则调用相应删除协议,并非统一的“立即释放内存”操作。
|
|
__del__ 的时机和运行环境不适合承担必须完成的业务清理,解释器退出、循环引用和异常等情况会增加复杂性。文件、锁、事务和网络连接应通过 with、try/finally 或显式关闭管理。
弱引用适合不延长对象寿命的索引或缓存,但对象类型要支持弱引用,且它也不是替代完整资源生命周期管理的万能方案。
52. sys.getsizeof 能得到整个对象图的真实内存吗?
热度:★★★☆☆|难度:进阶
参考答案: 它通常只报告对象自身占用,不递归统计所有被引用对象,且不同扩展对象的报告能力不同。列表自身大小不包含每个元素的完整独立内存。
|
|
递归求和时还要避免重复计数共享对象和陷入循环。分析服务内存需区分对象分配、原生扩展、内存映射和进程 RSS;不能把某个容器的 sizeof 当作整个服务内存结论。
53. 如何定位 Python 的 CPU 与内存性能问题?
热度:★★★★☆|难度:项目应用
参考答案: 先定义可重复工作负载和指标,再区分 CPU、I/O、锁等待、内存增长与外部服务延迟。用 cProfile 等定位调用热点,微基准可用 timeit;用 tracemalloc 比较分配快照。
|
|
tracemalloc 不覆盖所有原生扩展自行管理的内存,且最好尽早启用。性能结论要说明样本、环境和波动,不能只拿一次运行时间证明优化成立。参见tracemalloc 文档。
54. 模块导入时会发生什么?为什么出现循环导入?
热度:★★★★★|难度:进阶
参考答案: 常规导入先考虑模块缓存,再查找并加载模块;首次执行模块时会运行其顶层代码。模块在完全初始化前就可能出现在缓存中,循环导入时另一方可能访问到尚未定义的名称。
解决优先考虑抽取共同类型或接口、明确依赖方向、减少模块顶层副作用。局部延迟导入可解决部分问题,但不应掩盖整个架构的相互依赖。
项目陷阱: 文件命名为 json.py、typing.py 等,可能遮蔽标准库;导入阶段就连接数据库、发送请求,也会让测试收集和命令行启动变得脆弱。参见导入系统。
55. import module 与 from module import name 有什么区别?reload 会更新全部引用吗?
热度:★★★★☆|难度:进阶
参考答案: 前者绑定模块名,使用时通过模块访问属性;后者把当时得到的对象绑定到当前模块名称。原模块后来重绑定属性,不会自动更新所有已导入的本地名称。
importlib.reload 会重新执行模块代码,但其他模块持有的旧函数、旧类实例和名称引用不一定一起更新,因此热更新远不止调用 reload。
面试联系: Mock 应替换被测代码实际查找名称的位置,正是因为名称绑定不同。生产部署通常使用受控进程替换,而不是假设任意运行状态都能安全 reload。参见importlib 与 reload。
56. 类型注解会在运行时检查参数吗?Any 与 object 有什么不同?
热度:★★★★☆|难度:进阶
参考答案: 类型注解本身通常不强制参数类型,静态检查需要 mypy、Pyright 等工具,运行时校验需要额外逻辑或库。不能把“通过类型检查”当成“外部输入已经安全”。
|
|
Any 基本放宽静态检查;object 表示未知但合法对象,使用具体能力前通常需要缩小类型。滥用 Any 会让检查形同虚设。TypedDict 主要描述字典结构,不会自动检查传入 JSON 是否符合要求。参见typing 文档。
57. 前向引用和注解求值有哪些版本差异?
热度:★★★☆☆|难度:深入
参考答案: 前向引用允许引用尚未完成定义的类型;在传统求值环境中常用字符串或 from __future__ import annotations 避免定义时查找失败。
|
|
上例按 Python 3.10+ 的类型写法理解。Python 3.14 默认注解求值机制发生变化,反射代码不应假设所有版本的 __annotations__ 内容与求值时机完全一致。使用 get_type_hints 或相应注解工具时,还要考虑名称可见性及可能执行表达式的边界。参见Python 3.14 注解变化。
58. 虚拟环境、依赖锁定与 pyproject.toml 各解决什么问题?
热度:★★★★☆|难度:工程基础
参考答案: 虚拟环境隔离不同项目的 Python 包;项目元数据与构建配置描述项目如何安装;锁定文件或可复现依赖约束记录实际解析版本,减少环境漂移。仅写宽松依赖范围通常不足以重建完全一致的环境。
运行安装命令时使用明确解释器,例如 python -m pip;检查包安装在哪个环境,而不是看到“安装成功”就认为运行环境一致。Python 版本、平台和原生依赖也会影响可复现性。
追问: requirements 文件可以记录依赖,但文件名本身不保证锁定质量。发布库的兼容范围与部署应用的精确锁定有不同目的;不要把本机全局环境无差别冻结后当成项目最佳实践。参见Python Packaging 官方教程。
六、并发与异步:线程、进程、协程和取消(59—70)
59. 线程、进程、协程有什么区别?如何选择?
热度:★★★★★|难度:进阶
| 方式 | 特点 | 常见适用条件 |
|---|---|---|
| 线程 | 同进程共享内存,需同步 | 阻塞 I/O;释放 GIL 的扩展计算 |
| 进程 | 隔离地址空间,通信和启动有成本 | 可拆分的 CPU 密集任务 |
| asyncio 协程 | 事件循环协作调度 | 大量可异步等待的 I/O |
参考答案: 并发是多个任务在时间上交错推进,并行是多个任务同时执行。协程通常在 await 等挂起点让出执行,不是自动利用多个 CPU 核。
选择先看瓶颈、依赖是否异步兼容、任务粒度和资源限制。常见带 GIL 的 CPython 下,纯 Python CPU 计算不一定能通过多线程加速;自由线程构建、C 扩展和不同解释器需要另外判断。
60. GIL 是什么?有 GIL 就没有线程安全问题吗?
热度:★★★★★|难度:进阶
参考答案: 在启用 GIL 的 CPython 中,全局解释器锁限制同一解释器内多个线程同时执行 Python 代码。它是实现机制,不是 Python 语言本身要求,也不是业务数据的事务锁。
I/O 和部分扩展可以释放 GIL,因此多线程仍可能有价值。复合的“检查后修改”、多对象约束和共享状态更新仍需要同步,不能因为字节码或某个内置操作看起来安全就推导整个流程安全。
|
|
易错点: 不要用某个版本的一次压力测试证明 counter += 1 永远原子。正确性要建立在同步约定上。参见threading 文档。
61. Lock、RLock、Semaphore、Event、Condition 如何选择?
热度:★★★★☆|难度:进阶
| 工具 | 目的 | 关键点 |
|---|---|---|
| Lock | 互斥访问共享状态 | 同线程重复获取也可能阻塞 |
| RLock | 同线程可重入互斥 | 获取与释放次数需匹配 |
| Semaphore | 限制同时占用资源数量 | 不提供业务上的每秒速率限制 |
| Event | 通知某状态已发生 | 不替代完整的数据队列 |
| Condition | 持锁等待某条件变化 | 应循环检查条件 |
参考答案: 根据需要保护的不变量选择工具。锁范围既要覆盖完整状态转换,也要避免把慢网络请求长期包在锁内。
追问: 用 Condition 时应在循环中检查共享条件,不能只依赖“收到一次通知”。一个线程的 Lock 也不能直接充当跨机器分布式锁。
62. Queue 如何实现生产者消费者?join() 等待什么?
热度:★★★★☆|难度:进阶
参考答案: queue.Queue 提供线程间安全的入队出队和可选容量限制。Queue.join() 等待已入队工作被相应的 task_done() 标记完成,不等于等待消费者线程自动退出。
|
|
示例只用一个消费者。多个消费者要设计足够的停止信号、错误传递与退出协调;消费者意外死亡后,join 可能一直等,不能仅靠 finally 就认为完整系统没有死锁风险。参见Queue 文档。
63. 线程池和进程池如何取结果?超时能终止工作吗?
热度:★★★★★|难度:进阶
|
|
参考答案: submit 返回 Future,result 获取结果并传播任务异常;as_completed 按完成顺序产出,map 通常按输入顺序产出结果。
future.result(timeout=...) 超时通常只是调用方停止等待,不会强行中断已经运行的线程任务;cancel 通常只能取消尚未开始的工作。上下文退出默认还可能等待任务完成。超时应落实到网络、数据库和业务操作本身,并设计协作停止。参见concurrent.futures。
64. multiprocessing 常见坑有哪些?默认启动方式是否一直相同?
热度:★★★★☆|难度:深入
参考答案: 子进程任务通常应定义在可导入模块顶层,传入参数和返回结果需要满足所用通信机制的序列化要求。使用主入口保护,避免子进程导入时重复启动进程。
|
|
不能假设所有平台都默认 fork。Python 3.14 对默认方式有变化:支持条件下 POSIX 默认转向 forkserver,macOS 与 Windows 常用 spawn;应查目标平台并按需显式指定上下文。已有线程、连接池和锁也不能假设能安全继承复用。参见multiprocessing 启动方式。
65. coroutine、Task、Future 分别是什么?gather 和 TaskGroup 怎么选?
热度:★★★★★|难度:进阶
参考答案: 调用 async 函数得到协程对象;Task 负责调度协程并跟踪执行;Future 表示将来完成的结果,是更底层的等待机制。仅创建协程对象不等于完成执行,create_task 才把它交给事件循环调度。
Python 3.11+ 的 TaskGroup 提供结构化并发:退出作用域前等待子任务;一个任务出现非取消异常时,通常取消同组其他任务,并聚合相关异常。
|
|
gather 默认传播遇到的异常,但不会因此自动取消其他已提交任务,语义与 TaskGroup 不同。选型要明确部分成功与失败传播策略,而不是只看调用更短。参见协程与任务。
66. asyncio 取消和超时是怎样发生的?为什么不能随便吞 CancelledError?
热度:★★★★☆|难度:深入
参考答案: 取消通常在协程可响应的挂起点注入 CancelledError,是协作机制,不是任意时刻强杀。用 finally 释放资源,若捕获取消只为清理,通常应继续传播,避免破坏上层超时和 TaskGroup 协调。
|
|
TimeoutError 在 timeout 上下文外处理。同步阻塞代码、迟迟不让出执行的循环,可能延误超时。to_thread 取消也不保证后台线程立即停止;远程写入是否完成仍需通过业务协议确认。
67. 如何限制异步并发?Semaphore 能解决所有内存问题吗?
热度:★★★★★|难度:项目应用
参考答案: Semaphore 可限制某段临界区域同时进行的操作数量,适合控制在途 HTTP 请求;但先创建一百万个 Task,再让它们等 Semaphore,仍可能占用大量内存。
|
|
上例适合少量输入。大数据量应使用有界队列加固定 worker,或分批提交,建立背压。并发上限不等于每秒请求速率,还要单独处理限流、重试预算和连接池容量。
68. contextvars 为什么比全局变量更适合保存请求上下文?
热度:★★★☆☆|难度:深入
参考答案: 全局变量容易被并发请求覆盖;threading.local 在线程内隔离,但同一线程里的多个协程仍可能混用。ContextVar 提供上下文本地值,适合 trace ID、租户标识等异步请求信息。
|
|
任务通常在创建时复制当前上下文,后续修改不意味着自动双向同步;跨线程、进程和网络边界也应确认传播机制。不要把可变默认对象放进去后误以为其内部内容会自动隔离。参见contextvars。
69. 常见死锁与事件循环阻塞是怎么产生的?
热度:★★★★☆|难度:故障排查
参考答案: 线程死锁常来自不一致的加锁顺序、持锁等待其他线程、或线程池任务等待同池内无法调度的任务。异步程序则常因在事件循环线程执行同步 I/O、长时间 CPU 循环或错误使用阻塞锁而停滞。
预防方式包括统一锁顺序、缩小临界区、避免在锁内等待不受控调用、使用超时与任务状态诊断。同步函数需要放到合适执行器时,也要明确线程安全和取消边界。
排查顺序: 先区分 CPU 忙、阻塞系统调用、锁等待和外部依赖慢;获取线程栈或异步任务栈再判断。不能看到“没有输出”就直接归因于 GIL,也不能靠无限增加线程数解决锁竞争。
70. 自由线程 Python 会让所有多线程程序自动加速吗?
热度:★★★☆☆|难度:深入
参考答案: 不会。CPython 从 3.13 起提供可选的自由线程构建;是否启用要看解释器构建和运行状态,不能仅根据 Python 大版本判断“已经没有 GIL”。
不兼容的扩展可能使 GIL 重新启用;现有程序的共享状态也需要重新审查。内置容器的内部同步不等于多个操作构成原子业务事务,锁竞争、内存开销和任务粒度仍影响收益。
项目建议: 检查依赖兼容性,分别测试吞吐、延迟、内存与正确性,再决定迁移。它不消除数据竞争,也不替代进程隔离、异步 I/O 或合理的工作划分。参见自由线程官方说明。
七、工程必备:文件、序列化、日志与可测试性(71—80)
71. 文件读写有哪些基本原则?如何减少写到一半的损坏风险?
热度:★★★★☆|难度:工程应用
参考答案: 用 with 管理关闭,文本文件明确编码,二进制内容使用二进制模式;路径用 pathlib 组合,避免手工拼分隔符。大文件优先分块或逐行处理。
|
|
写入同一文件系统内的临时文件,再替换目标,可减少读者看到半文件的风险,但不等于断电后持久化保证。严格持久化还涉及 flush、fsync 和目录同步等平台规则;多写者也需要唯一临时名与协调。参见pathlib。
72. JSON 与 pickle 如何选择?JSON 转换有哪些信息损失?
热度:★★★★★|难度:基础
参考答案: JSON 适合跨语言数据交换,但支持的类型有限;pickle 可序列化更多 Python 对象,却与代码、版本和运行环境耦合,反序列化不可信内容可能执行任意代码。
|
|
JSON 对象键是字符串,元组通常转为数组,Decimal、日期等需要明确协议。不要让 default=str 无差别隐藏类型问题;金额字符串、时间格式和精度都要约定。解析外部 JSON 也需要限制大小和校验结构。参见JSON与pickle。
73. naive 与 aware datetime 有什么区别?测耗时用哪个时钟?
热度:★★★★★|难度:进阶
参考答案: naive datetime 缺少明确时区语义,aware datetime 携带可解释的时区信息。服务间传递时间应统一协议,通常以 UTC 保存时间点,展示时转换为业务时区。
|
|
测持续时间使用 monotonic/perf_counter,避免系统时间校准造成倒退。replace(tzinfo=...) 通常是附加或替换解释,不等于时区转换;夏令时还可能产生不存在或重复的本地时间。参见datetime与zoneinfo。
74. 为什么项目使用 logging 而不是到处 print?怎样避免重复日志?
热度:★★★★☆|难度:工程应用
参考答案: logging 支持级别、命名 logger、格式、过滤和输出通道,方便集中管理。业务模块使用 logging.getLogger(__name__),通常由应用入口统一配置 handler。
重复日志常来自重复添加 handler,或子 logger 和父 logger 都输出且 propagation 仍开启。多进程写同一滚动日志文件也可能冲突,应采用队列汇集或标准输出交由平台采集。
|
|
日志应带 trace ID、业务关联号和耗时,但避免完整凭据和个人敏感数据。日志量也要有预算,不能为了排查把巨大响应反复同步写盘。参见Logging Cookbook。
75. Python 项目常见的输入与安全边界有哪些?
热度:★★★★☆|难度:工程应用
参考答案: 不把外部输入交给 eval/exec,不反序列化不可信 pickle,不把输入直接拼进 SQL 或 shell。SQL 使用驱动参数绑定,命令使用参数列表,并限制目标操作。
文件路径要检查是否允许访问目标范围,不能只做字符串前缀比较;压缩包解包还要防止路径穿越。解析数据时限制大小、深度和处理成本,避免资源耗尽。
面试加分点: 类型注解、转义字符串和日志脱敏分别解决不同问题,不能相互替代。配置中的地址、路径和凭据也属于信任边界,应在进入业务逻辑前进行明确验证,而不是遇错后统一捕获继续执行。
76. 如何安全地运行外部命令并处理超时?
热度:★★★★☆|难度:工程应用
|
|
参考答案: 使用参数列表与默认 shell=False,显式设置超时、工作目录和必要环境;check=True 让非零退出码转为异常。需要流式输出时使用合适的 Popen 管理方式,避免把无限输出全部收集到内存。
边界: 超时终止直接子进程不等于终止它创建的所有后代进程。进程树清理与平台有关,要单独设计。示例 -c 内容是固定代码,不应替换成不可信输入。参见subprocess。
77. 怎样写可测试的 Python 代码?Mock 的边界在哪里?
热度:★★★★★|难度:项目应用
参考答案: 把纯业务计算与网络、数据库、时间和随机性等副作用分开;通过明确参数或接口注入外部依赖。测试核心逻辑时使用真实计算,外部交互用替身控制,再用少量集成测试验证真实协议。
|
|
不要把被测函数本身替换掉后再证明其预设结果正确。patch 应作用于代码查找名称的位置;关键客户端可使用 autospec 减少签名漂移。详细 fixture 和插件机制可继续复习同目录 Pytest 笔记。参见unittest.mock。
78. Python 算法题除了写出结果,还应该说明什么?
热度:★★★★★|难度:基础到进阶
参考答案: 说明输入假设、边界、时间和空间复杂度、是否修改原数据,以及结果是否允许重复。常见题型包括去重计数、Top K、二分、滑动窗口、栈队列、链表和树遍历。
|
|
此例平均 O(n) 时间、O(n) 空间,适用于可哈希数值。不要用 if previous 判断索引是否存在,因为合法索引 0 为假;重复值也不能错误复用同一位置。
79. Python 升级时重点验证什么?只跑单元测试够吗?
热度:★★★☆☆|难度:工程设计
参考答案: 检查语法和标准库变化、依赖支持范围、C 扩展轮子、序列化与反射行为、进程启动方式,以及性能和内存差异。对反射注解、异步调度、原生扩展依赖重的项目尤其需要兼容性验证。
建立目标版本测试矩阵,执行单元、集成和关键端到端路径;对真实负载做基准对比,审查弃用警告。先在隔离环境和小范围流量中验证,准备可执行的回退方式。
易错点: “代码能 import”不代表行为完全一致;“新版更快”也不保证你的工作负载更快。不要同时升级解释器、框架和所有业务依赖而又缺少定位手段。
80. 配置和密钥如何管理?为什么不建议散落在全局变量中?
热度:★★★★☆|难度:工程设计
参考答案: 在应用入口按明确优先级解析配置,验证类型和必填项,再把结构化配置注入组件。普通非敏感配置可版本管理,密钥通过受控环境或密钥管理系统提供。
不要在每个模块导入时分别读取并缓存环境变量,否则测试修改环境后可能不生效,也难追踪实际使用值。配置对象应尽量保持稳定,不要让任意业务函数随意修改全局环境。
项目追问: 配置错误应尽早暴露,日志显示环境和非敏感摘要即可;连接字符串、认证头和私钥不能因调试方便进入日志或异常表示。动态配置还需说明版本、一致性和回滚策略。
八、实际项目场景:从脚本到稳定服务(81—90)
81. 处理几十 GB 日志或 CSV,怎样避免一次读爆内存?
热度:★★★★★|难度:项目设计
场景: 每天扫描大量日志,按用户和错误类型输出统计,机器内存有限。
参考答案: 采用流式读取和逐条解析,按批次写入结果;CSV 使用 csv 模块,不直接按逗号 split,因为字段可能带引号、逗号和换行。输入、处理、输出都保持有界,避免最后又 list 化全部结果。
|
|
深入追问: 如果用户数量也很大,Counter 仍会持续增长;可用外部排序、分区聚合或数据库聚合。还要设计坏行处理、文件编码、超长单行、断点续跑和重复输入识别。生成器只能减少部分内存,并不自动解决所有聚合状态。
82. 一百万条记录需要调用接口,如何控制吞吐、内存和失败?
热度:★★★★★|难度:项目设计
参考答案: 不一次创建一百万个任务。使用流式生产者、有界队列、固定 worker 和受控写出,分别设置并发、速率、超时、重试预算和连接池。下例用本地计算模拟 I/O,展示有限在途工作。
|
|
边界: 示例 output 仍是内存列表,大规模时要替换为有界批量写出。本例采用任务失败即取消整组的策略;若要部分成功,需逐条记录可重试与永久失败,并处理取消和退出,不能简单吞异常。输出顺序也不保证与输入相同,可附序号后有界重排。
83. 网络不稳定,如何设计有限重试并避免重复写入?
热度:★★★★★|难度:项目设计
参考答案: 明确哪些错误可重试,限制次数和总时间预算,使用指数退避及适当抖动,并尊重服务端限流指引。写请求超时后,服务端可能已经完成,必须结合幂等键、状态查询或补偿机制。
|
|
这是精简策略示例,不含总截止时间、抖动和真实 HTTP 异常分类。实际客户端还要对单次调用设置超时,防止一次调用永久阻塞。不要对认证错误、参数错误和业务拒绝统一重试;也不要让多层重试相乘形成重试风暴。
84. 服务运行几天后内存越来越高,怎样排查?
热度:★★★★★|难度:故障排查
参考答案: 先确认增长是持续、台阶还是正常缓存预热,区分 RSS 与 Python 可追踪分配。固定工作负载比较内存快照,定位对象类型和引用链,检查无界缓存、全局列表、回调闭包、未结束任务和批量结果。
若 tracemalloc 增长不明显而 RSS 持续增长,检查原生扩展、内存映射和分配器保留,不应直接认定“GC 失效”。观察 GC 统计可以辅助,但反复 gc.collect() 通常不能修复仍被引用的数据。
项目闭环: 加入缓存容量和任务数量指标,建立可复现的长时间或批次压力测试。修复后验证增长曲线、吞吐和延迟,而不是只看服务重启后内存下降。
85. 接口平均响应突然变慢,如何判断该优化 Python 代码还是数据库?
热度:★★★★★|难度:故障排查
参考答案: 先看分位延迟和分阶段耗时:入口排队、认证、业务 CPU、数据库、远程依赖和序列化。平均值可能掩盖少量长尾请求,应比较相同请求类型与负载。
CPU 高时检查算法、重复计算和序列化热点;CPU 不高但延迟高时,重点看连接池等待、慢查询、锁竞争、外部超时与事件循环阻塞。数据库问题还需检查查询计划、索引和 N+1 请求,而不只是换更快的 JSON 库。
取舍: 缓存要定义一致性和失效策略,扩线程要考虑下游容量,异步改造要确认依赖链是否真的非阻塞。每项优化都用相同负载验证收益和副作用。
86. Python 操作数据库,怎样保证事务边界和避免“部分成功”?
热度:★★★★★|难度:项目设计
参考答案: 将同一数据库内必须共同成功的操作放在明确事务中,失败回滚,成功提交;SQL 使用参数绑定,避免字符串拼接。连接和事务对象按驱动约定管理,不假设可跨线程或进程共享。
|
|
示例依赖 sqlite3 此连接配置下的事务行为,不能推导所有驱动的 with 都会自动回滚和关闭。数据库与消息队列、第三方支付之间通常没有单一原子事务,可考虑事务消息表、幂等消费和补偿;“套一个 try/except”无法解决分布式一致性。
87. 清洗外部 JSON 数据时,缺失、空值、零和类型错误如何处理?
热度:★★★★★|难度:项目应用
参考答案: 在边界层做结构和类型校验,区分字段不存在、显式 null、空字符串、零和非法类型;业务层接收已规范化数据。不能用 value or default 把所有假值混为一谈。
|
|
是否接受数字字符串要由协议定义;自动强转可能接受不该接受的输入。校验失败应带可定位的字段信息,但避免将完整敏感请求直接写进错误日志。
88. 定时任务跨时区运行,怎样避免漏执行和重复执行?
热度:★★★★☆|难度:项目设计
参考答案: 先区分“每隔固定时长”与“当地时间每天某时”两个需求,明确业务时区和夏令时策略。持久化时间点通常使用 UTC,但业务日期应按目标时区计算。
为每个计划窗口建立稳定任务键,通过持久化状态、唯一约束或适当锁控制重复执行;完成后记录输入范围与结果。进程重启、调度延迟和上次未完成时,明确补跑、跳过或合并策略。
易错点: 仅在进程内用布尔变量标记已执行,无法应对多副本和重启。每天 sleep 24 小时也不等于“每天当地 9 点”;本地时间在夏令时切换时可能重复或不存在。处理必须符合业务定义。
89. 如何把一次性 Python 脚本改成可维护的后台任务或服务?
热度:★★★★☆|难度:项目设计
参考答案: 抽离输入、核心处理和输出,消除导入副作用和隐式全局状态。增加结构化配置、有限超时、错误分类、幂等、指标和可关联日志,并按资源生命周期关闭连接与执行器。
任务要能优雅停止:停止接收新工作,等待或取消在途任务,在规定时间内保存进度与清理。队列任务还需明确确认时机和崩溃后重投行为,避免“先确认后失败”造成丢失。
面试加分点: 不是所有脚本都需要重型框架。先围绕运行频率、失败成本和维护人数增加必要结构,用测试、明确退出码和可靠部署形成闭环,而不是单纯增加目录。
90. 面试官让你讲一个 Python 项目,怎样体现真实能力?
热度:★★★★★|难度:综合表达
参考答案: 按“业务问题 → 约束 → 方案 → 关键实现 → 验证证据 → 仍存在的限制”讲一个具体案例。不要只列“用了多线程、装饰器和 Redis”。
例如批量接口处理项目,可以说明原来一次性加载输入且无限提交任务,导致内存上涨和下游限流;通过流式输入、有界队列、固定 worker、重试预算与幂等标识控制负载;用失败注入验证超时、部分成功、取消和重启续跑。
优化结果引用真实日志或指标中的吞吐、峰值内存、P95 延迟和失败率;没有记录就说明验证范围,不能编造性能提升比例。最后解释为什么选线程或协程,为什么没有简单加大并发,以及数据恢复的边界。
常见追问: 你负责哪部分?哪一步最容易失败?如果进程被杀会怎样?为什么不用进程池?重试是否会重复写入?怎么证明没有丢数据?这些问题比背 API 更能区分实践深度。
九、高频错误说法纠正表
| 容易说错的话 | 更准确的表述 |
|---|---|
| Python 是弱类型语言 | 动态类型不等于弱类型,对象有明确类型 |
| 变量就是装值的盒子,赋值会复制 | 名称绑定对象,赋值通常不复制 |
| 不可变对象内部什么都不能变 | 不可变容器仍可引用可变对象 |
| 元组都可以当字典键 | 元组所有成员都可哈希才行 |
| dict/set 操作永远 O(1) | 通常平均 O(1),还要考虑最坏与哈希比较成本 |
is 比 == 快,所以都用 is |
身份与相等是不同语义 |
| 默认列表每次调用都会重新创建 | 默认表达式通常在定义执行时求值 |
| 生成器完全不占内存 | 仍有状态和引用,消费方也可能积累结果 |
| finally 一定不会改变返回结果 | finally 中返回或抛错可能覆盖原结果 |
del 保证立刻把内存还给系统 |
先删除绑定,生命周期与分配器需另行判断 |
| Python 有 GC 就不会内存泄漏 | 仍被引用的无界缓存、任务和容器不会自动消失 |
| 有 GIL 就不需要锁 | 业务复合操作仍需同步,且构建和版本不同 |
| result 超时会停止线程执行 | 通常只停止等待,正在执行的工作仍可能继续 |
| 多进程一直默认 fork | 默认方式与平台、版本有关 |
| async 函数天然并行 | 需要调度与协作等待,也不自动跨 CPU 并行 |
| Semaphore 能限制所有内存占用 | 若预创建大量任务,任务自身仍会占内存 |
| 类型注解会验证 JSON | 注解不等于运行时输入校验 |
replace(tzinfo=...) 就是时区转换 |
实际转换应明确使用 astimezone 等机制 |
| deepcopy 能复制任何资源并完成隔离 | 资源对象和业务所有权需单独设计 |
| 自由线程让所有程序自动更快 | 兼容性、同步、争用和工作负载都影响结果 |
十、复习路径与现场练习
| 目标 | 推荐范围 | 要达到的能力 |
|---|---|---|
| 入门扎实 | 1—26 | 能解释对象与容器,写正确的边界处理 |
| 中级过关 | 27—47、54—58、71—78 | 能写闭包、装饰器、生成器,解释对象协议与工程边界 |
| 进阶区分 | 48—53、59—70 | 能分析内存、并发、取消与版本差异 |
| 项目表达 | 79—90 | 能讲故障证据、恢复策略和方案取舍 |
建议亲手完成以下练习:
- 用一个例子同时解释赋值、浅拷贝、深拷贝和可变默认值。
- 写保序去重、Top K 和两数之和,说明边界与复杂度。
- 写一个保留元信息的装饰器,再改成能正确等待异步函数的版本。
- 写生成器和上下文管理器,模拟中途失败,检查资源是否清理。
- 用两线程修改共享状态,再加入显式锁,解释正确性依据。
- 写有界队列的异步工作池,测试单任务失败、取消、超时和输出背压。
- 模拟网络超时后服务端已经成功,设计幂等查询与安全重放。
- 用 tracemalloc 比较多轮处理前后快照,排查一个无界缓存案例。
- 给真实项目写一段第 90 题的回答,准备能支撑结论的日志或数据。
复习取舍: 优先掌握五星题和真实项目中用到的知识。元类、描述符等可以逐步深入,但对象共享、异常处理、数据隔离和并发边界不能只停留在术语层面。
十一、官方资料索引
以下链接用于核对语义和版本差异。多数固定到 Python 3.14 文档,便于避免跟随默认文档版本变化;面试热度和项目取舍属于本文的经验整理。
- Python 教程
- 控制流、参数与函数
- 内置类型
- 数据模型与特殊方法
- Unicode 指南
- copy 与 deepcopy
- collections
- 描述符指南
- dataclasses
- contextlib
- functools
- 异常处理
- 垃圾回收
- tracemalloc
- 性能剖析
- 模块导入
- typing
- Python Packaging
- threading
- Queue
- concurrent.futures
- multiprocessing
- asyncio 任务、取消和超时
- contextvars
- 自由线程
- Decimal
- JSON
- pickle
- datetime
- zoneinfo
- 日志实践
- subprocess
- unittest.mock
- Python 3.14 变化