【Python】Python语言面试题

这份笔记共整理 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 的变量与赋值是怎样工作的?函数参数是值传递还是引用传递?

热度:★★★★★|难度:基础

参考答案: 赋值建立名称与对象的绑定,不会默认复制对象。函数调用时形参绑定到传入的对象,常称为对象共享或传对象引用;用“值传递/引用传递”二选一容易遗漏重绑定与修改对象的区别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def update(items, number):
    items.append("new")   # 修改调用方也能看到的对象
    number = 99           # 只改变局部名称绑定
    items = ["local"]     # 不会替换调用方变量绑定

values = []
count = 1
update(values, count)
assert values == ["new"]
assert count == 1

加分点: 函数能否影响调用方,关键是是否修改共享对象,而不是“参数是不是局部变量”。需要隔离时由接口明确约定复制和所有权。

3. 可变与不可变对象有什么区别?元组里能放列表吗?

热度:★★★★★|难度:基础

参考答案: 列表、字典、集合通常可变;整数、字符串、元组等不可变。不可变指对象自身的结构或值不能原地修改,不代表它引用的所有对象都递归不可变。

1
2
3
4
5
6
7
8
record = ([1], "tag")
record[0].append(2)
assert record == ([1, 2], "tag")

text = "ab"
original = text
text += "c"
assert original == "ab" and text == "abc"

元组可以引用列表,不能替换元组中的引用位置,但可以修改那个列表。这样的元组也不能用作字典键,因为它包含不可哈希成员。

易错点: 不可变、可哈希和线程安全不是同义词,要分别判断。

4. is 与 == 有什么区别?为什么不能用 is 比较字符串?

热度:★★★★★|难度:基础

参考答案: is 比较对象身份,== 比较相等关系,后者可由类型自定义。判断单例 None 通常使用 is None;比较业务值使用 ==。

1
2
3
4
5
6
left = [1, 2]
right = [1, 2]
alias = left
assert left == right
assert left is not right
assert left is alias

CPython 的整数缓存、字符串驻留等优化可能让两个相等值碰巧共享身份,但不是一般业务逻辑可依赖的规则。对象存活期间身份稳定;对象销毁后,后续对象可能复用其 id,因此不能把 id() 当永久业务标识。

5. 哪些值是假?and、or 一定返回布尔值吗?

热度:★★★★★|难度:基础

参考答案: None、False、数值零、空容器等通常为假。自定义对象可以通过 __bool__ 或 __len__ 定义真值。and、or 会短路,并返回操作数,不保证返回 bool。

1
2
3
assert ("" or "default") == "default"
assert ([] and 123) == []
assert ("ready" and 123) == 123

项目陷阱: timeout = supplied or 30 会把合法的 0 也当成缺失。若只有 None 表示未提供,应写 30 if supplied is None else supplied。同理,不能用 if value 混淆空字符串、零和不存在。

6. /、//、% 有什么区别?负数整除如何计算?

热度:★★★★☆|难度:基础

参考答案: / 是真除法;对于整数,// 向负无穷方向取整,而不是向零截断;整数余数遵循 a == (a // b) * b + a % b,除数不能为零。

1
2
3
4
5
assert 7 / 2 == 3.5
assert 7 // 2 == 3
assert -7 // 2 == -4
assert -7 % 2 == 1
assert 7 % -2 == -1

非零整数余数与除数同号。若业务规则要求“向零截断”,必须明确实现,不能直接把 // 当成 C 等语言中所有整数除法行为的等价替代。

7. 如何区分“没传参数”与“显式传了 None”?

热度:★★★★☆|难度:基础

参考答案: 使用唯一哨兵对象,避免让一个值承担两种不同语义。这在配置更新、PATCH 接口和可选过滤条件中很常见。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
MISSING = object()


def resolve(value=MISSING):
    if value is MISSING:
        return "use default"
    if value is None:
        return "explicitly clear"
    return value

assert resolve() == "use default"
assert resolve(None) == "explicitly clear"
assert resolve(0) == 0

边界: 哨兵按身份比较;如果需要跨进程序列化或在公共 API 中稳定表达缺失,应设计明确协议,不能假设普通 object() 在反序列化后仍保留身份。

8. 字符串格式化有哪些方式?f-string 有什么注意事项?

热度:★★★★☆|难度:入门

参考答案: 常见方式有 f-string、str.format 和 % 格式化。f-string 适合代码中直观地插入表达式和格式规范;大量片段拼接通常使用 join。

1
2
3
4
name = "Alice"
score = 93.456
assert f"{name}: {score:.2f}" == "Alice: 93.46"
assert ",".join(["a", "b", "c"]) == "a,b,c"

项目注意: 日志通常用参数化形式 logger.info("user=%s", user_id),便于推迟消息格式化,但传参表达式自身仍会先计算。f-string 不是 SQL 参数绑定工具,不应把不可信输入拼进 SQL 或命令字符串。

9. for...else 和 while...else 的 else 何时执行?

热度:★★★☆☆|难度:基础

参考答案: 循环正常结束且未被 break 提前退出时执行 else。它不表示“循环体一次都没执行”;空循环也会正常进入 else。return 和向外传播的异常也会离开整个结构。

1
2
3
4
5
6
7
8
9
def find_even(values):
    for value in values:
        if value % 2 == 0:
            return value
    else:
        return None

assert find_even([1, 3, 5]) is None
assert find_even([1, 4, 5]) == 4

它适合搜索、重试等需要表达“没有提前成功退出”的逻辑;如果团队不熟悉,使用显式返回也可以,优先保证可读性。

10. 列表推导式与生成器表达式有什么区别?

热度:★★★★★|难度:基础

参考答案: 列表推导式立即计算并保存结果,适合多次访问;生成器表达式按需产出,适合流式处理,但通常只能遍历一次。

1
2
3
4
5
squares = [x * x for x in range(4)]
stream = (x * x for x in range(4))
assert squares == [0, 1, 4, 9]
assert sum(stream) == 14
assert list(stream) == []

生成器节省结果存储空间,不保证一定更快,也不是“完全不占内存”:它仍保存迭代状态及引用。不要用复杂嵌套推导式执行副作用;难以阅读时拆成普通循环。

11. str 与 bytes 的区别是什么?编码和解码方向是什么?

热度:★★★★★|难度:基础

参考答案: str 表示 Unicode 文本,bytes 表示字节序列。encode 将文本编码为字节,decode 将字节按编码解释为文本。

1
2
3
4
5
text = "中文"
raw = text.encode("utf-8")
assert len(text) == 2
assert len(raw) == 6
assert raw.decode("utf-8") == text

文件、网络和数据库边界应明确编码,不应默认所有字节都是 UTF-8。errors="ignore" 可能悄悄丢数据,只能在明确接受损失时使用。文本字符数量也不总等于屏幕上可见的字符簇数量,emoji 和组合字符尤其需要注意。参见Unicode 官方指南。

12. 为什么 0.1 + 0.2 != 0.3?金额怎样计算?

热度:★★★★★|难度:基础

参考答案: 常见二进制浮点数不能精确表示许多十进制小数,因此运算结果包含表示误差。科学计算可按业务容差比较;金额常使用最小货币单位整数或 Decimal,并明确精度和舍入规则。

1
2
3
4
5
6
from decimal import Decimal, ROUND_HALF_UP
import math

assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)
amount = Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
assert amount == Decimal("2.68")

从字符串构造 Decimal,避免先经过不精确的 float。Decimal 也受上下文精度、舍入和异常策略影响,并不意味着任意运算都自动符合财务规则。参见Decimal 文档。

二、常用容器:行为、复杂度与数据处理(13—24)

13. list、tuple、dict、set 如何选择?

热度:★★★★★|难度:基础

类型 主要特点 常见用途
list 有序、可变、可重复 顺序处理、动态结果集合
tuple 有序、不可变容器结构 固定结构记录、复合键候选
dict 键到值的映射,保留插入顺序 索引、配置、聚合
set 唯一成员集合,不提供业务顺序保证 去重、成员判断、集合运算

参考答案: 按访问方式和语义选类型,而不是按“哪个更快”机械选择。dict/set 的键或成员需要可哈希;元组只有成员都可哈希时才可哈希。

易错点: {} 是空字典,空集合要用 set()。set 的某次打印顺序稳定,不代表可以把它当有序序列。

14. 什么是可哈希?实现 __eq__ 后为什么可能不能做字典键?

热度:★★★★★|难度:进阶

参考答案: 可哈希对象需要在生命周期内保持稳定的哈希值,并满足“相等对象必须有相同哈希值”;相同哈希值不保证对象相等。

自定义类若实现值相等却没有合适的哈希策略,Python 通常将其设为不可哈希,以免键在容器中的行为不一致。不要让影响相等与哈希的字段在入字典后变化。

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass(frozen=True)
class UserKey:
    tenant: str
    user_id: int

lookup = {UserKey("demo", 1): "Alice"}
assert lookup[UserKey("demo", 1)] == "Alice"

这里字段本身也可哈希。frozen=True 不是递归冻结,不能据此认为任意 dataclass 都可以安全当键。

15. dict 是有序的吗?OrderedDict 还有意义吗?

热度:★★★★☆|难度:基础

参考答案: Python 3.7 起,字典保留插入顺序是语言保证;这不等于自动按键排序。更新已有键通常不改变位置,删除后重新插入会出现在末尾。

1
2
3
4
5
6
data = {"b": 1, "a": 2}
data["b"] = 3
assert list(data) == ["b", "a"]
del data["b"]
data["b"] = 4
assert list(data) == ["a", "b"]

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. 赋值、浅拷贝和深拷贝有什么区别?

热度:★★★★★|难度:基础

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import copy

original = [[1], [2]]
alias = original
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0].append(9)
assert alias is original
assert shallow is not original
assert shallow[0] == [1, 9]
assert deep[0] == [1]

参考答案: 赋值共享同一对象;浅拷贝创建新外层容器,内部引用通常共享;深拷贝递归复制支持复制的子对象,并使用 memo 处理循环和共享关系。

项目边界: deepcopy 不是通用隔离方案。连接、锁、文件等资源可能不适合复制;不可变对象可能仍复用原对象。应先明确哪些状态需要独立,再决定复制或重新构造。参见copy 文档。

18. 切片和序列乘法有哪些共享引用陷阱?

热度:★★★★★|难度:基础

1
2
3
4
5
6
7
bad = [[0] * 2] * 3
bad[0][0] = 1
assert bad == [[1, 0], [1, 0], [1, 0]]

good = [[0] * 2 for _ in range(3)]
good[0][0] = 1
assert good == [[1, 0], [0, 0], [0, 0]]

参考答案: 乘法重复的是元素引用,不是递归复制内部对象。items[:] 对普通 list 创建浅拷贝,仍不能隔离嵌套对象。

切片通常会复制所选引用并占用额外空间,不应把巨大列表的切片当成零成本视图;不同类型的切片行为可能不同,例如某些数组库会返回视图。回答时要注明对象类型。

19. 解包、*args 和 **kwargs 中的星号表示什么?

热度:★★★★☆|难度:基础

参考答案: 星号含义取决于上下文:赋值中收集剩余元素,调用中展开参数,函数定义中收集参数。** 用于关键字映射展开或收集。

1
2
3
4
5
6
7
8
first, *middle, last = [1, 2, 3, 4]
assert (first, middle, last) == (1, [2, 3], 4)

def add(a, b):
    return a + b

assert add(*[1, 2]) == 3
assert add(**{"a": 1, "b": 2}) == 3

重复传入同一个关键字会报错,不是自动覆盖。字典构造中的 {**left, **right} 则是另一种语境,右侧同名键覆盖左侧。不要把两种规则混为一谈。

20. sort 与 sorted 的区别是什么?如何多字段排序?

热度:★★★★★|难度:基础

参考答案: list.sort() 原地排序并返回 None;sorted() 接受可迭代对象并返回新列表。Python 排序稳定,相同排序键的元素保持原有相对顺序。

1
2
3
4
5
6
7
users = [
    {"name": "Bob", "score": 80},
    {"name": "Alice", "score": 90},
    {"name": "Carol", "score": 90},
]
result = sorted(users, key=lambda u: (-u["score"], u["name"]))
assert [u["name"] for u in result] == ["Alice", "Carol", "Bob"]

混合升降序可以构造合适的键,或利用稳定性分多轮排序。不要把 items = items.sort() 写成赋值,它会让 items 变成 None。排序键还应定义如何处理缺失值和类型不一致。

21. 如何去重并保持原顺序?对象不可哈希怎么办?

热度:★★★★★|难度:基础

参考答案: 可哈希值可以用 list(dict.fromkeys(values)) 保留第一次出现顺序。复杂对象可根据稳定业务键去重,同时说明保留首条还是末条。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def unique_by_id(rows):
    seen = set()
    result = []
    for row in rows:
        key = row["id"]
        if key not in seen:
            seen.add(key)
            result.append(row)
    return result

rows = [{"id": 1, "v": "first"}, {"id": 1, "v": "last"}]
assert unique_by_id(rows) == [{"id": 1, "v": "first"}]

任意转字符串作为去重键可能混淆类型或依赖不稳定表示;直接对列表做 x not in result 虽然可行,但大规模时可能退化为 O(n²)。

22. Counter、defaultdict、deque 分别适合什么场景?

热度:★★★★☆|难度:基础

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from collections import Counter, defaultdict, deque

counts = Counter(["a", "a", "b"])
assert counts["a"] == 2

groups = defaultdict(list)
for city, name in [("上海", "Alice"), ("上海", "Bob")]:
    groups[city].append(name)
assert groups["上海"] == ["Alice", "Bob"]

queue = deque([1, 2])
assert queue.popleft() == 1

参考答案: Counter 计数,defaultdict 简化分组,deque 适合双端队列,避免 list 头部删除的搬移成本。

追问: defaultdict[key] 访问缺失键会触发默认工厂,.get() 不会;deque(maxlen=n) 满时会丢弃另一端元素,不是阻塞队列。线程间通信需要同步语义时,应考虑 queue.Queue。参见collections 文档。

23. 为什么不建议遍历容器时直接增删元素?

热度:★★★★☆|难度:基础

参考答案: 修改列表可能导致索引移动、漏处理或重复处理;迭代字典时改变大小通常会抛异常。安全方法包括构造新结果、遍历快照,或在严格控制索引时原地修改。

1
2
3
4
5
6
7
8
9
values = [1, 2, 3, 4]
filtered = [value for value in values if value % 2 == 0]
assert filtered == [2, 4]

mapping = {"a": 0, "b": 1}
for key in list(mapping):
    if mapping[key] == 0:
        del mapping[key]
assert mapping == {"b": 1}

快照会占额外空间,而且浅快照不意味着内部对象不变。并发修改问题还需要锁或所有权设计,不能只靠复制键列表解决。

24. 如何从大量数据中找 Top K?什么时候不必完整排序?

热度:★★★★☆|难度:进阶

参考答案: 当 K 远小于 n 时,可以维护大小为 K 的堆,时间约 O(n log K)、额外空间约 O(K);完整排序约 O(n log n)。若 K 很大或还要完整有序结果,排序可能更简单有效。

1
2
3
4
import heapq

values = [8, 1, 9, 3, 7]
assert heapq.nlargest(3, values) == [9, 8, 7]

项目边界: 需定义并列、重复、排序方向和稳定规则。输入若已经在数据库,应考虑在数据源侧利用索引和 LIMIT,而不是先把所有记录拉进 Python。复杂度优化也要结合实际数据量和传输成本。

三、函数与协议:作用域、闭包、迭代和异常(25—36)

25. 函数参数有哪些种类?/ 和 * 的作用是什么?

热度:★★★★★|难度:基础

参考答案: 参数可以分为仅位置参数、位置或关键字参数、可变位置参数、仅关键字参数和可变关键字参数。/ 前是仅位置参数,单独 * 后或 *args 后是仅关键字参数。

1
2
3
4
5
6
def request(method, /, path, *, timeout=3, **options):
    return method, path, timeout, options

assert request("GET", "/users", timeout=2, retry=False) == (
    "GET", "/users", 2, {"retry": False}
)

这里 method 不能用关键字传,timeout 不能按位置传。合理限制传参方式可减少参数顺序错误,并为接口演进留余地;无必要时不要为了展示语法写得过度复杂。

26. 为什么默认参数不能随意写成列表或字典?

热度:★★★★★|难度:基础

参考答案: 默认值在函数定义执行时求值,不是每次调用重新创建。可变默认值可能被后续调用共享,造成跨调用污染。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def bad(value, items=[]):
    items.append(value)
    return items

assert bad(1) == [1]
assert bad(2) == [1, 2]


def good(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

assert good(1) == [1]
assert good(2) == [2]

不仅容器有问题,datetime.now() 等动态默认值也会只计算一次。若 None 是合法业务值,应使用独立哨兵。默认对象共享有时是刻意设计,但应显式说明,不要把它当隐形缓存。

27. LEGB 是什么?为什么会出现 UnboundLocalError?

热度:★★★★★|难度:进阶

参考答案: 常见名称查找依次考虑局部 Local、外层函数 Enclosing、模块 Global 和内置 Builtins。函数体内对某名称赋值,通常会让该名称被判断为局部变量,除非声明 global 或 nonlocal。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
count = 10

def broken():
    count += 1

try:
    broken()
except UnboundLocalError:
    pass
else:
    raise AssertionError("应出现局部变量读取错误")

global 改变模块级绑定,nonlocal 修改外层函数中的绑定。只调用共享列表的 append 不属于重绑定名称,因此未必需要 nonlocal。不要把 LEGB 套用成所有类体和特殊作用域都完全相同的简化规则。

28. 什么是闭包?循环中的 lambda 为什么常返回同一个值?

热度:★★★★★|难度:进阶

参考答案: 闭包使内部函数能够访问外层函数的自由变量。常见陷阱是延迟绑定:调用时读取变量当前值,而不是自动保存定义时的每次循环值。

1
2
3
4
5
6
7
8
9
def build_bad():
    return [lambda: i for i in range(3)]


def build_good():
    return [lambda i=i: i for i in range(3)]

assert [fn() for fn in build_bad()] == [2, 2, 2]
assert [fn() for fn in build_good()] == [0, 1, 2]

修复可以用默认参数绑定当前值,或使用工厂函数创建独立作用域。回调、线程任务和定时任务中同样会发生这个问题。

29. 装饰器是什么?为什么需要 functools.wraps?

热度:★★★★★|难度:进阶

参考答案: 装饰器接收可调用对象并返回替代对象。@decorator 通常等价于定义后执行 func = decorator(func),适合日志、计时、缓存等横切逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from functools import wraps


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    return wrapper


@trace
def add(a, b):
    """Return the sum."""
    return a + b

assert add(1, 2) == 3
assert add.__name__ == "add"
assert add.__wrapped__(2, 3) == 5

wraps 保留名称、文档等元信息,并设置 __wrapped__,便于内省。它不会让错误的同步包装器自动正确处理协程;异步函数通常需要异步 wrapper 并 await 原函数。

30. 带参数的装饰器如何实现?多个装饰器按什么顺序生效?

热度:★★★★☆|难度:进阶

参考答案: 带参数装饰器通常多一层工厂:先接收配置,再接收函数,最后在调用时执行业务包装。叠加时 @outer 在 @inner 外侧,相当于 outer(inner(func))。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from functools import wraps


def scale(factor):
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return factor * func(*args, **kwargs)
        return wrapper
    return decorate


@scale(3)
def amount():
    return 2

assert amount() == 6

项目追问: 缓存与认证、重试与事务的顺序可能改变安全和副作用语义。缓存若绕过身份校验,或重试包住不幂等事务,都可能引入严重问题;应先确定调用边界,再选择装饰顺序。

31. 生成器的执行时机是什么?yield 与 return 有什么区别?

热度:★★★★★|难度:进阶

参考答案: 含 yield 的普通函数调用后返回生成器,函数体通常在迭代推进时才开始执行。yield 暂停并产出值,后续推进从暂停处恢复;return 终止生成器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def numbers():
    yield 1
    yield 2
    return "done"

stream = numbers()
assert next(stream) == 1
assert next(stream) == 2
try:
    next(stream)
except StopIteration as exc:
    assert exc.value == "done"

追问: 生成器适合流式数据,但资源释放不能只依赖“以后会迭代完”。中途停止时应明确关闭或由外层上下文管理器控制资源。生成器内部也不应手动抛 StopIteration 来结束,使用 return。

32. 可迭代对象、迭代器和生成器是什么关系?

热度:★★★★★|难度:进阶

参考答案: 可迭代对象能够通过 iter() 获取迭代器;迭代器通过 next() 产生值,并在耗尽时抛 StopIteration;生成器是一种迭代器。列表可多次创建新迭代器,单个迭代器通常只能消费一次。

1
2
3
4
5
6
7
values = [1, 2]
iterator = iter(values)
assert iter(iterator) is iterator
assert next(iterator) == 1
assert list(iterator) == [2]
assert list(iterator) == []
assert list(values) == [1, 2]

实际陷阱: 把迭代器先用于日志输出,再交给业务处理,业务可能得到空结果。函数接口应说明接收一般 Iterable 还是需要可重复访问的序列,避免暗中消费输入。

33. yield from、send 与异步 await 有什么联系和区别?

热度:★★★☆☆|难度:深入

参考答案: yield from 将迭代委托给子迭代器,并能传递生成器的 send、throw、close 等协议;对子生成器,还能取得其 return 值。send 可以向暂停在 yield 处的生成器传值,首次推进通常用 next 或 send(None)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def child():
    yield 1
    return 7


def parent():
    result = yield from child()
    yield result

assert list(parent()) == [1, 7]

await 用于等待可等待对象,不能把任何普通可迭代对象随意 await。生成器与协程有历史联系,但现代 asyncio 项目应使用 async/await,不能把旧式生成器协程模板直接当成当前通用方案。

34. with 如何工作?如何编写上下文管理器?

热度:★★★★★|难度:进阶

参考答案: 上下文管理器通过 __enter__、__exit__ 管理进入和退出。成功进入后,即使代码块抛异常,也会调用退出逻辑;若进入本身失败,则不能依赖该次 __exit__ 清理未完全建立的资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from contextlib import contextmanager


@contextmanager
def temporary_append(values, item):
    values.append(item)
    try:
        yield values
    finally:
        values.pop()

items = []
with temporary_append(items, "working") as current:
    assert current == ["working"]
assert items == []

__exit__ 返回真值会抑制异常,因此不能随便 return True。多个动态资源可用 ExitStack;异步资源使用异步上下文管理器。进程强杀等情况下,退出逻辑也不保证运行。参见contextlib。

35. 异常捕获、异常链和 finally 的正确使用方式是什么?

热度:★★★★★|难度:进阶

参考答案: 尽量只捕获预期异常,并缩小 try 范围。else 在 try 正常结束后执行,finally 用于必要清理。裸 except 可能捕获退出或中断信号;一般业务异常处理使用具体异常或 Exception,但也不能无条件吞掉。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ConfigError(Exception):
    pass


def parse_count(raw):
    try:
        return int(raw)
    except (TypeError, ValueError) as exc:
        raise ConfigError("count 必须是整数") from exc

try:
    parse_count("bad")
except ConfigError as exc:
    assert isinstance(exc.__cause__, ValueError)

raise ... from exc 保留异常原因;在 except 中直接 raise 可重抛当前异常。finally 中 return 或再抛异常可能覆盖原始结果或错误,应避免。Python 3.11+ 的 ExceptionGroup 与 except* 用于分别处理一组并发异常,不等于普通 except 的简单替代。参见异常处理。

36. lru_cache 有哪些限制?缓存线程安全等于只计算一次吗?

热度:★★★★☆|难度:进阶

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from functools import lru_cache


@lru_cache(maxsize=128)
def square(value):
    return value * value

assert square(3) == 9
assert square(3) == 9
assert square.cache_info().hits == 1
square.cache_clear()

参考答案: 参数需要可哈希,缓存会持有参数和结果的引用。应避免缓存有副作用、依赖未编码外部状态或返回可变共享结果的函数。

内部数据结构线程安全,不代表同一个 key 并发首次请求只计算一次,竞争时可能重复计算。需要 single-flight 语义时要额外协调。maxsize=None 或 cache 无界增长,还需考虑内存和失效策略。参见functools 缓存说明。

四、面向对象:属性、继承与数据模型(37—48)

37. Python 的类与实例是什么关系?为什么说函数也是对象?

热度:★★★★☆|难度:基础

参考答案: 类定义对象的行为和结构约定,实例保存具体状态。类本身也是对象,可以赋值、传参或动态创建;函数同样可以作为值传递、存储和返回,这支持回调、装饰器和策略模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class User:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"hello {self.name}"

user = User("Alice")
callback = user.greet
assert callback() == "hello Alice"
assert callback.__self__ is user

从实例获取普通方法时,函数通常被绑定为方法并携带实例;因此 self 是约定名称,而实例绑定机制不是靠名称拼写触发的。

38. 类变量与实例变量有什么区别?共享可变类属性有什么坑?

热度:★★★★★|难度:基础

参考答案: 类属性通常被实例共享;实例赋值同名属性通常创建或更新自己的实例属性,但描述符可能改变行为。可变类属性若被原地修改,其他实例也能看到变化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class BadCart:
    items = []

left, right = BadCart(), BadCart()
left.items.append("book")
assert right.items == ["book"]

class Cart:
    def __init__(self):
        self.items = []

left, right = Cart(), Cart()
left.items.append("book")
assert right.items == []

实例独有的业务状态放在构造或专门初始化逻辑中;全局配置、计数等共享状态则应明确并发和生命周期约束。

39. 普通方法、classmethod 和 staticmethod 如何选择?

热度:★★★★★|难度:基础

参考答案: 普通方法绑定实例,适合访问实例状态;classmethod 绑定调用时的类,适合备用构造器和支持继承的类级操作;staticmethod 不自动绑定实例或类,适合与类语义相关的纯工具逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class User:
    def __init__(self, name):
        self.name = name

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"])

    @staticmethod
    def valid_name(name):
        return bool(name.strip())

assert User.from_dict({"name": "Alice"}).name == "Alice"
assert User.valid_name("Alice")

备用构造器返回 cls(...),有利于子类扩展;如果函数与类没明显关联,直接写模块级函数往往更简单。

40. __new__ 与 __init__ 有什么区别?单例怎么评价?

热度:★★★★☆|难度:进阶

参考答案: __new__ 创建并返回实例,__init__ 初始化已创建实例,且应返回 None。不可变类型的定制常需要在 new 阶段处理。若 new 返回的不是该类实例,后续初始化行为也会不同。

单例追问: 可以通过 new、模块级实例或其他方式控制复用,但必须说明线程竞争、重复初始化、测试隔离和多进程边界。一个进程里的单例不等于整个部署全局唯一。

项目中连接池、客户端共享常通过依赖注入或明确生命周期管理实现,比把所有东西写成隐式全局单例更容易测试。只有确有唯一性约束时再讨论单例实现,不要把它当默认设计模式。

41. 多继承的 MRO 是什么?super() 一定调用父类吗?

热度:★★★★☆|难度:进阶

参考答案: MRO 是属性和方法查找顺序,Python 使用 C3 线性化处理多继承。super() 沿当前对象的 MRO 找下一个实现,不是简单写死“调用直接父类”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class A:
    def steps(self):
        return ["A"]

class B(A):
    def steps(self):
        return ["B"] + super().steps()

class C(A):
    def steps(self):
        return ["C"] + super().steps()

class D(B, C):
    pass

assert D().steps() == ["B", "C", "A"]
assert D.__mro__[:4] == (D, B, C, A)

协作式多继承需要方法签名和 super 调用约定一致。复杂业务通常优先组合,避免通过层层继承隐藏流程。

42. property 与描述符是什么关系?属性查找优先级如何理解?

热度:★★★★☆|难度:深入

参考答案: 描述符是实现 __get__、__set__ 或 __delete__ 等协议的对象,property、普通方法绑定等都与描述符机制有关。具有设置或删除协议的是数据描述符;只有读取协议的通常是非数据描述符。

默认实例属性查找中,数据描述符优先于实例字典,实例字典优先于非数据描述符及普通类属性;继承查找还受 MRO 影响,自定义属性访问可改变行为。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Temperature:
    def __init__(self, value):
        self.celsius = value

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("低于绝对零度")
        self._celsius = value

assert Temperature(20).celsius == 20

边界: property 不是安全隔离手段;验证应避免意外触发昂贵 I/O。参见描述符指南。

43. __getattr__ 与 __getattribute__ 的区别是什么?

热度:★★★☆☆|难度:深入

参考答案: __getattribute__ 参与实例的普通属性访问;__getattr__ 一般在正常查找失败后提供兜底。覆盖 getattribute 时应通过 object.__getattribute__ 或合适的 super 访问原始属性,避免无限递归。

1
2
3
4
5
6
7
8
9
class Settings:
    def __getattr__(self, name):
        if name == "region":
            return "local"
        raise AttributeError(name)

settings = Settings()
assert settings.region == "local"
assert not hasattr(settings, "missing")

兜底不能对所有拼写错误都随意返回 None,否则 hasattr、调试和调用方错误检测都会受影响。特殊方法的隐式调用还可能绕过普通实例属性查找路径,不能假定拦截所有情况。

44. 常见魔术方法有哪些?__repr__ 与 __str__ 的区别?

热度:★★★★☆|难度:进阶

参考答案: __repr__ 提供面向开发和调试的表示,__str__ 面向用户可读展示;此外有 __len__、__iter__、__getitem__、__call__ 等协议入口,让对象接入内置语法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class User:
    def __init__(self, user_id):
        self.user_id = user_id

    def __repr__(self):
        return f"User(user_id={self.user_id!r})"

    def __str__(self):
        return f"user #{self.user_id}"

assert repr(User(1)) == "User(user_id=1)"
assert str(User(1)) == "user #1"

注意: repr 不应泄漏密码或 token,也不保证一定可被重新执行构造。运算符方法不支持某种操作数时,适当返回 NotImplemented,让 Python 尝试其他协议,而不是混淆它与 NotImplementedError。

45. dataclass 有什么价值?frozen=True 会递归冻结吗?

热度:★★★★☆|难度:进阶

参考答案: dataclass 根据字段声明生成初始化、表示、比较等方法,适合数据记录。它不自动完成业务校验或类型转换,frozen 通常只限制字段重新赋值,不会递归冻结字段指向的容器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from dataclasses import dataclass, field

@dataclass
class Order:
    order_id: int
    tags: list[str] = field(default_factory=list)

left, right = Order(1), Order(2)
left.tags.append("new")
assert right.tags == []

可变默认值用 default_factory,为每个实例构造独立对象。选择 frozen、eq、unsafe_hash 等参数前,要明确相等和哈希语义,不应只为了能放进 set 就强行生成哈希。

46. __slots__ 有什么用?一定省内存并禁止新增属性吗?

热度:★★★☆☆|难度:深入

参考答案: slots 声明实例属性槽位,在合适的类布局下可避免每个实例持有普通 __dict__,适合大量同结构对象,可能节省内存并改善属性访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(1, 2)
assert (point.x, point.y) == (1, 2)
assert not hasattr(point, "__dict__")

继承层级中若父类或子类引入 __dict__,限制会变化;弱引用还可能需要相应槽位。slots 也不代表对象不可变。是否值得使用应通过目标解释器和真实对象数量测量。

47. 鸭子类型、抽象基类和 Protocol 有什么区别?

热度:★★★★☆|难度:进阶

参考答案: 鸭子类型关注对象是否提供所需行为;ABC 可以声明抽象接口并通过继承等机制表达约束;Protocol 主要服务静态结构化类型检查,让不继承同一基类的对象也能匹配约定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from typing import Protocol

class Sender(Protocol):
    def send(self, message: str) -> str: ...

class FakeSender:
    def send(self, message: str) -> str:
        return f"sent:{message}"

def notify(sender: Sender) -> str:
    return sender.send("hello")

assert notify(FakeSender()) == "sent:hello"

类型注解本身不强制运行时校验。即使使用 runtime_checkable,检查能力也不等于全面验证方法签名和业务契约。选择接口机制应服务可替换性和可测试性。

48. 元类是什么?什么时候不需要元类?

热度:★★☆☆☆|难度:深入

参考答案: 元类控制类对象的创建,通常类的元类是 type。它可用于框架级字段收集、注册和类定义约束,但会增加理解、继承和调试成本。

如果只想在子类创建时登记信息,常可用 __init_subclass__;修改已创建类可用类装饰器;成员访问规则可用描述符。不要把简单需求都升级成元类设计。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Plugin:
    registry = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry[cls.__name__] = cls

class JsonPlugin(Plugin):
    pass

assert Plugin.registry["JsonPlugin"] is JsonPlugin

追问: 注册表持有强引用,动态大量生成类时需考虑生命周期;模块重复加载和命名冲突也应有明确策略。

五、运行时与工程基础:内存、导入和类型(49—58)

49. CPython 如何执行代码?.pyc 是机器码吗?

热度:★★★★☆|难度:进阶

参考答案: CPython 将源码解析、编译为代码对象及字节码,再执行。.pyc 通常是用于加速后续导入的字节码缓存,不是通用原生机器码,也不保证跨 Python 版本兼容。

缓存是否有效要依据对应版本的校验方式判断。删除 pycache 通常不会改变程序语义,但会失去相应导入缓存;发布时也不应把字节码当成可靠的源码保密手段。

追问: CPython、PyPy 等实现的执行和优化策略不同。讨论 JIT、字节码指令或原子性时,需要先明确实现和版本,不能把某次反汇编结果当成长期语言保证。

50. CPython 如何管理内存?引用计数为什么不够?

热度:★★★★★|难度:进阶

参考答案: CPython 常以引用计数为基础管理对象生命周期,并辅以循环垃圾回收,处理容器之间互相引用但整体不可达的情况。仅靠引用计数,循环对象可能无法降到零。

1
2
3
4
5
6
7
import gc

cycle = []
cycle.append(cycle)
assert cycle[0] is cycle
del cycle
gc.collect()  # 演示主动触发;业务代码不应默认每次请求都调用

内存分配器可能保留已释放的内存供后续使用,所以对象被回收不等于 RSS 立即下降。GC 的代际策略和自由线程实现会随版本变化,面试中应说明大方向,不要无条件背诵某个旧版阈值。参见gc 文档。

51. del 会立刻销毁对象吗?__del__ 能代替资源清理吗?

热度:★★★★☆|难度:进阶

参考答案: del name 删除名称绑定;若还有其他引用,对象继续存在。del container[key] 则调用相应删除协议,并非统一的“立即释放内存”操作。

1
2
3
4
left = [1, 2]
right = left
del left
assert right == [1, 2]

__del__ 的时机和运行环境不适合承担必须完成的业务清理,解释器退出、循环引用和异常等情况会增加复杂性。文件、锁、事务和网络连接应通过 with、try/finally 或显式关闭管理。

弱引用适合不延长对象寿命的索引或缓存,但对象类型要支持弱引用,且它也不是替代完整资源生命周期管理的万能方案。

52. sys.getsizeof 能得到整个对象图的真实内存吗?

热度:★★★☆☆|难度:进阶

参考答案: 它通常只报告对象自身占用,不递归统计所有被引用对象,且不同扩展对象的报告能力不同。列表自身大小不包含每个元素的完整独立内存。

1
2
3
4
5
import sys

items = ["x" * 1000]
assert sys.getsizeof(items) > 0
assert sys.getsizeof(items[0]) > 0

递归求和时还要避免重复计数共享对象和陷入循环。分析服务内存需区分对象分配、原生扩展、内存映射和进程 RSS;不能把某个容器的 sizeof 当作整个服务内存结论。

53. 如何定位 Python 的 CPU 与内存性能问题?

热度:★★★★☆|难度:项目应用

参考答案: 先定义可重复工作负载和指标,再区分 CPU、I/O、锁等待、内存增长与外部服务延迟。用 cProfile 等定位调用热点,微基准可用 timeit;用 tracemalloc 比较分配快照。

1
2
3
4
5
6
7
8
9
import tracemalloc

tracemalloc.start()
before = tracemalloc.take_snapshot()
items = [bytearray(128) for _ in range(1000)]
after = tracemalloc.take_snapshot()
stats = after.compare_to(before, "lineno")
assert stats
tracemalloc.stop()

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 等工具,运行时校验需要额外逻辑或库。不能把“通过类型检查”当成“外部输入已经安全”。

1
2
3
4
5
def label(value: int) -> str:
    return str(value)

assert label(1) == "1"
assert label("not-an-int") == "not-an-int"  # 运行时仍可调用,静态检查器会提示

Any 基本放宽静态检查;object 表示未知但合法对象,使用具体能力前通常需要缩小类型。滥用 Any 会让检查形同虚设。TypedDict 主要描述字典结构,不会自动检查传入 JSON 是否符合要求。参见typing 文档。

57. 前向引用和注解求值有哪些版本差异?

热度:★★★☆☆|难度:深入

参考答案: 前向引用允许引用尚未完成定义的类型;在传统求值环境中常用字符串或 from __future__ import annotations 避免定义时查找失败。

1
2
3
4
5
6
7
8
from __future__ import annotations

class Node:
    def __init__(self, value: int, next_node: Node | None = None):
        self.value = value
        self.next_node = next_node

assert Node(1).next_node is None

上例按 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,因此多线程仍可能有价值。复合的“检查后修改”、多对象约束和共享状态更新仍需要同步,不能因为字节码或某个内置操作看起来安全就推导整个流程安全。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from threading import Lock

lock = Lock()
counts = {}

def increment(key):
    with lock:
        counts[key] = counts.get(key, 0) + 1

increment("a")
assert counts["a"] == 1

易错点: 不要用某个版本的一次压力测试证明 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() 标记完成,不等于等待消费者线程自动退出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from queue import Queue
from threading import Thread

queue = Queue(maxsize=2)
results = []
STOP = object()

def consume():
    while True:
        item = queue.get()
        try:
            if item is STOP:
                return
            results.append(item * 2)
        finally:
            queue.task_done()

worker = Thread(target=consume)
worker.start()
for value in [1, 2]:
    queue.put(value)
queue.put(STOP)
queue.join()
worker.join()
assert results == [2, 4]

示例只用一个消费者。多个消费者要设计足够的停止信号、错误传递与退出协调;消费者意外死亡后,join 可能一直等,不能仅靠 finally 就认为完整系统没有死锁风险。参见Queue 文档。

63. 线程池和进程池如何取结果?超时能终止工作吗?

热度:★★★★★|难度:进阶

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from concurrent.futures import ThreadPoolExecutor, as_completed


def square(value):
    return value * value

with ThreadPoolExecutor(max_workers=2) as pool:
    futures = [pool.submit(square, value) for value in [1, 2, 3]]
    results = [future.result() for future in as_completed(futures)]
assert sorted(results) == [1, 4, 9]

参考答案: submit 返回 Future,result 获取结果并传播任务异常;as_completed 按完成顺序产出,map 通常按输入顺序产出结果。

future.result(timeout=...) 超时通常只是调用方停止等待,不会强行中断已经运行的线程任务;cancel 通常只能取消尚未开始的工作。上下文退出默认还可能等待任务完成。超时应落实到网络、数据库和业务操作本身,并设计协作停止。参见concurrent.futures。

64. multiprocessing 常见坑有哪些?默认启动方式是否一直相同?

热度:★★★★☆|难度:深入

参考答案: 子进程任务通常应定义在可导入模块顶层,传入参数和返回结果需要满足所用通信机制的序列化要求。使用主入口保护,避免子进程导入时重复启动进程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 保存为独立脚本运行
from concurrent.futures import ProcessPoolExecutor
import multiprocessing as mp


def square(value):
    return value * value


if __name__ == "__main__":
    context = mp.get_context("spawn")
    with ProcessPoolExecutor(max_workers=2, mp_context=context) as pool:
        assert list(pool.map(square, [1, 2, 3])) == [1, 4, 9]

不能假设所有平台都默认 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 提供结构化并发:退出作用域前等待子任务;一个任务出现非取消异常时,通常取消同组其他任务,并聚合相关异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Python 3.11+
import asyncio

async def fetch(value):
    await asyncio.sleep(0)
    return value * 2

async def main():
    async with asyncio.TaskGroup() as group:
        left = group.create_task(fetch(1))
        right = group.create_task(fetch(2))
    assert [left.result(), right.result()] == [2, 4]

asyncio.run(main())

gather 默认传播遇到的异常,但不会因此自动取消其他已提交任务,语义与 TaskGroup 不同。选型要明确部分成功与失败传播策略,而不是只看调用更短。参见协程与任务。

66. asyncio 取消和超时是怎样发生的?为什么不能随便吞 CancelledError?

热度:★★★★☆|难度:深入

参考答案: 取消通常在协程可响应的挂起点注入 CancelledError,是协作机制,不是任意时刻强杀。用 finally 释放资源,若捕获取消只为清理,通常应继续传播,避免破坏上层超时和 TaskGroup 协调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Python 3.11+
import asyncio

async def main():
    try:
        async with asyncio.timeout(0.01):
            await asyncio.sleep(1)
    except TimeoutError:
        return "timeout"
    raise AssertionError("预期应超时")

assert asyncio.run(main()) == "timeout"

TimeoutError 在 timeout 上下文外处理。同步阻塞代码、迟迟不让出执行的循环,可能延误超时。to_thread 取消也不保证后台线程立即停止;远程写入是否完成仍需通过业务协议确认。

67. 如何限制异步并发?Semaphore 能解决所有内存问题吗?

热度:★★★★★|难度:项目应用

参考答案: Semaphore 可限制某段临界区域同时进行的操作数量,适合控制在途 HTTP 请求;但先创建一百万个 Task,再让它们等 Semaphore,仍可能占用大量内存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import asyncio

async def main():
    semaphore = asyncio.Semaphore(2)

    async def work(value):
        async with semaphore:
            await asyncio.sleep(0)
            return value * 2

    result = await asyncio.gather(*(work(value) for value in range(4)))
    assert result == [0, 2, 4, 6]

asyncio.run(main())

上例适合少量输入。大数据量应使用有界队列加固定 worker,或分批提交,建立背压。并发上限不等于每秒请求速率,还要单独处理限流、重试预算和连接池容量。

68. contextvars 为什么比全局变量更适合保存请求上下文?

热度:★★★☆☆|难度:深入

参考答案: 全局变量容易被并发请求覆盖;threading.local 在线程内隔离,但同一线程里的多个协程仍可能混用。ContextVar 提供上下文本地值,适合 trace ID、租户标识等异步请求信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import asyncio
from contextvars import ContextVar

request_id = ContextVar("request_id", default="none")

async def handle(value):
    token = request_id.set(value)
    try:
        await asyncio.sleep(0)
        return request_id.get()
    finally:
        request_id.reset(token)

async def main():
    assert await asyncio.gather(handle("a"), handle("b")) == ["a", "b"]
    assert request_id.get() == "none"

asyncio.run(main())

任务通常在创建时复制当前上下文,后续修改不意味着自动双向同步;跨线程、进程和网络边界也应确认传播机制。不要把可变默认对象放进去后误以为其内部内容会自动隔离。参见contextvars。

69. 常见死锁与事件循环阻塞是怎么产生的?

热度:★★★★☆|难度:故障排查

参考答案: 线程死锁常来自不一致的加锁顺序、持锁等待其他线程、或线程池任务等待同池内无法调度的任务。异步程序则常因在事件循环线程执行同步 I/O、长时间 CPU 循环或错误使用阻塞锁而停滞。

预防方式包括统一锁顺序、缩小临界区、避免在锁内等待不受控调用、使用超时与任务状态诊断。同步函数需要放到合适执行器时,也要明确线程安全和取消边界。

排查顺序: 先区分 CPU 忙、阻塞系统调用、锁等待和外部依赖慢;获取线程栈或异步任务栈再判断。不能看到“没有输出”就直接归因于 GIL,也不能靠无限增加线程数解决锁竞争。

70. 自由线程 Python 会让所有多线程程序自动加速吗?

热度:★★★☆☆|难度:深入

参考答案: 不会。CPython 从 3.13 起提供可选的自由线程构建;是否启用要看解释器构建和运行状态,不能仅根据 Python 大版本判断“已经没有 GIL”。

不兼容的扩展可能使 GIL 重新启用;现有程序的共享状态也需要重新审查。内置容器的内部同步不等于多个操作构成原子业务事务,锁竞争、内存开销和任务粒度仍影响收益。

项目建议: 检查依赖兼容性,分别测试吞吐、延迟、内存与正确性,再决定迁移。它不消除数据竞争,也不替代进程隔离、异步 I/O 或合理的工作划分。参见自由线程官方说明。

七、工程必备:文件、序列化、日志与可测试性(71—80)

71. 文件读写有哪些基本原则?如何减少写到一半的损坏风险?

热度:★★★★☆|难度:工程应用

参考答案: 用 with 管理关闭,文本文件明确编码,二进制内容使用二进制模式;路径用 pathlib 组合,避免手工拼分隔符。大文件优先分块或逐行处理。

1
2
3
4
5
6
7
8
9
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as directory:
    path = Path(directory) / "result.txt"
    temporary = path.with_suffix(".tmp")
    temporary.write_text("完整结果", encoding="utf-8")
    temporary.replace(path)
    assert path.read_text(encoding="utf-8") == "完整结果"

写入同一文件系统内的临时文件,再替换目标,可减少读者看到半文件的风险,但不等于断电后持久化保证。严格持久化还涉及 flush、fsync 和目录同步等平台规则;多写者也需要唯一临时名与协调。参见pathlib。

72. JSON 与 pickle 如何选择?JSON 转换有哪些信息损失?

热度:★★★★★|难度:基础

参考答案: JSON 适合跨语言数据交换,但支持的类型有限;pickle 可序列化更多 Python 对象,却与代码、版本和运行环境耦合,反序列化不可信内容可能执行任意代码。

1
2
3
4
5
6
7
import json
from decimal import Decimal

payload = {"name": "中文", "amount": str(Decimal("12.30"))}
raw = json.dumps(payload, ensure_ascii=False, allow_nan=False)
assert json.loads(raw) == payload
assert json.loads(json.dumps({1: "value"})) == {"1": "value"}

JSON 对象键是字符串,元组通常转为数组,Decimal、日期等需要明确协议。不要让 default=str 无差别隐藏类型问题;金额字符串、时间格式和精度都要约定。解析外部 JSON 也需要限制大小和校验结构。参见JSON与pickle。

73. naive 与 aware datetime 有什么区别?测耗时用哪个时钟?

热度:★★★★★|难度:进阶

参考答案: naive datetime 缺少明确时区语义,aware datetime 携带可解释的时区信息。服务间传递时间应统一协议,通常以 UTC 保存时间点,展示时转换为业务时区。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
import time

instant = datetime(2026, 10, 24, 0, 0, tzinfo=timezone.utc)
local = instant.astimezone(ZoneInfo("Asia/Shanghai"))
assert local.hour == 8

started = time.perf_counter()
assert time.perf_counter() - started >= 0

测持续时间使用 monotonic/perf_counter,避免系统时间校准造成倒退。replace(tzinfo=...) 通常是附加或替换解释,不等于时区转换;夏令时还可能产生不存在或重复的本地时间。参见datetime与zoneinfo。

74. 为什么项目使用 logging 而不是到处 print?怎样避免重复日志?

热度:★★★★☆|难度:工程应用

参考答案: logging 支持级别、命名 logger、格式、过滤和输出通道,方便集中管理。业务模块使用 logging.getLogger(__name__),通常由应用入口统一配置 handler。

重复日志常来自重复添加 handler,或子 logger 和父 logger 都输出且 propagation 仍开启。多进程写同一滚动日志文件也可能冲突,应采用队列汇集或标准输出交由平台采集。

1
2
3
4
5
import logging

logger = logging.getLogger("orders")
logger.addHandler(logging.NullHandler())
logger.info("order_id=%s status=%s", "demo-1", "created")

日志应带 trace ID、业务关联号和耗时,但避免完整凭据和个人敏感数据。日志量也要有预算,不能为了排查把巨大响应反复同步写盘。参见Logging Cookbook。

75. Python 项目常见的输入与安全边界有哪些?

热度:★★★★☆|难度:工程应用

参考答案: 不把外部输入交给 eval/exec,不反序列化不可信 pickle,不把输入直接拼进 SQL 或 shell。SQL 使用驱动参数绑定,命令使用参数列表,并限制目标操作。

文件路径要检查是否允许访问目标范围,不能只做字符串前缀比较;压缩包解包还要防止路径穿越。解析数据时限制大小、深度和处理成本,避免资源耗尽。

面试加分点: 类型注解、转义字符串和日志脱敏分别解决不同问题,不能相互替代。配置中的地址、路径和凭据也属于信任边界,应在进入业务逻辑前进行明确验证,而不是遇错后统一捕获继续执行。

76. 如何安全地运行外部命令并处理超时?

热度:★★★★☆|难度:工程应用

1
2
3
4
5
6
7
8
import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "print('ok')"],
    capture_output=True, text=True, check=True, timeout=3,
)
assert result.stdout.strip() == "ok"

参考答案: 使用参数列表与默认 shell=False,显式设置超时、工作目录和必要环境;check=True 让非零退出码转为异常。需要流式输出时使用合适的 Popen 管理方式,避免把无限输出全部收集到内存。

边界: 超时终止直接子进程不等于终止它创建的所有后代进程。进程树清理与平台有关,要单独设计。示例 -c 内容是固定代码,不应替换成不可信输入。参见subprocess。

77. 怎样写可测试的 Python 代码?Mock 的边界在哪里?

热度:★★★★★|难度:项目应用

参考答案: 把纯业务计算与网络、数据库、时间和随机性等副作用分开;通过明确参数或接口注入外部依赖。测试核心逻辑时使用真实计算,外部交互用替身控制,再用少量集成测试验证真实协议。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from unittest.mock import Mock


def welcome(user_id, sender):
    message = f"welcome:{user_id}"
    sender.send(message)
    return message

sender = Mock()
assert welcome(1, sender) == "welcome:1"
sender.send.assert_called_once_with("welcome:1")

不要把被测函数本身替换掉后再证明其预设结果正确。patch 应作用于代码查找名称的位置;关键客户端可使用 autospec 减少签名漂移。详细 fixture 和插件机制可继续复习同目录 Pytest 笔记。参见unittest.mock。

78. Python 算法题除了写出结果,还应该说明什么?

热度:★★★★★|难度:基础到进阶

参考答案: 说明输入假设、边界、时间和空间复杂度、是否修改原数据,以及结果是否允许重复。常见题型包括去重计数、Top K、二分、滑动窗口、栈队列、链表和树遍历。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def two_sum(values, target):
    seen = {}
    for index, value in enumerate(values):
        previous = seen.get(target - value)
        if previous is not None:
            return previous, index
        seen[value] = index
    return None

assert two_sum([3, 3], 6) == (0, 1)
assert two_sum([1, 2], 9) is None

此例平均 O(n) 时间、O(n) 空间,适用于可哈希数值。不要用 if previous 判断索引是否存在,因为合法索引 0 为假;重复值也不能错误复用同一位置。

79. Python 升级时重点验证什么?只跑单元测试够吗?

热度:★★★☆☆|难度:工程设计

参考答案: 检查语法和标准库变化、依赖支持范围、C 扩展轮子、序列化与反射行为、进程启动方式,以及性能和内存差异。对反射注解、异步调度、原生扩展依赖重的项目尤其需要兼容性验证。

建立目标版本测试矩阵,执行单元、集成和关键端到端路径;对真实负载做基准对比,审查弃用警告。先在隔离环境和小范围流量中验证,准备可执行的回退方式。

易错点: “代码能 import”不代表行为完全一致;“新版更快”也不保证你的工作负载更快。不要同时升级解释器、框架和所有业务依赖而又缺少定位手段。

80. 配置和密钥如何管理?为什么不建议散落在全局变量中?

热度:★★★★☆|难度:工程设计

参考答案: 在应用入口按明确优先级解析配置,验证类型和必填项,再把结构化配置注入组件。普通非敏感配置可版本管理,密钥通过受控环境或密钥管理系统提供。

不要在每个模块导入时分别读取并缓存环境变量,否则测试修改环境后可能不生效,也难追踪实际使用值。配置对象应尽量保持稳定,不要让任意业务函数随意修改全局环境。

项目追问: 配置错误应尽早暴露,日志显示环境和非敏感摘要即可;连接字符串、认证头和私钥不能因调试方便进入日志或异常表示。动态配置还需说明版本、一致性和回滚策略。

八、实际项目场景:从脚本到稳定服务(81—90)

81. 处理几十 GB 日志或 CSV,怎样避免一次读爆内存?

热度:★★★★★|难度:项目设计

场景: 每天扫描大量日志,按用户和错误类型输出统计,机器内存有限。

参考答案: 采用流式读取和逐条解析,按批次写入结果;CSV 使用 csv 模块,不直接按逗号 split,因为字段可能带引号、逗号和换行。输入、处理、输出都保持有界,避免最后又 list 化全部结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import csv
import io
from collections import Counter

source = io.StringIO('user,status\nAlice,ok\nBob,error\nAlice,error\n')
counts = Counter()
for row in csv.DictReader(source):
    if row["status"] == "error":
        counts[row["user"]] += 1
assert counts == {"Bob": 1, "Alice": 1}

深入追问: 如果用户数量也很大,Counter 仍会持续增长;可用外部排序、分区聚合或数据库聚合。还要设计坏行处理、文件编码、超长单行、断点续跑和重复输入识别。生成器只能减少部分内存,并不自动解决所有聚合状态。

82. 一百万条记录需要调用接口,如何控制吞吐、内存和失败?

热度:★★★★★|难度:项目设计

参考答案: 不一次创建一百万个任务。使用流式生产者、有界队列、固定 worker 和受控写出,分别设置并发、速率、超时、重试预算和连接池。下例用本地计算模拟 I/O,展示有限在途工作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Python 3.11+;示例不访问外部接口
import asyncio

async def process_all(values):
    queue = asyncio.Queue(maxsize=6)
    stop = object()
    output = []
    worker_count = 3

    async def produce():
        for value in values:
            await queue.put(value)
        for _ in range(worker_count):
            await queue.put(stop)

    async def consume():
        while True:
            value = await queue.get()
            try:
                if value is stop:
                    return
                await asyncio.sleep(0)
                output.append(value * 2)
            finally:
                queue.task_done()

    async with asyncio.TaskGroup() as group:
        group.create_task(produce())
        for _ in range(worker_count):
            group.create_task(consume())
    return output

assert sorted(asyncio.run(process_all(range(10)))) == list(range(0, 20, 2))

边界: 示例 output 仍是内存列表,大规模时要替换为有界批量写出。本例采用任务失败即取消整组的策略;若要部分成功,需逐条记录可重试与永久失败,并处理取消和退出,不能简单吞异常。输出顺序也不保证与输入相同,可附序号后有界重排。

83. 网络不稳定,如何设计有限重试并避免重复写入?

热度:★★★★★|难度:项目设计

参考答案: 明确哪些错误可重试,限制次数和总时间预算,使用指数退避及适当抖动,并尊重服务端限流指引。写请求超时后,服务端可能已经完成,必须结合幂等键、状态查询或补偿机制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import time
from unittest.mock import Mock


def retry(operation, *, attempts=3, sleep=time.sleep):
    if attempts < 1:
        raise ValueError("attempts must be positive")
    for index in range(attempts):
        try:
            return operation()
        except TimeoutError:
            if index == attempts - 1:
                raise
            sleep(min(0.1 * (2 ** index), 1.0))

operation = Mock(side_effect=[TimeoutError("temporary"), "ok"])
assert retry(operation, sleep=lambda _: None) == "ok"
assert operation.call_count == 2

这是精简策略示例,不含总截止时间、抖动和真实 HTTP 异常分类。实际客户端还要对单次调用设置超时,防止一次调用永久阻塞。不要对认证错误、参数错误和业务拒绝统一重试;也不要让多层重试相乘形成重试风暴。

84. 服务运行几天后内存越来越高,怎样排查?

热度:★★★★★|难度:故障排查

参考答案: 先确认增长是持续、台阶还是正常缓存预热,区分 RSS 与 Python 可追踪分配。固定工作负载比较内存快照,定位对象类型和引用链,检查无界缓存、全局列表、回调闭包、未结束任务和批量结果。

若 tracemalloc 增长不明显而 RSS 持续增长,检查原生扩展、内存映射和分配器保留,不应直接认定“GC 失效”。观察 GC 统计可以辅助,但反复 gc.collect() 通常不能修复仍被引用的数据。

项目闭环: 加入缓存容量和任务数量指标,建立可复现的长时间或批次压力测试。修复后验证增长曲线、吞吐和延迟,而不是只看服务重启后内存下降。

85. 接口平均响应突然变慢,如何判断该优化 Python 代码还是数据库?

热度:★★★★★|难度:故障排查

参考答案: 先看分位延迟和分阶段耗时:入口排队、认证、业务 CPU、数据库、远程依赖和序列化。平均值可能掩盖少量长尾请求,应比较相同请求类型与负载。

CPU 高时检查算法、重复计算和序列化热点;CPU 不高但延迟高时,重点看连接池等待、慢查询、锁竞争、外部超时与事件循环阻塞。数据库问题还需检查查询计划、索引和 N+1 请求,而不只是换更快的 JSON 库。

取舍: 缓存要定义一致性和失效策略,扩线程要考虑下游容量,异步改造要确认依赖链是否真的非阻塞。每项优化都用相同负载验证收益和副作用。

86. Python 操作数据库,怎样保证事务边界和避免“部分成功”?

热度:★★★★★|难度:项目设计

参考答案: 将同一数据库内必须共同成功的操作放在明确事务中,失败回滚,成功提交;SQL 使用参数绑定,避免字符串拼接。连接和事务对象按驱动约定管理,不假设可跨线程或进程共享。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import sqlite3

connection = sqlite3.connect(":memory:")
try:
    connection.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY)")
    try:
        with connection:
            connection.execute("INSERT INTO orders(id) VALUES (?)", (1,))
            raise ValueError("模拟业务失败")
    except ValueError:
        pass
    assert connection.execute("SELECT COUNT(*) FROM orders").fetchone()[0] == 0
finally:
    connection.close()

示例依赖 sqlite3 此连接配置下的事务行为,不能推导所有驱动的 with 都会自动回滚和关闭。数据库与消息队列、第三方支付之间通常没有单一原子事务,可考虑事务消息表、幂等消费和补偿;“套一个 try/except”无法解决分布式一致性。

87. 清洗外部 JSON 数据时,缺失、空值、零和类型错误如何处理?

热度:★★★★★|难度:项目应用

参考答案: 在边界层做结构和类型校验,区分字段不存在、显式 null、空字符串、零和非法类型;业务层接收已规范化数据。不能用 value or default 把所有假值混为一谈。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
MISSING = object()


def parse_limit(payload):
    value = payload.get("limit", MISSING)
    if value is MISSING:
        return 100
    # bool 是 int 的子类;本接口明确不接受布尔值。
    if type(value) is not int or value < 0:
        raise ValueError("limit 必须是非负整数")
    return value

assert parse_limit({}) == 100
assert parse_limit({"limit": 0}) == 0
for invalid in [None, "10", True, -1]:
    try:
        parse_limit({"limit": invalid})
    except ValueError:
        pass
    else:
        raise AssertionError("非法输入未被拒绝")

是否接受数字字符串要由协议定义;自动强转可能接受不该接受的输入。校验失败应带可定位的字段信息,但避免将完整敏感请求直接写进错误日志。

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 能讲故障证据、恢复策略和方案取舍

建议亲手完成以下练习:

  1. 用一个例子同时解释赋值、浅拷贝、深拷贝和可变默认值。
  2. 写保序去重、Top K 和两数之和,说明边界与复杂度。
  3. 写一个保留元信息的装饰器,再改成能正确等待异步函数的版本。
  4. 写生成器和上下文管理器,模拟中途失败,检查资源是否清理。
  5. 用两线程修改共享状态,再加入显式锁,解释正确性依据。
  6. 写有界队列的异步工作池,测试单任务失败、取消、超时和输出背压。
  7. 模拟网络超时后服务端已经成功,设计幂等查询与安全重放。
  8. 用 tracemalloc 比较多轮处理前后快照,排查一个无界缓存案例。
  9. 给真实项目写一段第 90 题的回答,准备能支撑结论的日志或数据。

复习取舍: 优先掌握五星题和真实项目中用到的知识。元类、描述符等可以逐步深入,但对象共享、异常处理、数据隔离和并发边界不能只停留在术语层面。

十一、官方资料索引

以下链接用于核对语义和版本差异。多数固定到 Python 3.14 文档,便于避免跟随默认文档版本变化;面试热度和项目取舍属于本文的经验整理。

comments powered by Disqus
使用 Hugo 构建
主题 Stack 由 Jimmy 设计