实战篇 / 第 26 章
第 26 章 · 面试题精选与学习路线
本章导读:教程的最后一章不是「新知识」,而是「把学过的东西换成 offer」。我们按面试官真实提问的方式,把语言基础、数据结构、并发、数据库、Web、测试工程化五个方向的高频题逐条拆解:先给一句话结论,再给原理与最小可运行代码,最后给面试官真正想听的「加分项」与常见错误回答。本章后半部分给出一份三个月可执行的学习路线图、每日时间分配表、项目清单与资料推荐,帮助你把 26 章的内容转化为可验证的能力。
先看这一节:面试的底层逻辑与答题框架
技术面试本质上是信息不对称下的信任建立过程。面试官在有限的一小时里要回答一个问题:「这个候选人进来后,能不能独立把事情做对?」他无法验证你十年经验,只能靠几个精心设计的问题去探测你的深度、广度与边界感。
面试官真正在考什么
| 考察维度 | 面试官想听到什么 | 什么回答会直接减分 |
| 原理深度 | 能说清机制、边界条件、为什么这样设计 | 只会背结论,问「为什么」就卡壳 |
| 取舍意识 | 知道方案的代价,没有银弹 | 任何问题都答「用 X 就行」 |
| 工程直觉 | 遇事先定位再动手,考虑监控与回滚 | 上来就改代码不考虑线上影响 |
| 表达结构 | 先结论、再展开、最后给方案 | 想到哪说到哪,面试官跟不上 |
| 诚实度 | 不会就说「没做过,我的思路是……」 | 硬编,细节一问就崩 |
万能答题框架:STAR + FIVE
# 结构化表达模板(口述时在心里过一遍,不要背台词)
【1. 复述确认】 "我理解您想问的是 X,对吗?"
【2. 一句话结论】 "简单说,A 比 B 快,因为 X。"
【3. 分点展开】 "这里有三个关键点:
第一,……(机制/边界)
第二,……(实践中的坑)
第三,……(我实际怎么用的)"
【4. 反向验证】 "如果要我反过来选 B,条件是……"
【5. 主动补刀】 "还有个相关的问题您可能想知道:……"
# 遇到不会的问题
"这个我没在实际项目里用过。但按我的理解,它的核心是 X。
如果让我实现,我会先……再…… 最后用…… 验证。
我不确定的地方是 Y,您能提示一下方向吗?"
# 面试官问「你没做过的技术」
"这块我了解得比较浅。我目前的理解是 X。
在上一个项目里,我用的是 Z,因为 Y。
如果团队要用 X,我需要先补的是 ……"
要点面试中敢说「我不确定,但我可以推理」,远好过硬编。资深工程师与初级工程师最明显的区别,不是知道多少,而是面对未知时的推理路径是否清晰。
面试前的知识查漏清单
- 把你的简历上每一项技术都准备一个「我做了什么、遇到什么问题、怎么解决的」三句话版本。
- 简历上写的框架,源码至少读过其入口、核心类、请求链路三处。
- 准备 2 个你写砸过的项目复盘,这比 2 个成功案例更能体现能力。
- 手边有可运行的 demo 或可讲的技术方案,比抽象描述有说服力得多。
语言基础高频题
这一组的共同特点:概念不难,但极易答得浅。能说出「是什么」只是及格,能说出「为什么这样设计、代价是什么」才是优秀。
1. 列表推导式、生成器表达式、集合推导式有什么区别?
结论:三者语法结构相同,区别在于结果的存储方式——列表推导生成一个真实 list,集合推导生成 set(去重),生成器表达式生成一个惰性求值的迭代器。
对比实验
import sys
import time
# 列表推导:立刻全部算完,全部存进内存
t0 = time.perf_counter()
squares_list = [x * x for x in range(1_000_000)]
print(f"列表推导 {time.perf_counter()-t0:.3f}s,占用 {sys.getsizeof(squares_list)/1024/1024:.1f} MB")
# 输出:列表推导 0.048s,占用 36.8 MB
# 生成器表达式:算一个用一个,不占内存
t0 = time.perf_counter()
squares_gen = (x * x for x in range(1_000_000))
print(f"生成器创建 {time.perf_counter()-t0:.6f}s,占用 {sys.getsizeof(squares_gen)} 字节")
# 输出:生成器创建 0.000002s,占用 48 字节
# 集合推导:去重 + 可快速查找
names = ["Bob", "bob", "BOB", "alice"]
print({n.lower() for n in names}) # {'bob', 'alice'}
# 关键区别:能否复用
gen = (i for i in range(3))
print(list(gen), list(gen))
# 输出:[0, 1, 2] [] —— 生成器是一次性的,第二次拿到空列表
print(list(squares_list)[:3], list(squares_list)[:3])
# 输出:[1, 4, 9] [1, 4, 9] —— 列表可以反复遍历
要点面试加分说法:「生成器的价值不只是省内存,更是流式处理。处理一个 10 GB 的日志文件时,用列表推导会直接 OOM,用生成器配合 csv.reader 可以做到常量内存流式处理,这才是它真正的工程价值。」
2. 可变默认参数为什么是经典陷阱?
结论:默认参数在函数定义时求值一次并绑定到函数对象上,之后所有调用共享同一个对象。
# 经典错误
def bad_append(item, bucket=[]): # 坑在 [] 只在定义时创建一次
bucket.append(item)
return bucket
print(bad_append(1)) # [1]
print(bad_append(2)) # [1, 2] ← 上次的结果还在!
print(bad_append(3)) # [1, 2, 3]
# 正确写法:用 None 做哨兵
def good_append(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(good_append(1)) # [1]
print(good_append(2)) # [2] ← 互不影响
# 更 Pythonic 的写法:用不可变默认 + *args 解包
def collect(item, *items, **kwargs):
return [item, *items]
# 底层原理:默认值保存在函数对象的 __defaults__ 中
import inspect
print(inspect.signature(bad_append))
# 输出:(item, bucket=[])
print(bad_append.__defaults__)
# 输出:([],) ← 全局共享的就是这一个列表对象
# 衍生考点:闭包中的循环变量
fns = [lambda: i for i in range(3)]
print([f() for f in fns]) # [2, 2, 2] ← 都是最后一个 i
fns2 = [lambda i=i: i for i in range(3)] # 默认参数绑定,逃逸技巧
print([f() for f in fns2]) # [0, 1, 2]
常见坑面试时如果说「因为 Python 会复制一份默认值」,是错的。准确表述是「默认值在函数定义时求值一次,并作为函数对象的属性保存,每次调用都复用它」。这个精度差异,资深面试官立刻能分辨。
3. 深拷贝与浅拷贝的区别?
结论:浅拷贝只复制外层容器,内部对象仍是同一份引用;深拷贝递归复制所有层级,循环引用也能正确处理。
import copy
original = [[1, 2], [3, 4]]
# 赋值:连外层容器都是同一个对象
a = original
a[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] ← 全被改了
# 浅拷贝:新外层,内层共享
original = [[1, 2], [3, 4]]
b = copy.copy(original)
b[0].append(99)
b.append([5])
print(original) # [[1, 2, 99], [3, 4]] ← 内层被改
print(b) # [[1, 2, 99], [3, 4], [5]]
# 深拷贝:全部复制
original = [[1, 2], [3, 4]]
c = copy.deepcopy(original)
c[0].append(99)
print(original) # [[1, 2], [3, 4]] ← 完全不受影响
# 深拷贝处理循环引用(这是它比手写递归强的地方)
lst = [1, 2]
lst.append(lst) # 自己引用自己
cloned = copy.deepcopy(lst)
print(cloned[2] is cloned) # True —— 结构被正确重建
# copy 模块的完整规则表
class A:
pass
print(copy.copy(1)) # 不可变类型直接返回原对象(1)
print(copy.copy("ab")) # 'ab' ← 不是列表的 copy 方法!
print(copy.copy((1, 2))) # 元组不可变,返回自身
d = {"x": [1]}
print(copy.copy(d)["x"] is d["x"]) # True 浅:值共享
print(copy.deepcopy(d)["x"] is d["x"]) # False 深:值也复制
# 自定义类如何定义拷贝行为
import copyreg
class Resource:
def __init__(self, handle):
self.handle = handle # 文件句柄等不可拷贝的资源
def __copy__(self):
return Resource(handle="shared-handle")
def __deepcopy__(self, memo):
return Resource(handle="independent-handle")
r = Resource("f1")
print(copy.deepcopy(r).handle) # independent-handle
要点补充:copy.copy 对 list/dict/set 等可变容器会创建新对象;对 int/str/tuple 等不可变对象直接返回自身引用(因为不可变,共享无害)。因此不能写 x = copy.copy(s) 来复制字符串。
4. Python 的 GIL 是什么?它被取消了吗?
结论:GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器级别的互斥锁,保证同一时刻只有一个线程在执行字节码。因此多线程无法利用多核做 CPU 并行;但它不是语言的语义规定,只是 CPython 的实现细节,且从 Python 3.13 开始可以在部分平台编译成「自由线程(free-threaded)」版本,3.14 起逐步默认。
import multiprocessing as mp
import threading
import time
def cpu_work(n: int) -> int:
total = 0
for i in range(n):
total += i * i
return total
N = 8_000_000
def bench() -> None:
t0 = time.perf_counter()
threads = [threading.Thread(target=cpu_work, args=(N,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
t_thread = time.perf_counter() - t0
t0 = time.perf_counter()
with mp.Pool(4) as pool:
pool.map(cpu_work, [N] * 4)
t_proc = time.perf_counter() - t0
print(f"4 线程(CPU 密集): {t_thread:.2f}s")
print(f"4 进程(CPU 密集): {t_proc:.2f}s")
print(f"进程/线程 加速比: {t_thread / t_proc:.1f}x")
if __name__ == "__main__":
bench()
# 典型输出:
# 4 线程(CPU 密集): 2.60s
# 4 进程(CPU 密集): 0.72s
# 进程/线程 加速比: 3.6x ← 线程几乎没有加速,因为 GIL 串行化了
# 反过来:IO 密集型任务,多线程非常有效
import urllib.request
def fetch(url: str) -> int:
with urllib.request.urlopen(url, timeout=3) as r:
return len(r.read())
# 20 个 URL 并发下载:多线程约 0.6s,串行约 12s
# 因为真正的耗时在网络等待,GIL 在等待期间是释放的
常见坑错误回答一:「Python 是单线程语言,不能并发」——错,多线程完全可以,只是 CPU 密集时无法并行。错误回答二:「GIL 锁的是线程」——错,锁的是解释器执行字节码这一步,IO 期间会释放。加分回答:「过去替代 GIL 的方案有 Jython(真并行但生态不全)、IronPython、pypy 的 gil-less 分支、以及 2023 年的 subinterpreter 提案,都没能成为主流,原因是 CPython 生态与 C 扩展的兼容包袱太重。」
5. 装饰器的执行时机与本质
结论装饰器是「函数包装函数」。@log 修饰 def f(): 等价于 f = log(f),在定义时执行一次,此后调用 f() 实际调用的是包装后的函数。
import functools
import time
# 1) 最简装饰器
def log_call(fn):
def wrapper(*args, **kwargs):
print(f"[调用] {fn.__name__} args={args}")
result = fn(*args, **kwargs)
print(f"[返回] {fn.__name__} -> {result}")
return result
return wrapper
# 2) 带参数与返回值的通用装饰器(用 *args 接收任意配置)
def retry(times: int = 3, delay: float = 0.1, exceptions=(Exception,)):
def deco(fn):
@functools.wraps(fn) # 保留原函数元信息
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, times + 1):
try:
return fn(*args, **kwargs)
except exceptions as exc:
last_exc = exc
print(f"[重试] 第 {attempt}/{times} 次失败: {exc}")
if attempt < times:
time.sleep(delay * attempt) # 指数退避
raise last_exc
return wrapper
return deco
# 3) 组合使用:装饰器自下而上应用
calls = []
def count(fn):
@functools.wraps(fn)
def w(*a, **k):
calls.append(fn.__name__)
return fn(*a, **k)
return w
@count
@retry(times=2)
def unstable(x: int) -> int:
if x % 2 == 1:
raise ValueError("随机失败")
return x * 2
print(unstable(2)) # 4
print(unstable(3)) # 重试后抛 ValueError
print(calls) # ['unstable', 'unstable']
# 4) functools.wraps 为什么必须写
print(unstable.__name__) # 'unstable' ← 没有 wraps 就是 'wrapper'
print(unstable.__doc__) # 原始 docstring 被保留
# 5) 类装饰器:可以给类加属性与方法
import functools
def add_repr(cls):
cls.repr_short = lambda self: f"<{self.__class__.__name__} {self.id}>"
return cls
@add_repr
class User:
def __init__(self, uid):
self.id = uid
print(User(7).repr_short()) # <User 7>
# 6) 参数装饰器的执行顺序验证
@print_msg("进入")
@print_msg("离开")
def demo():
print("函数体")
# 定义阶段:demo 先被装饰一次
# 调用阶段:进入 -> 函数体 -> 离开(由内向外)
6. 迭代器与生成器
结论迭代器实现了 __iter__ 与 __next__,是「一次一步」的对象;生成器是「用 yield 快速写出的迭代器」,每次 yield 会把状态挂起,下次 next() 从挂起点恢复。
# 1) 自己实现一个迭代器协议
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # 返回自身即为迭代器
def __next__(self):
if self.current <= 0:
raise StopIteration # 必须抛这个异常来结束
self.current -= 1
return self.current + 1
print(list(Countdown(3))) # [3, 2, 1]
# 2) 生成器:yield 让函数变成「状态机」
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print([next(fib_gen) for _ in range(8)] if (fib_gen := fib()) else [])
# 输出:[0, 1, 1, 2, 3, 5, 8, 13]
# 3) send:双向通信,把值送进生成器内部
def echo():
while True:
received = yield # yield 表达式先返回一个值,等下次 send
print("收到:", received)
g = echo()
next(g) # 启动生成器,停在第一个 yield
g.send("你好") # 打印「收到: 你好」
# 4) 实战:分块读取大文件(生成器最典型的工程用途)
def read_in_chunks(path: str, size: int = 1024 * 64):
with open(path, "r", encoding="utf-8") as f:
while chunk := f.read(size): # 海象运算符,3.8+
yield chunk
# 5) 生成器实现内存友好的 CSV 统计
import csv
from collections import Counter
def count_words(path: str):
"""逐行流式统计,返回 Counter。10GB 文件也是常量内存。"""
counter: Counter[str] = Counter()
with open(path, encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
counter.update(row["title"].lower().split())
return counter
# 6) 陷阱:生成器只能消费一次
gen = (i for i in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] ← 第二次为空
# 需要复用就转成 list 或 tuple,但会失去惰性优势
# 7) 双向链表 / 树的深度优先遍历常用生成器
def walk(node):
yield node
for child in node.children:
yield from walk(child) # 委托给子生成器
要点加分补充:yield from 比手写 for 循环委托更快,因为 C 层实现;contextlib.contextmanager 底层就是生成器;async def 中 await 一个异步生成器可以拿到 __anext__,这是实现异步流式接口的关键。
7. 面向对象:多继承、MRO 与魔术方法
结论Python 的 MRO(Method Resolution Order,方法解析顺序)遵循 C3 线性化算法,保证方法查找顺序既满足继承声明顺序,又在多继承下保持单调性(子类不会在父类之前)。
# 1) MRO 顺序
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__)
# 输出:(D, B, C, A, object) —— B 在 C 前,因为 D 的声明顺序是 (B, C)
# 2) 菱形继承与 super() 的正确用法
class Logger:
def __init__(self, name):
self.name = name
print(f"[Logger] 初始化 {name}")
def report(self):
print(f"[Logger] {self.name} 的日志")
class Database:
def __init__(self, dsn):
self.dsn = dsn
print(f"[Database] 连接 {dsn}")
class Service(Database, Logger): # 多继承混合能力
def __init__(self, name, dsn):
super().__init__(name) # 只写一次,交给 MRO 决定谁先执行
self.dsn = dsn
def report(self):
super().report() # 链式调用所有父类实现
print(f"[Service] 组合完成 dsn={self.dsn}")
Service("订单服务", "postgres://localhost/shop")
# 输出:
# [Logger] 初始化 订单服务 ← Logger 先跑(MRO 决定)
# [Service] 组合完成 dsn=postgres://localhost/shop
print(Service.__mro__)
# (Service, Database, Logger, object)
# 3) 抽象基类:强制子类实现
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
"""所有子类必须实现"""
@abstractmethod
def refund(self, txn_id: str) -> bool: ...
def describe(self) -> str:
return f"支付网关: {type(self).__name__}"
# PaymentGateway() # TypeError: Can't instantiate abstract class
class AlipayGateway(PaymentGateway):
def pay(self, amount: float) -> str:
return f"alipay_{amount}"
def refund(self, txn_id: str) -> bool:
return True
print(AlipayGateway().describe()) # 支付网关: AlipayGateway
# 4) 常用魔术方法对照
class Money:
def __init__(self, amount: float):
self.amount = amount
def __repr__(self) -> str: # 调试用,无歧义
return f"Money({self.amount})"
def __str__(self) -> str: # 给人看
return f"¥{self.amount:.2f}"
def __eq__(self, other) -> bool:
return isinstance(other, Money) and self.amount == other.amount
def __hash__(self) -> int: # 定义 __eq__ 后必须同时定义
return hash(self.amount) # 否则对象不可放入 set/dict
def __lt__(self, other) -> bool:
return self.amount < other.amount
def __add__(self, other) -> "Money":
return Money(self.amount + other.amount)
def __bool__(self) -> bool:
return self.amount > 0
def __len__(self) -> int:
return len(str(self.amount))
def __getitem__(self, idx) -> str:
return str(self)[idx]
def __enter__(self) -> "Money":
print("打开事务")
return self
def __exit__(self, exc_type, exc, tb) -> None:
print("提交或回滚事务")
return False # 返回 True 会吞掉异常
m = Money(19.9)
print(repr(m), str(m)) # Money(19.9) ¥19.90
print(m + Money(0.1)) # Money(20.0)
print(bool(Money(0))) # False
print(m < Money(100)) # True
print(sorted([Money(3), Money(1)])) # [Money(1), Money(3)] 依赖 __lt__
# 5) dataclass:现代 Python 写值对象的推荐方式
from dataclasses import dataclass, field, asdict
from typing import Optional
@dataclass(frozen=True, slots=True, order=True) # 不可变 + 省内存 + 可比较
class User:
uid: int
name: str
email: Optional[str] = None
tags: list[str] = field(default_factory=list)
u = User(1, "张三")
print(asdict(u)) # {'uid': 1, 'name': '张三', ...}
print(u == User(1, "张三")) # True 自动生成 __eq__
print(User(1, "a") < User(2, "b")) # True order=True 生成比较方法
try:
u.uid = 99 # frozen=True 抛异常
except Exception as e:
print(type(e).__name__) # FrozenInstanceError
常见坑面试高频陷阱:定义 __eq__ 后不定义 __hash__,会把对象的 __hash__ 置为 None,导致 obj in some_set 直接抛 TypeError: unhashable type。规则是:定义了 __eq__ 的可变类必须显式 __hash__ = None,不可变类才定义 __hash__。
数据结构与算法高频题
1. list、tuple、set、dict 的底层与复杂度
| 类型 | 底层结构 | 查找 | 插入/删除 | 随机访问 | 有序 | 可哈希 |
| list | 动态数组(连续内存 + 容量缓冲) | O(n) | 尾部均摊 O(1),头部 O(n) | O(1) | 是 | 否 |
| tuple | 不可变动态数组 | O(n) | 不允许 | O(1) | 是 | 是 |
| set | 哈希表(开放寻址) | O(1) 均摊 | O(1) 均摊 | 否 | 否 | 否 |
| dict | 哈希表 + 开放寻址(3.6 起保序) | O(1) 均摊 | O(1) 均摊 | 否 | 是(3.7+ 保证) | 否 |
| frozenset | 不可变 set | O(1) 均摊 | 不允许 | 否 | 否 | 是 |
| deque | 双端队列(分块链表) | O(n) | 两端均摊 O(1) | 两端 O(1) | 是 | 否 |
import sys
from collections import deque
# 1) list 的摊还扩容原理:容量按 1.125 倍增长
a = []
last_size = sys.getsizeof(a) - 56 # 减去对象头
for i in range(1000):
a.append(i)
cur = sys.getsizeof(a) - 56
if cur != last_size:
print(f"len={len(a):4d} 容量={cur//8:4d}")
last_size = cur
# 输出(节选):
# len= 5 容量= 8
# len= 9 容量= 16
# len= 17 容量= 32
# 观察:扩容不频繁,因此 append 均摊 O(1);但单次扩容是 O(n) 的拷贝
# 2) 切片是 O(n) 且产生新对象,不是视图
xs = list(range(1_000_000))
t0 = __import__("time").perf_counter()
for _ in range(100):
_ = xs[::2] # 每次都完整复制
print(f"切片 100 次: {__import__('time').perf_counter()-t0:.3f}s")
# 3) deque 的正确用法:队列 / 栈 / 滑动窗口
q = deque(maxlen=5) # maxlen 自动淘汰最旧的,空间有界
for i in range(10):
q.append(i)
print(list(q)) # [5, 6, 7, 8, 9]
# 滑动窗口最大值(单调队列),O(n)
def max_in_window(nums: list[int], k: int) -> list[int]:
dq = deque() # 存下标,保持值单调递减
out = []
for i, v in enumerate(nums):
while dq and nums[dq[-1]] <= v:
dq.pop() # 队尾不可能是最大值了
dq.append(i)
if dq[0] <= i - k:
dq.popleft() # 队首滑出窗口
if i >= k - 1:
out.append(nums[dq[0]])
return out
print(max_in_window([1, 3, -1, -3, 5, 3, 6, 7], 3)) # [3, 3, 5, 5, 6, 7]
# 4) dict 的三个实用技巧
from collections import defaultdict, Counter, OrderedDict
dd = defaultdict(list)
for word in ["a", "b", "a"]:
dd[word].append(1) # 不必先判断 key 是否存在
print(dict(dd)) # {'a': [1, 1], 'b': [1]}
cnt = Counter("mississippi") # 计数神器
print(cnt.most_common(2)) # [('i', 4), ('s', 4)]
od = OrderedDict([("b", 1), ("a", 2)])
od.move_to_end("b") # 普通 dict 做不到
print(list(od)) # ['a', 'b']
# 5) 分组计数:dict + lambda 一步到位
data = [("北京", 18), ("上海", 15), ("北京", 20), ("上海", 12)]
grouped = {}
for city, age in data:
grouped.setdefault(city, []).append(age)
print({k: sum(v) / len(v) for k, v in grouped.items()})
# {'北京': 19.0, '上海': 13.5}
要点面试加分:能说出 CPython 3.6 起 dict 改为「紧凑布局 + 插入顺序保持」,3.7 把它写进语言规范,说明你关注过版本演进。另外 hash 表的「开放寻址」(linear probing)比「拉链法」在 CPython 中更常用,因为缓存局部性更好,但删除时需要墓碑标记(dummy slot),这是 dict 扩容成本高的原因之一。
2. 列表推导 vs 生成器 vs map/filter 的选型
| 写法 | 何时用 | 性能特点 |
列表推导 [f(x) for x in it] | 需要立即全部结果、要重复遍历、要索引 | 最快(内置循环,比 for 快 20%~40%) |
生成器 (f(x) for x in it) | 数据大、只遍历一次、流式处理 | 最低内存,可提前退出(break) |
map(f, it) | 已有现成函数、要并行映射 | 与生成器同性能;map 是惰性的 |
filter(pred, it) | 已有判定函数 | 惰性;返回可迭代对象不是 bool |
zip(a, b, strict=True) | 多序列并行遍历 | strict=True(3.10+)在长度不等时抛异常 |
# map/filter 是惰性的,返回迭代器
m = map(str, [1, 2, 3])
print(type(m)) # <class 'map'>
print(list(m)) # ['1', '2', '3']
print(list(m)) # [] 一次性
# map 配合 zip 做并行处理
prices = [100.5, 20.0, 3.5]
rates = [0.9, 0.8, 1.0]
final = [round(p * r, 2) for p, r in zip(prices, rates, strict=True)]
print(final) # [90.45, 16.0, 3.5]
# 生成器的提前退出优势:list 推导必须全部算完
def expensive(x):
print(f"计算 {x}")
return x * 2
squares = [expensive(x) for x in range(100)] # 全部 100 个都会打印
print(len(squares))
# 输出:计算 0 ... 计算 99 然后才得到结果
g = (expensive(x) for x in range(100)) # 惰性
for i, v in enumerate(g):
if i >= 3:
break # 只算了 4 个就停
# 输出:计算 0..计算 3
# 三者性能实测
import timeit
nums = range(200_000)
t_list = timeit.timeit(lambda: [x * 2 for x in nums], number=10)
t_gen = timeit.timeit(lambda: list(x * 2 for x in nums), number=10)
t_map = timeit.timeit(lambda: list(map(lambda x: x * 2, nums)), number=10)
print(f"推导式 {t_list:.3f}s / 生成器 {t_gen:.3f}s / map {t_map:.3f}s")
# 典型:推导式最快,map 略慢(多一层 lambda 调用),差距不大
3. 常见排序与查找算法(手写一遍就够)
import random
from typing import Any, Callable
def bubble_sort(a: list[int]) -> list[int]:
"""O(n²),教学意义 > 实用价值"""
arr = a[:]
for i in range(len(arr) - 1):
swapped = False
for j in range(len(arr) - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped: # 已有序,提前退出
break
return arr
def merge_sort(a: list[int]) -> list[int]:
"""O(n log n) 稳定排序:分治 + 归并,能用于「归并多个有序流」"""
if len(a) <= 1:
return a[:]
mid = len(a) // 2
left, right = merge_sort(a[:mid]), merge_sort(a[mid:])
out, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # 用 <= 保证稳定性
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
out.extend(left[i:]); out.extend(right[j:])
return out
def quick_sort(a: list[int]) -> list[int]:
"""O(n log n) 平均 / O(n²) 最坏;三数取中可避免有序输入退化"""
if len(a) <= 1:
return a[:]
pivot = a[len(a) // 2]
left = [x for x in a if x < pivot]
mid = [x for x in a if x == pivot]
right = [x for x in a if x > pivot]
return quick_sort(left) + mid + quick_sort(right)
def binary_search(a: list[int], target: int) -> int:
"""O(log n),要求有序;找不到返回 -1"""
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2 # 防溢出写法
if a[mid] == target:
return mid
if a[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
def my_sort(items: list[int], key: Callable[[Any], Any] = None,
reverse: bool = False) -> list[int]:
"""手写稳定排序 functools.cmp_to_key 版本"""
from functools import cmp_to_key
def cmp(a, b):
ka, kb = (key(a), key(b)) if key else (a, b)
return (ka < kb) - (ka > kb)
return sorted(items, key=cmp_to_key(cmp), reverse=reverse)
def custom_sort(items: list[dict]) -> list[dict]:
"""多级排序:先按分数降序,同分按年龄升序"""
return sorted(
items,
key=lambda d: (-d["score"], d["age"]),
)
rows = [{"name": "a", "score": 90, "age": 20},
{"name": "b", "score": 90, "age": 18},
{"name": "c", "score": 95, "age": 25}]
print(custom_sort(rows))
# [{'name': 'c', ...}, {'name': 'b', ...}, {'name': 'a', ...}]
data = [5, 2, 9, 1, 7]
print(merge_sort(data)) # [1, 2, 5, 7, 9]
print(binary_search(sorted(data), 7)) # 3
# 稳定性实测
pairs = [("a", 1), ("b", 2), ("c", 3)]
print(sorted(pairs, key=lambda p: p[0])) # 稳定
print(sorted(pairs, key=lambda p: -p[0])) # 反转键仍稳定:b, c, a
要点面试不会让你默写快排,但会问「什么时候快排比归并好」。答案:内存充裕、要极致速度时快排(缓存友好);要稳定性或需要「合并多个已排序流」时用归并(外部排序、Top-K、日志归并)。Python 内置 sorted 是 Timsort——归并排序 + 对现实数据中「局部有序片段」的探测,所以在「几乎有序的真实数据」上接近 O(n)。
4. LRU 缓存与 collections
from collections import OrderedDict, deque, Counter, defaultdict, namedtuple
from functools import lru_cache, cache
# 1) 手写 LRU:用 OrderedDict 的「移动到末尾」能力 O(1)
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # 标记为最近使用
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # 淘汰最久未用的
def __len__(self):
return len(self.cache)
c = LRUCache(2)
c.put(1, "a"); c.put(2, "b")
print(c.get(1)) # a(1 变为最近使用)
c.put(3, "c") # 容量满,淘汰 2
print(c.get(2)) # -1
# 2) 内置 functools.lru_cache(线程安全的 C 实现)
calls = []
@lru_cache(maxsize=128)
def fetch_user(uid: int) -> dict:
calls.append(uid)
return {"id": uid}
fetch_user(1); fetch_user(1)
print(f"真实查询 {len(calls)} 次") # 1
print(fetch_user.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
fetch_user.cache_clear()
fetch_user.cache_parameters() # {'maxsize': 128, 'typed': False}
# 3) cache:无上限版本(3.9+)
@cache
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(60)) # 秒出
fib.cache_clear()
# 4) 带缓存的方法要小心:self 是参数,导致整个对象被强引用
class Service:
def __init__(self, dsn):
self.dsn = dsn
@lru_cache
def heavy_query(self, key): # self 在缓存键里
return key * 2
# 更好:缓存挂在实例属性上,或确保服务对象生命周期明确
# 5) namedtuple 与 dataclass 的选择
Point = namedtuple("Point", "x y") # 不可变、可索引、体积小
p = Point(1, 2)
print(p.x, p[0], tuple(p)) # 1 1 (1, 2)
# 6) deque 的堆用法:优先队列
import heapq
heap = []
for x in [5, 1, 8]:
heapq.heappush(heap, x)
print(heapq.heappop(heap)) # 1 最小堆,O(log n)
# heapq 没有 maxheap,用负数模拟
maxh = [-x for x in [5, 1, 8]]
heapq.heapify(maxh)
print(-heapq.heappop(maxh)) # 8
并发编程高频题
1. 多线程 vs 多进程 vs 协程,选型标准是什么?
| 维度 | threading | multiprocessing | asyncio |
| 并行能力 | 无(GIL),IO 等待可重叠 | 真并行,多核 | 单线程多任务协作 |
| 切换开销 | 极小(微秒) | 大(进程创建 ~ms) | 极小 |
| 内存开销 | 共享堆,一份 | 每进程一份(copy-on-write) | 协程 KB 级 |
| 通信方式 | 共享内存(有竞态) | IPC:管道/队列/共享内存 | 同线程,await |
| 适合任务 | IO 密集且库是同步的 | CPU 密集:计算、图片、模型推理 | 高并发 IO:爬虫、网关、API 聚合 |
| 库要求 | 任意同步库 | 任意(注意可序列化) | 必须是 await/异步版(aiohttp 等) |
| 并发量级 | 几十到几百 | 受 CPU 核数限制 | 数万到十万 |
import asyncio
import time
# 1) asyncio 版:100 个任务并发,单线程搞定
async def fetch(url_id: int) -> int:
await asyncio.sleep(0.1) # 模拟网络等待
return url_id
async def main_async():
t0 = time.perf_counter()
results = await asyncio.gather(*[fetch(i) for i in range(100)])
dt = time.perf_counter() - t0
print(f"asyncio 100 个任务: {dt:.2f}s,结果和={sum(results)}")
# 输出:asyncio 100 个任务: 0.10s,结果和=4950
asyncio.run(main_async()) # 串行会是 10s
# 2) 控制并发量:别一次创建 10 万个任务,会打爆内存与 fd
async def limited_main():
sem = asyncio.Semaphore(20) # 信号量限流
async def guarded(i: int) -> int:
async with sem: # 最多 20 个同时执行
await asyncio.sleep(0.05)
return i
return await asyncio.gather(*[guarded(i) for i in range(200)])
print(len(asyncio.run(limited_main()))) # 200
# 3) 同步库放进 async 的三种处理方式
import httpx
import asyncio
async def with_native_async():
"""最佳:换用原生异步库"""
async with httpx.AsyncClient() as client:
r = await client.get("https://example.com")
return r.status_code
async def with_threadpool():
"""次优:把阻塞调用丢进线程池,不阻塞事件循环"""
loop = asyncio.get_running_loop() # 必须在协程内获取
def blocking(url): # 普通同步函数
return httpx.get(url, timeout=10) # 同步客户端调用
resp = await loop.run_in_executor(None, blocking, "https://example.com")
return resp.status_code
# 4) asyncio 的三个常用原语对比
async def primitives_demo():
# TaskGroup(3.11+):一组任务全成功才结束,一个失败自动取消其余
async def work(i: int) -> int:
await asyncio.sleep(0.01)
return i * 2
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(work(i)) for i in range(5)]
print("TaskGroup 结果:", [t.result() for t in tasks])
# 超时控制
try:
await asyncio.wait_for(asyncio.sleep(5), timeout=0.1)
except asyncio.TimeoutError:
print("wait_for 超时了")
# 取消与清理
async def cancellable():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
print("收到取消信号,清理资源")
raise
task = asyncio.create_task(cancellable())
await asyncio.sleep(0.05)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
asyncio.run(primitives_demo())
要点高频陷阱题:「在 async 函数里调用了同步的 requests.get() 会怎样?」正确答案:整个事件循环被阻塞,所有并发任务全部停摆,性能比单线程串行还差。补救方案按优先级:换异步库 → run_in_executor → 独立线程池 + asyncio.wrap_future。
2. 线程安全:锁、条件变量、队列
import threading
import time
from concurrent.futures import ThreadPoolExecutor
# 1) 最经典:非原子操作导致的竞态条件
counter = 0
lock = threading.Lock()
def unsafe_increment():
global counter
for _ in range(100_000):
counter += 1 # 读-改-写三步,不是原子的
def safe_increment():
global counter
for _ in range(100_000):
with lock:
counter += 1
def run_bench(fn):
global counter
counter = 0
threads = [threading.Thread(target=fn) for _ in range(4)]
t0 = time.perf_counter()
for t in threads:
t.start()
for t in threads:
t.join()
return counter, time.perf_counter() - t0
print("无锁:", run_bench(unsafe_increment)) # 计数器明显少于 400000
print("有锁:", run_bench(safe_increment))
# 2) RLock:可重入锁,避免递归调用自身时死锁
rlock = threading.RLock()
def recurse(n: int) -> int:
with rlock:
return 1 if n == 0 else recurse(n - 1) + 1
print(recurse(100)) # 没有 RLock 会死锁
# 3) Condition:生产者-消费者
import queue
data_queue: queue.Queue[int] = queue.Queue(maxsize=10)
done = threading.Event()
def consumer():
while True:
item = data_queue.get()
if item is None: # 哨兵值表示结束
data_queue.task_done()
return
print(f"消费 {item}")
data_queue.task_done()
def producer(n: int):
for i in range(n):
data_queue.put(i)
data_queue.put(None)
consumers = [threading.Thread(target=consumer) for _ in range(2)]
for c in consumers:
c.start()
producer(10)
data_queue.join() # 等待所有 task_done
done.set()
for c in consumers:
c.join()
# 4) 优先用 Queue 而不是自己拿锁
def worker(q: queue.Queue, results: list):
while True:
job = q.get()
if job is None:
q.task_done()
break
results.append(job * job)
q.task_done()
jobs: queue.Queue = queue.Queue()
results: list[int] = []
for i in range(10):
jobs.put(i)
for _ in range(3):
jobs.put(None)
threads = [threading.Thread(target=worker, args=(jobs, results)) for _ in range(3)]
for t in threads:
t.start()
jobs.join()
print(sorted(results)) # 线程安全的收集
# 5) ThreadPoolExecutor:生产环境最常用的线程池
def heavy_io(task_id: int) -> str:
time.sleep(0.1)
return f"任务{task_id}完成"
with ThreadPoolExecutor(max_workers=10, thread_name_prefix="io") as pool:
futures = {pool.submit(heavy_io, i): i for i in range(20)}
done_count = 0
for fut in concurrent.futures.as_completed(futures):
if fut.exception():
print(f"任务 {futures[fut]} 失败: {fut.exception()}")
else:
done_count += 1
print(f"完成 {done_count}/20")
# 6) GIL 下的「假并行」优化:把纯计算交给 C 层
from functools import lru_cache
def fast_way(xs: list[int]) -> list[int]:
return list(map(abs, xs)) # map 内部是 C 循环,持锁时间极短
def slow_way(xs: list[int]) -> list[int]:
return [abs(x) for x in xs] # 纯 Python 循环,频繁争锁
big = list(range(100_000)) * 10
print(timeit.timeit(lambda: fast_way(big), number=5))
print(timeit.timeit(lambda: slow_way(big), number=5))
要点加分回答:「Python 里真正的线程安全策略是尽量不共享。CPython 的原子操作(如 dict.setdefault、列表的 append)受 GIL 保护其实已经是安全的,但一旦要「检查后再行动」(check-then-act)就必须显式加锁。线程安全的三种手段:锁(互斥)、不可变数据(最省事)、把状态收进单个线程(消息传递)。」
3. multiprocessing 的进程池与通信
import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor, TimeoutError as FTimeout
import os
import time
def compute(n: int) -> int:
"""子进程执行的函数:必须可 pickle(顶层函数,不能是 lambda 或局部函数)"""
return sum(i * i for i in range(n))
def init_worker() -> None:
"""每个子进程启动时执行一次:建连接池在这里做最划算"""
import redis
global _conn
_conn = redis.from_url("redis://localhost:6379/0")
def task_with_init(x: int) -> int:
return x + 1
if __name__ == "__main__":
# 1) 基本用法:map 分发,分块计算
with mp.Pool(processes=4) as pool:
t0 = time.perf_counter()
results = pool.map(compute, [1_000_000] * 8)
print(f"进程池 8 个任务: {time.perf_counter()-t0:.2f}s")
# 2) imap:惰性返回,适合流式处理与提前终止
with mp.Pool(4) as pool:
for i, r in enumerate(pool.imap(compute, [100_000] * 100, chunksize=5)):
if i >= 5:
pool.terminate() # 不要了就主动终止
break
print(i, r)
# 3) 起新式接口 ProcessPoolExecutor:更好的返回值与异常传递
with ProcessPoolExecutor(max_workers=4, initializer=init_worker) as ex:
fut = ex.submit(task_with_init, 1)
print(fut.result(timeout=5))
print(list(ex.map(task_with_init, range(5))))
# 4) 进程间通信四种方式
# (a) Queue:生产者-消费者
q = mp.Queue()
p = mp.Process(target=lambda: q.put("来自子进程"))
p.start()
print(q.get())
p.join()
# (b) Pipe:点对点双向
parent, child = mp.Pipe()
p = mp.Process(target=lambda: child.send("ping"))
p.start()
print(parent.recv())
p.join()
# (c) SharedMemory:大数组高效共享(比 Manager 快得多)
from multiprocessing import shared_memory
shm = shared_memory.SharedMemory(create=True, size=1024 * 64)
buf = shm.buf
buf[:5] = b"hello"
print(bytes(buf[:5])) # b'hello'
shm.close(); shm.unlink()
# (d) Manager:代理对象,适合复杂结构但慢
with mp.Manager() as mgr:
d = mgr.dict({"a": 1})
d["b"] = 2
print(dict(d))
# 5) 平台差异:Windows/macOS 的 spawn 必须 if __name__ 保护
print(mp.get_start_method()) # Linux: fork;Windows: spawn
# 6) fork 带来的坑:子进程继承了父进程的锁与连接
# 典型症状:fork 后数据库连接在两个进程共用同一 socket,导致协议错乱
# 解决:用 spawn 上下文,或在 fork 前建立连接、fork 后重建
ctx = mp.get_context("spawn")
with ctx.Pool(2) as pool:
print(pool.map(compute, [1000, 2000]))
常见坑进程池必踩的三个坑:① 传给 Pool.map 的函数必须是模块顶层可 pickle 的函数,lambda、局部函数、带打开文件句柄的对象都会报 AttributeError: Can't pickle local object;② Windows/macOS 用 spawn,子进程会重新导入主模块,必须有 if __name__ == "__main__": 保护,否则无限递归创建进程;③ fork 会继承父进程已建立的数据库连接,必须在子进程内重建(标准做法是用 initializer)。
4. 死锁的成因与规避
# 死锁四个必要条件(Coffman 条件)
# 1) 互斥 2) 持有并等待 3) 不可剥夺 4) 循环等待
# 经典死锁:两个线程以相反顺序获取两把锁
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def transfer_a():
with lock_a: # 先拿 A
with lock_b: # 等 B
pass
def transfer_b():
with lock_b: # 先拿 B —— 顺序相反
with lock_a: # 等 A -> 死锁
pass
# 解法一:全局固定加锁顺序
import itertools
ORDER = (lock_a, lock_b)
def transfer_fixed(which_from, which_to, amount):
with which_from:
with which_to:
pass
# 解法二:一次性申请所有锁(用 acquire 的多参数形式)
ok = lock_a.acquire(blocking=False) and lock_b.acquire(blocking=False)
if ok:
try:
pass
finally:
lock_b.release(); lock_a.release()
# 解法三:加超时
if lock_a.acquire(timeout=1.0):
try:
if lock_b.acquire(timeout=1.0):
try:
pass
finally:
lock_b.release()
finally:
lock_a.release()
# 解法四:用 contextlib 做可重入与有序锁
import contextlib
from contextlib import contextmanager
@contextmanager
def ordered_locks(*locks):
"""按 id 排序获取,彻底规避循环等待"""
for lk in sorted(locks, key=lambda x: id(x)):
lk.acquire()
try:
yield
finally:
for lk in reversed(sorted(locks, key=lambda x: id(x))):
lk.release()
# 解法五:尽量用 Queue 传递数据,而不是锁保护共享状态
# 生产环境最实用的一条:能用消息队列解决的并发,不要用锁
数据库与存储高频题
1. ACID 与隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
| Read Uncommitted(读未提交) | 会 | 会 | 会 | 几乎无实际使用 |
| Read Committed(读已提交,PostgreSQL 默认) | 不会 | 会 | 会 | 每次语句读到最新已提交数据 |
| Repeatable Read(可重复读,MySQL 默认) | 不会 | 不会 | 会(InnoDB 基本避免) | 事务内多次读结果一致 |
| Serializable(串行化) | 不会 | 不会 | 不会 | 完全串行,并发性能差 |
import sqlite3
import threading
# SQLite 支持串行化隔离,用它演示并发读写的差异
conn = sqlite3.connect(":memory:", check_same_thread=False)
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("CREATE TABLE account (id INTEGER PRIMARY KEY, balance INTEGER)")
conn.execute("INSERT INTO account VALUES (1, 1000)")
# 1) 原子更新:一条 SQL 完成「读改写」,避免丢失更新
def safe_deduct(uid: int, amount: int) -> bool:
cur = conn.cursor()
cur.execute(
"UPDATE account SET balance = balance - ? "
"WHERE id = ? AND balance >= ?", # 条件写进 SQL,靠数据库保证原子
(amount, uid, amount),
)
conn.commit()
return cur.rowcount > 0 # 影响行数为 0 说明余额不足
print(safe_deduct(1, 300)) # True
print(conn.execute("SELECT balance FROM account WHERE id=1").fetchone()) # (700,)
# 2) 不安全的写法:先查后改,事务间存在竞态
def unsafe_deduct(uid: int, amount: int) -> bool:
row = conn.execute("SELECT balance FROM account WHERE id=?", (uid,)).fetchone()
if row[0] < amount:
return False
time.sleep(0.001) # 模拟两次 SQL 之间的耗时
conn.execute("UPDATE account SET balance=? WHERE id=?", (row[0] - amount, uid))
conn.commit()
return True
# 3) 悲观锁 vs 乐观锁
# 悲观锁:SELECT ... FOR UPDATE,串行化冲突,适合冲突频繁
def pessimistic_deduct(uid: int, amount: int) -> bool:
conn.execute("BEGIN")
row = conn.execute(
"SELECT balance FROM account WHERE id=? FOR UPDATE", (uid,)
).fetchone()
if row[0] < amount:
conn.rollback()
return False
conn.execute("UPDATE account SET balance=? WHERE id=?", (row[0] - amount, uid))
conn.commit()
return True
# 4) 乐观锁:版本号,适合冲突少
# 建表时:CREATE TABLE account (..., version INTEGER DEFAULT 0);
def optimistic_deduct(uid: int, amount: int, expected_version: int) -> bool:
cur = conn.execute(
"UPDATE account SET balance = balance - ?, version = version + 1 "
"WHERE id = ? AND version = ?", # 版本不匹配则影响 0 行
(amount, uid, expected_version),
)
conn.commit()
return cur.rowcount > 0
# 5) 显式事务:Python DB-API 的事务边界
conn.execute("BEGIN")
try:
conn.execute("UPDATE account SET balance = balance + 100 WHERE id = 1")
conn.execute("UPDATE account SET balance = balance - 100 WHERE id = 2")
conn.commit() # 全部成功才提交
except Exception:
conn.rollback() # 任一失败整体回滚
raise
# 6) 连接池的正确使用:不要每次请求新建连接
import contextlib
@contextlib.contextmanager
def get_conn(pool):
"""从池里借连接,用完必还;即使出错也要归还"""
conn = pool.getconn()
try:
yield conn
except Exception:
conn.rollback()
raise
finally:
pool.putconn(conn)
要点加分回答:ACID 中的 C(Consistency,一致性)在不同系统里含义不同——关系型数据库里通常指「约束不被破坏」(外键、唯一、非空);分布式系统里常指「副本数据一致」。面试时说清楚你指的是哪一种,会显得层次分明。
2. 索引原理:B+ 树、聚簇索引与最左前缀
| 概念 | 说明 | 面试要点 |
| B+ 树 | 多路平衡查找树,数据只在叶子,叶子用链表串起来便于范围扫描 | 树高低(3~4 层就能存千万级),磁盘 IO 次数少 |
| 聚簇索引 | InnoDB 中主键索引即数据本身,二级索引叶子存主键值 | 主键要短且有序,否则会页分裂 |
| 回表 | 二级索引查到主键后再去聚簇索引取完整行 | 这就是「覆盖索引」优化要避免的回表 |
| 最左前缀 | 联合索引 (a,b,c) 可用于 a、(a,b)、(a,b,c) | 范围查询之后的列失效 |
| 索引失效场景 | 对索引列做运算、用函数、隐式类型转换、前导模糊 %x、OR 连接非索引列 | 能举出 3 个例子就合格 |
# 1) 最左前缀与范围截断
-- 建联合索引
CREATE INDEX idx_abc ON orders (user_id, status, created_at);
-- ✅ 走索引
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
SELECT * FROM orders WHERE user_id = 1 AND status = 'paid' AND created_at > '2026-01-01';
-- ⚠️ 部分可用:user_id 走了索引,created_at 没用上
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2026-01-01';
-- ❌ 不走索引:跳过了最左列
SELECT * FROM orders WHERE status = 'paid';
-- ❌ 不走索引:前导通配符
SELECT * FROM orders WHERE user_id LIKE '%123';
# 2) 覆盖索引:避免回表
-- 需要字段都在索引里,EXPLAIN 的 Extra 出现 Using index
CREATE INDEX idx_cover ON orders (user_id, status, amount);
SELECT user_id, status, amount FROM orders WHERE user_id = 1;
-- 只查索引里的列,不需要回聚簇索引 -> 快得多
# 3) 排序方向与索引方向一致时可用索引消除排序
CREATE INDEX idx_time ON orders (created_at DESC);
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20;
# 4) 索引下推(Index Condition Pushdown,ICP)
SELECT * FROM orders WHERE user_id = 1 AND name LIKE '%张%';
-- ICP 把 name 的过滤下推到存储引擎层,减少回表次数
常见坑最经典的错误回答是「索引越多越好」。正确说法:索引加速读、拖慢写——每次 INSERT/DELETE 都要维护所有相关索引,且索引占空间、影响缓冲池命中率。原则是「按查询模式建索引,3~5 个常用复合索引通常足够,写多的表反而要少建」。
3. 深分页、慢查询与连接池
# 1) 深分页问题
-- ❌ OFFSET 1000000 LIMIT 20:数据库要先扫描并丢弃前 100 万行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 方案一:游标分页(keyset pagination),只扫 20 行
SELECT * FROM orders WHERE id > 12345 ORDER BY id LIMIT 20;
-- ✅ 方案二:业务排序字段 + 游标
SELECT * FROM orders
WHERE (created_at, id) < ('2026-01-01 10:00:00', 999)
ORDER BY created_at DESC, id DESC
LIMIT 20;
# Python 侧实现游标分页
import sqlite3
conn = sqlite3.connect("shop.db")
PAGE_SIZE = 20
def fetch_page(cursor: tuple[str, int] | None = None) -> tuple[list, tuple | None]:
if cursor is None:
sql = "SELECT created_at, id, amount FROM orders ORDER BY created_at DESC, id DESC LIMIT ?"
args = (PAGE_SIZE,)
else:
last_time, last_id = cursor
sql = ("SELECT created_at, id, amount FROM orders "
"WHERE (created_at, id) < (?, ?) "
"ORDER BY created_at DESC, id DESC LIMIT ?")
args = (last_time, last_id, PAGE_SIZE)
rows = conn.execute(sql, args).fetchall()
if not rows:
return [], None
last = rows[-1]
return rows, (last[0], last[1])
rows, next_cursor = fetch_page()
while rows and next_cursor:
rows, next_cursor = fetch_page(next_cursor)
# 2) 慢查询日志
# MySQL: SET GLOBAL long_query_time = 0.2;
# SHOW VARIABLES LIKE 'slow_query_log%';
# PostgreSQL: ALTER SYSTEM SET log_min_duration_statement = 200;
# SELECT * FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
# 3) 用 Python 做慢查询定位与告警
import logging
import time
slow_queries: list[tuple[float, str]] = []
SLOW_MS = 200
def timed_query(sql: str, params: tuple = (), conn=conn) -> list:
t0 = time.perf_counter()
cur = conn.execute(sql, params)
rows = cur.fetchall()
dt = (time.perf_counter() - t0) * 1000
if dt > SLOW_MS:
slow_queries.append((dt, sql))
logging.warning("慢查询 %.1fms: %s params=%s", dt, sql, params)
if len(slow_queries) > 100:
logging.error("检测到 %d 条慢查询,检查索引与执行计划", len(slow_queries))
return rows
timed_query("SELECT * FROM orders WHERE user_id = ?", (1,))
# 4) 连接池:数据库能承受的连接数计算
# 连接数上限 = CPU 核数 * 2 + 有效磁盘数(PostgreSQL 官方建议)
# 应用侧:每个 gunicorn worker 独立建池
# 总连接数 = workers 数 × pool_size
# 例:8 workers × pool_size 10 + 20 个管理连接 = 100 -> 需要 max_connections >= 120
import os
WORKERS = 8
POOL_SIZE = 10
print(f"总连接数 = {WORKERS} × {POOL_SIZE} = {WORKERS * POOL_SIZE}")
print(f"建议 max_connections = {WORKERS * POOL_SIZE + 20}")
# 5) 连接池参数的工程含义
# pool_size : 常驻连接数,应 >= (该进程并发请求数 / worker 数)
# max_overflow : 峰值临时连接,超过后请求排队并超时
# pool_timeout : 排队等待秒数,超时抛 QueuePool timeout —— 这个异常几乎总是
# 「数据库慢」的信号,而不是「应用配置错」
4. Redis 高频数据结构与缓存一致性
| 结构 | 底层 | 典型场景 | 复杂度 |
| String | SDS 动态字符串 | 缓存对象、计数器、分布式锁、Token | O(1) |
| Hash | 字段级 string 编码 | 对象属性、可部分更新的结构 | O(1) |
| List | quicklist(3.2 后) | 消息队列、时间线 | 两端 O(1) |
| Set | intset / hashtable | 标签、去重、共同好友 | O(1) 均摊 |
| Sorted Set | 跳表 + 哈希表 | 排行榜、延迟队列、范围查询 | O(log n) |
| Bitmap | String 上的位操作 | 签到、布尔标记、省内存 | O(1) |
| HyperLogLog | 固定 12KB | UV 统计(误差 0.81%) | O(1) |
| Stream | 持久化消息流 | 可重放的轻量消息队列 | O(1) |
import time
import redis
r = redis.from_url("redis://localhost:6379/0", decode_responses=True)
# 1) String 做分布式锁(SET NX EX)
LOCK = "lock:order:1001"
def acquire_lock(key: str, ttl_ms: int = 5000) -> str | None:
token = __import__("secrets").token_urlsafe(16) # 唯一标识,防误删他人锁
ok = r.set(key, token, nx=True, ex=ttl_ms // 1000)
return token if ok else None
def release_lock(key: str, token: str) -> None:
"""必须用 Lua 保证「比对并删除」的原子性"""
lua = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
r.eval(lua, 1, key, token)
# 2) ZSet 做排行榜
def record_score(user: str, score: float) -> None:
r.zadd("rank:score", {user: score})
def top_n(n: int = 10) -> list[tuple[str, float]]:
return r.zrevrange("rank:score", 0, n - 1, withscores=True)
def user_rank(user: str) -> int | None:
rank = r.zrevrank("rank:score", user) # 0-based
return rank + 1 if rank is not None else None
record_score("alice", 900)
record_score("bob", 750)
record_score("carol", 1200)
print(top_n(3)) # [('carol', 1200.0), ('alice', 900.0), ('bob', 750.0)]
# 3) ZSet 做延时队列(比 Redis List 靠谱)
def delay_task(task_id: str, delay_seconds: int) -> None:
now = time.time()
r.zadd("delay:queue", {task_id: now + delay_seconds})
def poll_due_tasks(limit: int = 100) -> list[str]:
now = time.time()
due = r.zrangebyscore("delay:queue", 0, now, start=0, num=limit)
if due:
r.zrem("delay:queue", *due) # 取出的同时删除
return due
delay_task("order_timeout:1001", 1800)
# 4) HyperLogLog 统计 UV:100 万用户只用 ~12KB
pf = r.pfadd("uv:20260101", *[f"user{i}" for i in range(1_000_000)])
print(pf) # 1 表示已创建
print(r.pfcount("uv:20260101")) # 约 997307(误差 < 1%)
# 5) Pipeline 批处理:把 100 次往返压成 1 次
import time as _t
t0 = _t.perf_counter()
for i in range(1000):
r.set(f"k{i}", i)
print(f"逐条 1000 次: {_t.perf_counter()-t0:.3f}s")
t0 = _t.perf_counter()
pipe = r.pipeline(transaction=False) # transaction=False 更快
for i in range(1000):
pipe.set(f"k{i}", i)
pipe.execute()
print(f"Pipeline 1000 次: {_t.perf_counter()-t0:.3f}s") # 通常快 5~10 倍
# 6) Lua 保证复杂操作的原子性
LUA = """
local stock = tonumber(redis.call('get', KEYS[1]) or '0')
local need = tonumber(ARGV[1])
if stock >= need then
redis.call('decrby', KEYS[1], need)
return 1
else
return 0
end
"""
if r.eval(LUA, 1, "stock:sku1", 1):
print("扣减成功")
else
print("库存不足")
常见坑缓存一致性三问,面试必考:
(1)先更新数据库还是先更新缓存?→ 推荐「先更新 DB,再删除缓存(Cache-Aside)」,避免并发写导致缓存被旧值覆盖。
(2)删除缓存失败怎么办?→ 重试队列 + 延迟双删(延迟是为了应对主从延迟导致的读到旧值回填)。
(3)为什么不能直接更新缓存?→ 更新可能失败、可能并发乱序、可能无写权限;删除是幂等的「惰性加载」,代价只是下次读多一次 DB。
Web 与工程化高频题
1. WSGI、ASGI 与框架关系
| 规范 | 定义 | 代表实现 | 支持语法 |
| WSGI(Web Server Gateway Interface) | Python 2.7 时代定义的同步服务器接口 | Flask、Django、gunicorn | def 同步函数 |
| ASGI(Asynchronous Server Gateway Interface) | 面向异步的接口规范,支持 WebSocket | FastAPI、Starlette、uvicorn | async def / await |
| ASGI 可兼容 WSGI | 异步框架也能包装同步视图 | starlette.middleware.WSGIMiddleware | 两者 |
# WSGI 应用的最简形态:一个可调用对象,接收 environ 与 start_response
def simple_wsgi_app(environ, start_response):
"""environ 包含请求的全部信息(PATH_INFO、QUERY_STRING、REQUEST_METHOD 等)"""
path = environ.get("PATH_INFO", "/")
method = environ.get("REQUEST_METHOD", "GET")
if path == "/healthz" and method == "GET":
body = b'{"status":"ok"}'
status = "200 OK"
elif path == "/echo" and method == "POST":
try:
length = int(environ.get("CONTENT_LENGTH") or 0)
except ValueError:
length = 0
raw = environ["wsgi.input"].read(length)
body = raw.upper() if raw else b""
status = "200 OK"
else:
body, status = b"not found", "404 Not Found"
headers = [("Content-Type", "application/octet-stream"),
("Content-Length", str(len(body)))]
start_response(status, headers) # 必须先调用 start_response
return [body] # 必须返回可迭代的 bytes 序列
# 用 wsgiref 起一个测试服务器
from wsgiref.simple_server import make_server
if __name__ == "__main__":
srv = make_server("127.0.0.1", 8001, simple_wsgi_app)
print("测试服务器启动:http://127.0.0.1:8001/healthz")
# srv.serve_forever()
# ASGI 应用的最简形态:async 可调用对象,接收 scope/receive/send
async def simple_asgi_app(scope, receive, send):
"""scope 是连接信息,receive/send 是协程,用来收发消息"""
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
return
if scope["type"] != "http":
return
await send({"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/plain; charset=utf-8")]})
await send({"type": "http.response.body", "body": b"hello from asgi"})
# 三层结构总结:
# 客户端 -> Nginx -> WSGI/ASGI 服务器(gunicorn/uvicorn) -> 应用框架 -> 你的代码
要点面试常追问:「为什么 Flask 是同步框架却能跑高并发?」答案:Flask 本身是同步的(一个请求一个线程阻塞),真正提供并发的是服务器。gunicorn 用 gthread 类开多个线程,用 gevent 类做协程化;而 FastAPI 走 ASGI,服务器(如 uvicorn)本身就是事件循环,异步在框架层而非服务器层实现。
2. 常见中间件要做什么
import time
import uuid
from functools import wraps
from flask import Flask, g, request, jsonify
app = Flask(__name__)
# 1) 请求 ID 中间件:贯穿日志与下游调用
@app.before_request
def _assign_request_id():
g.request_id = request.headers.get("X-Request-Id", uuid.uuid4().hex)
g.start = time.perf_counter()
@app.after_request
def _add_headers(resp):
resp.headers["X-Request-Id"] = g.request_id
resp.headers["X-Response-Time-Ms"] = f"{(time.perf_counter()-g.start)*1000:.2f}"
return resp
# 2) 统一错误处理中间件(装饰器版,可挂到任意接口)
def handle_errors(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
try:
return fn(*args, **kwargs)
except ValidationError as exc:
return jsonify(code=400, message="参数错误", detail=exc.errors), 400
except NotFoundError as exc:
return jsonify(code=404, message=str(exc)), 404
except PermissionError as exc:
return jsonify(code=403, message=str(exc)), 403
except Exception:
app.logger.exception("未处理异常 request_id=%s", g.get("request_id"))
# 注意:生产环境绝不返回 traceback
return jsonify(code=500, message="服务内部错误"), 500
return wrapper
class ValidationError(Exception): pass
class NotFoundError(Exception): pass
@app.get("/api/orders/<oid>")
@handle_errors
def get_order(oid: int):
if oid != 42:
raise NotFoundError(f"订单 {oid} 不存在")
return jsonify(id=oid, status="paid")
# 3) 限流中间件:滑动窗口或令牌桶
class RateLimiter:
"""内存版滑动窗口:适合单实例;多实例请用 Redis 实现"""
def __init__(self, limit: int = 100, window: int = 60):
self.limit, self.window = limit, window
self.hits: dict[str, list[float]] = {}
def check(self, key: str) -> bool:
now = time.time()
window_hits = [t for t in self.hits.get(key, []) if now - t < self.window]
if len(window_hits) >= self.limit:
return False
window_hits.append(now)
self.hits[key] = window_hits
return True
def cleanup(self) -> None:
now = time.time()
for key in list(self.hits):
self.hits[key] = [t for t in self.hits[key] if now - t < self.window]
if not self.hits[key]:
del self.hits[key]
limiter = RateLimiter(limit=10, window=60)
@app.before_request
def _rate_limit():
if request.endpoint == "health":
return None
ip = request.remote_addr or "unknown"
if not limiter.check(ip):
return jsonify(code=429, message="请求过于频繁"), 429
return None
@app.get("/health")
def health():
return jsonify(status="ok")
# 4) 幂等中间件:防止重复提交
_seen: dict[str, object] = {}
def idempotent(key_prefix: str = "idemp"):
def deco(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
key = f"{key_prefix}:{request.headers.get('Idempotency-Key', '')}"
if key in _seen:
return jsonify(code=0, data=_seen[key], duplicated=True), 200
result = fn(*args, **kwargs)
_seen[key] = result.get_json() if hasattr(result, "get_json") else result
return result
return wrapper
return deco
@app.post("/api/orders")
@idempotent()
def create_order():
return jsonify(order_no="ORD20260101001", amount=99.0)
3. RESTful、状态码与 API 设计
| 场景 | 方法 | 成功码 | 返回体 |
| 查询列表 | GET /orders | 200 | {"code":0,"data":[...],"total":100} |
| 查询单个 | GET /orders/42 | 200 / 404 | 对象或错误 |
| 创建 | POST /orders | 201 + Location 头 | 新对象 |
| 整体替换 | PUT /orders/42 | 200 / 204 | 更新后对象 |
| 局部更新 | PATCH /orders/42 | 200 | 更新后对象 |
| 删除 | DELETE /orders/42 | 204 | 空 |
| 参数错误 | — | 400 | 字段级错误列表 |
| 未认证 / 无权限 | — | 401 / 403 | 错误码 |
| 触发限流 | — | 429 + Retry-After | 错误码 |
| 并发冲突 | — | 409 | 冲突说明 |
| 依赖不可用 | — | 503 | 错误码 |
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
class OrderStatus(str, Enum):
"""用 str 混入 Enum:序列化时自动转字符串,且可比较"""
CREATED = "created"
PAID = "paid"
SHIPPED = "shipped"
COMPLETED = "completed"
CANCELLED = "cancelled"
@property
def is_final(self) -> bool:
return self in {OrderStatus.COMPLETED, OrderStatus.CANCELLED}
@dataclass
class ApiResponse:
code: int
message: str
data: object = None
request_id: str | None = None
def to_dict(self) -> dict:
d = {"code": self.code, "message": self.message, "data": self.data}
if self.request_id:
d["request_id"] = self.request_id
return d
# 分页与排序参数的安全处理
import re
SORT_WHITELIST = {"created_at", "amount", "status"}
def parse_pagination(args: dict) -> tuple[int, int, str, str]:
page = max(1, min(int(args.get("page", 1)), 1000)) # 上限防爬
size = max(1, min(int(args.get("size", 20)), 100)) # 上限防压库
sort = args.get("sort", "created_at")
if sort not in SORT_WHITELIST: # 白名单防注入
sort = "created_at"
order = "desc" if args.get("order", "desc").lower() == "desc" else "asc"
return page, size, sort, order
# 分页响应的标准结构
def build_page(rows: list, total: int, page: int, size: int) -> dict:
return ApiResponse(
code=0, message="ok", data={
"items": rows,
"pagination": {
"page": page, "size": size, "total": total,
"pages": (total + size - 1) // size,
},
},
).to_dict()
# 接口版本化策略
# /api/v1/orders —— 显式版本,改动大时升 v2
# /orders —— 通过 Accept 头或请求头协商
# 实践:内部系统用头部协商,开放 API 用路径版本
4. 测试与调试高频问答
| 问题 | 参考答案要点 |
| 单元测试 vs 集成测试 | 单元测逻辑不测依赖(mock 掉 I/O);集成测模块间协作(用真实依赖或测试容器) |
| 覆盖率目标 | 不是越高越好;核心业务逻辑应 80%+,框架样板代码可低;关注「分支覆盖」而非行覆盖 |
| 测试金字塔比例 | 单元 70% / 集成 20% / 端到端 10%;E2E 慢且易碎,要控制数量 |
| Mock 还是真实依赖 | Mock 掉你没在测的东西;测数据库行为时用 testcontainers 起真库 |
| 如何测异步代码 | pytest-asyncio、AsyncMock、anyio 插件;避免真实 sleep,用事件或 mock |
| Flaky 测试怎么办 | 先隔离随机源(注入随机种子、冻结时间)、去除时序依赖;再决定重试或删除 |
| 如何调试线上问题 | 先止血(回滚/限流/降级),再定位(日志/指标/火焰图),最后复盘 |
import pytest
from unittest.mock import patch, MagicMock
from datetime import datetime, timezone
# 1) 参数化测试:把数据与逻辑分离
@pytest.mark.parametrize(
"amount, discount_rate, expected",
[
(100, 0.0, 100.0),
(100, 0.1, 90.0),
(99.99, 0.15, 84.99),
(0, 0.5, 0.0),
],
)
def test_calculate_discount(amount, discount_rate, expected):
from app.pricing import calculate_discount
assert calculate_discount(amount, discount_rate) == pytest.approx(expected)
# 2) fixture + 依赖覆盖:分层复用测试资源
@pytest.fixture
def db_conn():
"""每个测试一个独立事务,结束后回滚,保证测试间零污染"""
from app.db import engine
conn = engine.connect()
trans = conn.begin()
yield conn
trans.rollback()
conn.close()
@pytest.fixture
def mock_payment_gateway():
with patch("app.services.payment_gateway.charge") as mock:
mock.return_value = {"status": "success", "txn_id": "txn_123"}
yield mock
# 3) 依赖注入让测试变简单(不要在函数内部硬编码)
def create_order(user_id: int, payment=None, db=None) -> dict:
payment = payment or default_payment_gateway
db = db or default_db
txn = payment.charge(user_id, 99.0)
order = db.insert_order(user_id=user_id, txn_id=txn["txn_id"])
return order
def test_create_order_with_mock(db_conn, mock_payment_gateway):
order = create_order(42, payment=mock_payment_gateway, db=db_conn)
assert order["status"] == "created"
mock_payment_gateway.assert_called_once_with(42, 99.0)
# 4) 冻结时间,消除 flaky
def test_token_expiry(monkeypatch):
from app.auth import create_token, verify_token
fake_now = datetime(2026, 1, 1, tzinfo=timezone.utc)
monkeypatch.setattr("app.auth.datetime_now", lambda: fake_now)
token = create_token(user_id=42, expire_seconds=3600)
assert verify_token(token)["user_id"] == 42
# 时间推进到 2 小时后,token 应过期
monkeypatch.setattr(
"app.auth.datetime_now",
lambda: fake_now.replace(hour=fake_now.hour + 2),
)
from app.auth import TokenExpired
with pytest.raises(TokenExpired):
verify_token(token)
# 5) 异步测试
import pytest
@pytest.mark.asyncio
async def test_async_fetch():
from app.clients import fetch_user
with patch("app.clients.http_client.get",
new=MagicMock(side_effect=lambda *a, **k: _fake_response())):
user = await fetch_user(42)
assert user["id"] == 42
async def _fake_response():
class R:
status_code = 200
async def json(self):
return {"id": 42}
return R()
# 6) 契约测试:保证 API 变更不破坏下游
def test_api_contract():
"""用 schema 校验响应结构,防止破坏性变更上线"""
from app.models import OrderSchema
payload = {"id": 1, "amount": 99.0, "status": "paid", "created_at": "2026-01-01T00:00:00Z"}
OrderSchema.model_validate(payload) # 不抛异常即通过
with pytest.raises(Exception):
OrderSchema.model_validate({"id": 1}) # 缺字段应失败
三个月学习路线图
前面的题目告诉你「考什么」,这一节告诉你「怎么准备」。下面的路线图假设你每天能投入 2~3 小时、周末 4~5 小时,共 13 周。每周有一个可验证的产出物,没有产出物的学习等于没学。
总体节奏
| 阶段 | 周次 | 主题 | 核心产出 | 可投递岗位 |
| 打地基 | 第 1~4 周 | 语法、数据结构、函数与 OOP | 4 个小练习项目 + 刷题 100 道 | 实习 / 初级后端 |
| 上强度 | 第 5~8 周 | 标准库、并发、Web、数据库 | 1 个 CRUD 后端 + 1 个爬虫 | 初级后端 |
| 做项目 | 第 9~11 周 | 工程化、部署、测试、综合项目 | 1 个可上线项目(含 CI/CD 与监控) | 中级后端 |
| 冲面试 | 第 12~13 周 | 面经、八股、项目包装、模拟面试 | 简历 + 2 个可讲的项目故事 | 中级后端 / 数据方向 |
第 1~4 周:打地基
| 周 | 学习内容 | 每日练习 | 周末产出 |
| 第 1 周 |
环境、类型系统、运算符、流程控制 |
每天 10 道 LeetCode 简单题;写 5 个小程序 |
猜数字游戏(命令行版,含输入校验与异常处理) |
| 第 2 周 |
字符串、内置数据结构(list/dict/set/tuple) |
每天 10 道简单 + 5 道中等;手写 5 个数据结构类 |
通讯录管理器(增删改查 + JSON 持久化 + 模糊搜索) |
| 第 3 周 |
函数、作用域、装饰器、生成器 |
每天手写 3 个装饰器(计时/重试/缓存) |
文本统计工具(词频、高频词、阅读器装饰器) |
| 第 4 周 |
类与 OOP、异常、文件与序列化 |
每天 1 个设计模式实现;刷题补齐 100 道 |
学生成绩管理系统(类建模 + CSV/JSON 存储 + 单元测试) |
第 5~8 周:上强度
| 周 | 学习内容 | 关键练习 | 周末产出 |
| 第 5 周 |
标准库(os/collections/datetime/json/re)、模块化、类型注解 |
把前 4 周项目全部重构为包结构 + 类型注解 + pytest 覆盖 |
包结构模板项目(含 pyproject、ruff、mypy、CI) |
| 第 6 周 |
并发:线程、进程、asyncio、锁与队列 |
用三种方式分别实现「1000 个 URL 并发抓取」并对比耗时 |
并发爬虫(含速率限制、重试、去重、结果入库) |
| 第 7 周 |
数据库:SQL、索引、事务、ORM、连接池 |
手写 30 条常用 SQL;解释 10 个索引失效场景 |
博客后端(FastAPI + SQLAlchemy + Alembic 迁移) |
| 第 8 周 |
Web:HTTP 协议、REST、中间件、鉴权与限流 |
用 curl / httpie 调试 30 个 API 细节 |
带鉴权的 REST API(JWT + 权限 + 限流 + OpenAPI 文档) |
第 9~11 周:做项目
| 周 | 主题 | 必做清单 | 验收标准 |
| 第 9 周 |
测试与工程化 |
pytest(单测/集成/fixture/参数化/覆盖率)、ruff、mypy、pre-commit |
核心模块覆盖率 ≥ 80%,CI 必过 |
| 第 10 周 |
缓存与性能 |
Redis 缓存策略、多级缓存、慢查询定位、py-spy 实战一次 |
接口 P99 优化前后有量化对比数据 |
| 第 11 周 |
部署与运维 |
Docker 多阶段、Compose、Nginx + gunicorn、日志与监控、CI/CD 部署 |
一条 docker compose up -d 拉起全套并可健康检查 |
第 12~13 周:冲面试
| 任务 | 每日投入 | 产出 |
| 复习本章面试题,按目录自问自答并录音回听 | 1 小时 | 32 道题的答案录音,找出卡壳的 10 道重点攻 |
| 刷算法题(中等难度为主) | 1 小时 | 累计 200 道,能口述复杂度与思路 |
| 项目复盘与包装 | 1 小时 | 每个项目写好「背景-方案-取舍-结果-反思」五段式讲稿 |
| 模拟面试(对着摄像头自问自答或找同学互面) | 30 分钟 | 至少 3 次完整 60 分钟模拟 |
时间分配建议(在职 vs 在校)
| 身份 | 工作日 | 周末 | 重点倾斜 |
| 在校生 | 3~4 小时(含课程) | 6 小时 | 算法 + 项目广度,先能进大厂 |
| 在职转行 | 2 小时 | 5 小时 | 深度 + 工程经验,把工作项目讲成亮点 |
| 已工作 1~3 年 | 1.5 小时 | 4 小时 | 系统设计与性能调优,准备晋升 |
要点三条坚持下来的经验:第一,每天都要写代码,哪怕只有 30 行;第二,项目必须有真实使用者(哪怕只有你自己和你的同事),没被用过的项目和玩具没区别;第三,每个技术点都要能落到一行代码或一个配置项上,否则就是概念游戏。
项目清单与资料推荐
推荐项目(按难度递增)
| 项目 | 难度 | 核心技术点 | 面试可讲的故事 |
| 命令行待办工具 | ★ | argparse、JSON 持久化、异常处理 | 「我如何设计子命令与退出码,让它能被脚本集成」 |
| 并发爬虫 | ★★ | asyncio、限流、robots、去重、增量 | 「我如何把 10 万页的抓取从 3 小时压到 20 分钟」 |
| 博客 API | ★★ | FastAPI、ORM、迁移、JWT、分页 | 「我如何做接口幂等与防止 N+1 查询」 |
| 数据看板 | ★★★ | 定时任务、聚合、缓存、可视化 | 「日增量数据如何做到秒级刷新」 |
| 全栈论坛 | ★★★ | 前后端分离、WebSocket、文件上传、反垃圾 | 「如何设计消息不重复投递与顺序保证」 |
| 可观测的服务 | ★★★★ | 结构化日志、指标、链路、告警、Docker 编排 | 「一次线上故障的完整定位与复盘」 |
必读资料(官方优先)
| 类别 | 资料 | 为什么 | 阅读方式 |
| 语言 | Python 官方文档 Tutorial + Language Reference | 最准确,语言细节的最终裁判 | 不要通读,按查用;数据模型章节必读 |
| 语言 | Fluent Python(Lukasz Lason) | 讲解「为什么这样设计」最到位 | 配合本书第 4~13 章对照读 |
| 工具 | PEP 8 / PEP 20 / PEP 484 | 代码风格、Python 之禅、类型注解规范 | PEP 8 通读一遍,20 背下来 |
| 并发 | Python 并发编程实战 / asyncio 官方文档 | 线程池、asyncio 模式与陷阱 | 重点看「何时不用并发」那一章 |
| 工程 | 《重构》第 2 版 | 代码坏味道与手法,面试讲代码质量时用得上 | 对照自己项目做一轮重构 |
| 算法 | 《算法第 4 版》(Sedgewick)或 LeetCode 官方题解 | 数据结构与复杂度 | 刷题为主,算法设计技巧(DP/图论)重点学 |
| 系统设计 | 《数据密集型应用系统设计》(DDIA) | 数据库、消息、分布式一致性的原理 | 高级面试重点,慢慢啃 |
| 运维 | Docker 官方文档 + Compose Specification | 规范准确,比二手教程靠谱 | 重点:多阶段构建、健康检查、网络 |
| 安全 | OWASP Top 10 / Python Packaging User Guide | 常见漏洞与包管理规范 | OWASP 每年更新,快速浏览一遍 |
开源项目阅读清单
# 推荐按「小 -> 大」的顺序读源码,每个都只用半天,只看入口与主链路
# 1) 300 行级:搞清楚原理
- https://github.com/psf/requests —— 了解 requests 的 Session 与适配器机制
- https://github.com/pallets/click —— 了解命令行的设计模式(参数如何转成函数调用)
- https://github.com/samuelcolvin/pydantic —— 了解类型注解驱动的运行时校验
# 2) 3000 行级:学习工程组织
- https://github.com/encode/starlette —— 理解 ASGI 的最小实现(本章前面已手写过一个)
- https://github.com/pallets/flask —— 看 WSGI 的中间件链条如何串起来
- https://github.com/psf/black —— 代码格式化与 AST 改写的工程实践
# 3) 大型项目:学设计
- https://github.com/django/django —— 看框架级的中间件、信号、ORM 设计
- https://github.com/sqlalchemy/sqlalchemy —— ORM 的 Session、Unit of Work、Identity Map
- https://github.com/celery/celery —— 分布式任务队列的完整设计
# 阅读方法(推荐)
# 1. 先跑起来,看它解决什么问题
# 2. 从 CLI 入口往下追三层,看主链路
# 3. 挑一个你不懂的点,问 AI 或查文档,然后回去读那一段
# 4. 写下「我理解的设计取舍」,这是面试最好的素材
刷题与面经资源的使用方法
- 按专题分类刷,而不是随机刷。本章前面列的高频题型就是最好的分类:数据结构 20%、并发 15%、数据库 20%、Web 与工程 20%、算法其余 25%。
- 先想 10 分钟再查答案。看答案的学习效率约为独立思考的 30%。
- 每道题写下复杂度(时间、空间)与一句「如果输入规模翻 10 倍会怎样」。
- 面经按目录学,同一题在不同公司可能问法不同,但考点相同。遇到不会的记下来,24 小时内补课。
- 每 5 道错题整理一次,错因分为:概念不清 / 没想到边界 / 手写不熟 / 语言特性没掌握。四类错因的补救方式完全不同。
技巧建立个人的「错题本」与「八股卡壳本」两个 Markdown 文件,按本章六个方向分类。面试前一周只翻这两个文件,效果比重新刷 100 道题好得多。
常见反模式与临场应对
| 反模式 | 表现 | 怎么改 |
| 背答案 | 换一种问法就答不上 | 每题都手写一遍最小可运行版本 |
| 只学不用 | 概念全会,项目写不出 | 每周至少一个可运行产出 |
| 只做加法 | 堆框架,不懂原理 | 用一次标准库重写你用框架写的功能 |
| 假装精通 | 一问底层就崩 | 明确边界,说清「我用过什么程度」 |
| 刷题不写 | 看懂但写不出 | 手写代码必须 IDE 之外手敲一遍 |
| 项目无法深讲 | 面试官追问细节就露馅 | 每个项目写 3 份材料:架构图、关键取舍、事故复盘 |
| 简历注水 | 背调或深挖时崩盘 | 只写能扛住 5 分钟追问的内容 |
面试前一天的三件事
- 重读自己的两个项目:把架构图默画一遍,确认每个技术选型都能说清「为什么不用另一个方案」。
- 翻错题本与卡壳本:只过标记为「必须会」的 20 题。
- 准备好反问面试官的两个问题,例如:「团队目前的代码质量与部署流程是怎么做的?」「这个岗位半年内最期望达成的目标是什么?」——这会显著提升面试官的印象分。
本章小结
- 答题框架:复述确认 → 一句话结论 → 分点展开 → 反向验证 → 主动补刀;不会时展示推理路径而不是硬编。
- 语言基础:推导式与生成器的惰性差异、可变默认参数的求值时机、拷贝的深浅规则、GIL 的历史与现状、装饰器的定义时执行、MRO 与 C3 线性化、
__eq__ 与 __hash__ 的绑定规则。
- 数据结构:list/tuple/set/dict 的底层与复杂度、deque 的单调队列、lru_cache 原理、稳定性、字典保序的版本演进。
- 并发:线程/进程/协程的选型标准由「CPU 密集 vs IO 密集」决定;事件循环里不能调用同步阻塞库;死锁四个必要条件与五种规避方式;进程池必须用可 pickle 的顶层函数。
- 数据库:四种隔离级别与幻读、索引失效场景、深分页的游标方案、连接池超时是最常见的「数据库慢」信号、Redis 分布式锁必须用 Lua 保证比对与删除的原子性、缓存一致性的「先更新 DB 再删缓存」。
- Web:WSGI 与 ASGI 的本质差异、四个必备中间件(请求 ID、错误处理、限流、幂等)、状态码与错误响应结构、测试的四个高频追问。
- 学习路线:13 周四阶段(打地基 → 上强度 → 做项目 → 冲面试),每阶段有可验证产出物;每天 2~3 小时在周末补足 4~5 小时。
- 资料:官方文档优先,Fluent Python 补原理,DDIA 攻高级系统设计,开源源码按小到大读三级。
- 反模式:背答案、只学不用、假装精通、刷题不写码、项目讲不透——每一种都有明确的补救动作。
- 面试本质:不是考你知道多少,而是考你能否被信任把事情做对。诚实标注能力边界,反而是最专业的表现。
练习题
-
口述训练:GIL 专题:用不超过 3 分钟讲清「什么是 GIL、它锁的是什么、为什么 CPython 要有它、它带来了什么后果、有哪些缓解手段」。然后用一段代码验证你的说法:分别用线程与进程做 CPU 密集任务,打印耗时对比。
参考答案要点必须包含:GIL 是 CPython 的实现细节而非语言规范;它保证同一时刻只有一个线程执行字节码;原因是 CPython 用引用计数管理内存,非原子操作需互斥保护;后果是纯 CPU 密集的多线程无法并行,但 IO 等待期间会释放所以 IO 密集仍可并发;缓解手段按优先级为——多进程、multiprocessing、释放 GIL 的 C 扩展(NumPy 的大运算)、3.13+ 的自由线程构建、以及把瓶颈下推到数据库/Redis。验证代码见本章 s1 第 4 题的 bench()。
-
手写题:从零实现(不许用内置函数):① 一个单线程安全的 LRU 缓存类;② 一个支持超时与重试的装饰器,必须保留原函数
__name__ 与 __doc__;③ 一个能对任意可迭代对象做分块(chunk)且支持惰性求值的生成器。要求:每题写测试用例。
参考答案要点LRU 用 collections.OrderedDict + move_to_end + popitem(last=False),或用 dict(3.7+ 保序)+ 双端链表手写;装饰器用 functools.wraps;chunk 生成器写成 def chunked(it, size): it = iter(it); while (chunk := list(itertools.islice(it, size))): yield chunk;测试至少覆盖:容量淘汰顺序、缓存命中计数、超时后异常抛出、__name__ 保持、最后一个不满块的边界。
-
缓存系统设计题:为「商品详情」设计缓存层,要求:① 说明缓存穿透/击穿/雪崩的成因与对策;② 给出缓存与数据库的一致性保证方案并指出其代价;③ 给出热点 key 的保护策略;④ 说明 Redis 内存超限时的降级路径。写出可运行的核心代码。
参考答案要点穿透——查不存在的 key:用布隆过滤器前置拦截 + 空值短 TTL 缓存;击穿——热点 key 过期:single-flight(Redis SET NX 锁)或逻辑过期不真正删 key;雪崩——大批 key 同时过期:TTL 加随机抖动 + 多级缓存 + 服务降级返回兜底数据。一致性:Cache-Aside「先更新 DB,再删除缓存」,代价是删除失败需要重试队列,可加延迟双删应对主从延迟。热点保护:本地缓存 + 互斥重建 + 永不过期逻辑 key。内存超限:maxmemory-policy allkeys-lru 淘汰 + 命中率骤降告警 + 直接回源(要有数据库保护:限流/熔断)。
-
SQL 与索引实战:给出一张
orders(id, user_id, status, amount, created_at)(100 万行),写 SQL 并分析:① 查询某用户近 30 天已支付订单(走什么索引);② 按金额分页且每页 20 条(如何避免深分页);③ 统计每个状态的数量(能否走索引,为什么);④ 删除 3 年前的数据(如何不锁表)。
参考答案要点①建 idx_user_time (user_id, created_at) 走索引,且 created_at > ? 作为范围条件正好命中联合索引第二列;②深分页用游标:WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT 20,并建 (created_at, id) 索引;③SELECT status, count(*) FROM orders GROUP BY status 在有 status 索引时可用索引扫描(覆盖),MySQL 5.7+ 与 PG 支持,但全表 count 无意义,属于「能走索引但不该走」;④分批删除:DELETE FROM orders WHERE created_at < '2023-01-01' LIMIT 5000 循环执行,或用 WHERE id IN (SELECT id ... LIMIT 5000),避免一次锁百万行导致长时间阻塞与大量 undo。
-
并发场景设计题:一个「秒杀」接口要支撑 1 万并发下单,库存 100 件。请给出技术方案:数据库层面怎么保证不超卖?Redis 层面怎么优化?用户侧怎么防刷?并说明每一步的取舍。
参考答案要点数据库层最终兜底:UPDATE stock SET available = available - 1 WHERE sku_id = ? AND available > 0 靠行锁与条件判断保证不超卖,或用 SELECT ... FOR UPDATE 悲观锁(吞吐低),更常用「Redis 预扣 + MQ 异步下单 + 定时对账补偿」。Redis 层:DECR 原子扣减(Lua 脚本保证判断与扣减原子),热点 key 前置到本地缓存或做 key 分散(stock_1..stock_9 分桶降低单 key 压力);下单资格用 Redis Set 的 SISMEMBER + SADD 的原子操作(Lua)限制一人一单。用户防刷:设备指纹 + 手机号 + IP 三维度限流、验证码/滑块、请求签名防伪造、行为验证。取舍说明:纯 Redis 方案快但有一致性风险(扣了库存消息丢了),必须设计「库存预扣 + 超时未支付回补 + 定时对账」的补偿链路;纯数据库方案简单可靠但吞吐有限(数千 QPS 就吃力)。
-
项目包装题:选你做过的(或本章练习做过的)一个项目,按「背景 → 方案 → 取舍 → 结果 → 反思」五段式写一份 800 字的项目描述,其中「取舍」必须包含至少 3 个具体的技术选型对比(例如「为什么用 Redis 而不是直接查库」「为什么用异步任务而不是同步处理」「为什么不用微服务」)。
参考答案要点结构模板:
①背景——业务痛点与量级(QPS、数据规模、团队规模);
②方案——架构分层与关键技术;
③取舍——3 个「A vs B vs C」的决策,每个都要说出当时的选择理由 + 现在的回看(哪怕有回看也承认「当时考虑不周,现在会改成 X」);
④结果——量化指标(延迟从 X 降到 Y、QPS 从 A 到 B、故障率下降);
⑤反思——踩过的坑与改进(这一段最能体现成长,面试官最爱听)。
写作提示:每个技术名词后面跟一句「它解决了什么问题」,否则听起来像在背技术栈。
-
源码阅读报告:读
requests 源码的 sessions.py 与 adapters.py(或 starlette 的 applications.py),回答:① Session 相比直接调 requests.get 多了什么能力?② 连接池是如何复用的?③ 重试是怎么实现的?输出一份 500 字报告并画出调用链。
参考答案要点Session 的能力:默认配置持久化(headers/params/hooks/auth 跨请求继承)、Cookie 自动管理与跨域保持、连接池复用(底层 HTTPAdapter 持有 urllib3.PoolManager)、默认的 RedirectMixin 重定向处理、更方便的上下文管理(with Session() as s: 会自动 close())。连接池复用:HTTPAdapter.send 调用 poolmanager.connection_from_url(url),相同 host:port:scheme 得到同一个连接池;requests.get 每次新建 Session 与 PoolManager,因此无法复用连接(这是性能问题的常见来源)。重试:urllib3.util.retry.Retry 对象配置在 adapter 上,支持按状态码或连接错误重试、指数退避、通过 mount() 挂到 Session 上。调用链:Session.request → send(准备 request)→ adapter.send → urllib3 → socket 层。
-
综合实战:三个月计划的个人版本:根据你当前的真实水平(第几章、每天能投入多少小时、目标岗位是什么),写出一份属于自己的 13 周计划。要求:① 每周有明确产出物;② 每周包含算法题数量与面经题数量;③ 第 11 周必须有一个可用 Docker 一键部署的项目;④ 计划要包含 2 个「失败预案」(某周没完成时怎么补)。
参考答案要点计划模板示例(在职转行版):
W1-2 每天 2 小时学语法+数据结构,周末 5 小时做「命令行待办 + 通讯录」;每天 10 道简单题。
W3-4 函数/OOP/异常,每天 3 道中等题,周末做「成绩管理系统 + 补 100 道题」。
W5-6 标准库+并发,每天 2 道中等题,周末做「并发爬虫」,对比三种并发模型耗时。
W7-8 数据库+Web,手写 30 条 SQL,周末做「博客 API + 鉴权」。
W9-10 测试与性能,给已有项目补 pytest 覆盖 80%,做一次 py-spy 定位实战。
W11 Docker+Nginx+CI/CD,把博客 API 容器化并写好部署文档。
W12-13 面经冲刺 + 简历 + 3 次模拟面试。
失败预案示例:若 W6 未完成并发爬虫,则把 W7 的数据库学习减半顺延,优先保证 W11 的可部署项目完成(因为它是简历上最硬的证据)。每周日晚上做 15 分钟的进度复盘,只调整下周计划,不修改整体目标。