Day 001 — Python 基础全通:从解释器到 asyncio,面试一个都不放过
秋招大幕拉开,作为今年的应届毕业生,我也正式迎来了自己的‘战场’。作为一名技术岗求职者,我深知除了日常的代码编写与文档阅读,将知识内化并灵活运用才是核心竞争力。尽管在AI时代,信息检索变得前所未有的便捷,但我依然坚持通过亲手梳理和撰写文档来构建自己的知识体系。因此,在接下来的半个多月里,我将系统性地整理‘Java后端’与‘AI应用开发(Agent方向)’两大核心领域的面试知识点。这不仅是为了强化记忆、疏通知识脉络,更是为了在面试中展现出扎实的技术底蕴,从而顺利斩获心仪的Offer
📅 2026-07-15 | 🏷️ Python · AI Agent 方向 · AI应用开发 | 🎯 覆盖面试中 90% 的 Python 基础八股
📌 先看地图:今天要拿下什么
Python 面试八股全貌(基础篇)
│
├── 🔴 必考(面试出现率 >80%)
│ ├── is vs ==、可变/不可变对象、深浅拷贝
│ ├── 装饰器(手写)、闭包、*args/**kwargs
│ ├── GIL 是什么 & 怎么绕过
│ ├── 垃圾回收机制(引用计数 + 标记清除 + 分代回收)
│ └── asyncio 原理 & 与多线程的区别
│
├── 🟡 高频(面试出现率 50%-80%)
│ ├── __new__ vs __init__、super()/MRO
│ ├── 迭代器 vs 生成器、yield from
│ ├── 列表推导式 vs 生成器表达式
│ ├── try-except-else-finally 执行顺序
│ └── Pydantic BaseModel 数据校验
│
└── 🟢 加分项(面试出现率 30%-50%)
├── 元类(metaclass)概念 & 应用场景
├── __slots__ 内存优化
├── 上下文管理器原理(__enter__/__exit__)
└── typing 泛型 & Protocol
一、Python 是怎么跑起来的?(必考)
1.1 一句话说清楚
Python 是解释型、动态强类型语言。CPython 先把 .py 编译成 .pyc 字节码,再由基于栈的 PVM(Python 虚拟机)逐条解释执行。
对比其他语言,感受一下 Python 在哪:
| 语言 | 执行方式 | 类型系统 | 代表场景 |
|---|---|---|---|
| Python (CPython) | 编译→字节码→PVM解释 | 动态强类型 | AI、脚本、Web后端 |
| Java | 编译→字节码→JVM(JIT编译) | 静态强类型 | 企业后端、Android |
| JavaScript (V8) | JIT直接编译为机器码 | 动态弱类型 | 浏览器、Node.js |
| C/C++ | 直接编译为机器码 | 静态强类型 | 操作系统、游戏引擎 |
1.2 几个 Python 解释器,各有什么本事?
| 解释器 | 核心技术 | 速度 | 什么时候用 |
|---|---|---|---|
| CPython | C 语言实现,参考标准 | ⭐⭐ | 99% 的时候用它,生态最全 |
| PyPy | JIT 编译,比 CPython 快 4-5 倍 | ⭐⭐⭐⭐ | 纯 Python 计算密集任务 |
| Jython | 跑在 JVM 上,能调 Java 类库 | ⭐⭐ | 需要跟 Java 生态系统集成 |
| RustPython | Rust 实现,实验性 | ⭐⭐ | 嵌入 Rust 应用 |
💡 面试官追问:“AI 开发为什么用 CPython 而不用 PyPy?”
满分回答:因为 PyTorch/TensorFlow 的矩阵运算实际发生在 C/C++/CUDA 层,Python 只是胶水层。PyPy 的 JIT 加速不了 C 扩展调用,反而跟 C 扩展的兼容性不如 CPython。用 CPython + C 扩展的组合,本质上是"用 Python 写逻辑,用 C/C++ 跑计算"。
1.3 GIL — 面试绕不开的坎
GIL(Global Interpreter Lock):CPython 里的一把大锁,同一时刻只允许一个线程执行 Python 字节码。
问题:为什么 Python 多线程不能利用多核?
答案:因为 GIL。
CPU 密集型(计算、图像处理)
→ 多线程无效,GIL 导致线程串行
→ ✅ 用 multiprocessing(多进程),每个进程有独立 GIL
IO 密集型(网络请求、文件读写、数据库查询)
→ 线程在等 IO 时主动释放 GIL,多线程有效
→ ✅ 直接用 threading,更轻量的用 asyncio(协程)
# 一图胜千言:验证 GIL 的存在
import time, threading
def count(n=10**7):
"""纯 Python 计算——CPU 密集型"""
for _ in range(n): pass
# 单线程
t0 = time.time(); count(); count()
print(f"串行: {time.time()-t0:.1f}s")
# 双线程(期望减半,实际不减——GIL 的锅)
t0 = time.time()
t1 = threading.Thread(target=count); t2 = threading.Thread(target=count)
t1.start(); t2.start(); t1.join(); t2.join()
print(f"多线程: {time.time()-t0:.1f}s ← 几乎等于串行,GIL 导致!")
🎤 面试标准回答模板:
“GIL 是 CPython 的历史设计选择,初衷是简化内存管理——引用计数不需要加锁。代价是 CPU 密集型多线程无法利用多核。绕过方案有三:CPU 密集用 multiprocessing(多进程),IO 密集用 threading 或 asyncio(协程),或者把计算密集型逻辑写成 C 扩展(在 C 层可以主动释放 GIL)。FastAPI 的高并发就是靠 asyncio 单线程协程模型,天然不受 GIL 困扰。”
1.4 内存管理:垃圾回收三部曲
| 机制 | 原理 | 特点 |
|---|---|---|
| 引用计数 | 每个对象维护被引用次数,归零立即释放 | 主力,实时,但处理不了循环引用 |
| 标记清除 | 从 GC Roots 出发标记可达对象,清除不可达的 | 兜底,专门解决循环引用 |
| 分代回收 | 对象分 Gen0/1/2 三代,新生代扫得勤,老年代扫得少 | 优化,基于"大多数对象朝生夕死" |
import sys, gc
# 引用计数 —— 实时生效
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2(a 持有 + getrefcount 参数临时持有)
# 循环引用 —— 引用计数解决不了
class Node:
def __init__(self): self.ref = None
x = Node(); y = Node()
x.ref = y; y.ref = x # 互相引用
del x; del y # 此时引用计数 ≠ 0,但对象已不可达
gc.collect() # 标记清除会找到并清理它们
# 分代阈值
print(gc.get_threshold()) # (700, 10, 10)
# Gen0 中对象数超 700 → 触发 Gen0 GC
# Gen0 GC 每 10 次触发 1 次 Gen1 GC
# Gen1 GC 每 10 次触发 1 次 Gen2 GC
🧠 记忆口诀:「引用计数主力军,标记清除抓循环,分代回收提效率」
二、数据类型:面试翻车高发区(必考)
2.1 核心认知:一切皆对象,变量只是标签
# Python 没有"基本类型",连整数都是对象
print(type(42)) # <class 'int'>
print(42 .__add__(1)) # 3 — 整数也有方法
print(isinstance(42, object)) # True — int 继承自 object
print(isinstance(int, object)) # True — 类型本身也是对象
2.2 一张表看懂所有内置类型
| 类型 | 可变? | 示例 | 底层实现 | 常用场景 |
|---|---|---|---|---|
int |
不可变 | 42 |
任意精度整数 | 计数、索引 |
float |
不可变 | 3.14 |
C double (IEEE 754) | 数值计算 |
str |
不可变 | "hello" |
紧凑 Unicode 数组 | 文本处理 |
bytes |
不可变 | b"hello" |
不可变字节序列 | 网络传输、文件IO |
tuple |
不可变 | (1, 2) |
定长数组 | 多返回值、字典 key |
list |
可变 | [1, 2] |
动态数组(预分配) | 数据收集 |
dict |
可变 | {"a":1} |
哈希表(开放寻址) | 映射、缓存、配置 |
set |
可变 | {1, 2} |
哈希表(只存key) | 去重、集合运算 |
bool |
不可变 | True/False |
int 子类(True=1) | 条件判断 |
⚠️ 面试最容易踩的坑:记住 str 和 tuple 是不可变的!所有"修改"都是创建新对象。
2.3 可变 vs 不可变 —— 四个经典陷阱
# ┌─────────────────────────────────────────────┐
# │ 陷阱 1:默认参数只计算一次(字节高频题!) │
# └─────────────────────────────────────────────┘
def add_user(name, users=[]):
users.append(name)
return users
print(add_user("Alice")) # ['Alice'] ← 正常
print(add_user("Bob")) # ['Alice', 'Bob'] ← 预期 ['Bob'],翻车!
# 原理:def 语句执行时 [] 创建一次,此后所有调用共享同一 list
# 修复:
def add_user_fixed(name, users=None):
if users is None:
users = []
users.append(name)
return users
# ┌─────────────────────────────────────────────┐
# │ 陷阱 2:list *= n 的浅拷贝效应 │
# └─────────────────────────────────────────────┘
grid = [[0]] * 3 # 内层 3 个引用指向同一个 [0]!
grid[0][0] = 99
print(grid) # [[99], [99], [99]] ← 全变了!
# 正确写法:
grid = [[0] for _ in range(3)]
# ┌─────────────────────────────────────────────┐
# │ 陷阱 3:tuple "不可变"但存的 list 可以变 │
# └─────────────────────────────────────────────┘
t = (1, 2, [3, 4])
t[2].append(5) # ✅ 合法 — 改变的是 list 的内容,不是 tuple 的引用
# t[0] = 99 # ❌ TypeError — 不能改变 tuple 存储的引用
print(t) # (1, 2, [3, 4, 5])
# ┌─────────────────────────────────────────────┐
# │ 陷阱 4:a += b 对可变/不可变的行为不同 │
# └─────────────────────────────────────────────┘
a = [1, 2]
old = id(a)
a += [3] # 可变对象:__iadd__ 原地修改,id 不变
print(id(a) == old) # True
b = (1, 2)
old_b = id(b)
b += (3,) # 不可变对象:没有 __iadd__ → 回退到 b = b + (3,),id 变了
print(id(b) != old_b) # True
2.4 is vs == vs 深浅拷贝 —— 面试必问三连
一句话总结:
| 操作 | 比较什么 | 调用方法 | 一句话 |
|---|---|---|---|
== |
值相等 | __eq__ |
“你跟我长得一样吗?” |
is |
身份相等 | 比较 id() |
“你就是我吗?” |
= (赋值) |
共享同一对象 | — | “给你也拿一张我的名片” |
copy (浅拷贝) |
新外壳,共享内脏 | copy.copy() |
“克隆身体,内脏共用” |
deepcopy (深拷贝) |
内外全部独立 | copy.deepcopy() |
“连内脏都重新 3D 打印” |
# === is vs == 判断规则 ===
# ✅ 必须用 is 的:None, True, False(都是单例)
# ✅ 应该用 is 的:枚举、模块、类对象(也是单例)
# ✅ 必须用 == 的:字符串比较、数值比较、自定义值对象
x = None
if x is None: # ✅ 正确
pass
# if x == None: # ❌ 危险 — __eq__ 可能被重载
# === 整数缓存:-5 ~ 256 ===
a, b = 256, 256
print(a is b) # True — CPython 启动时就创建好了 -5 到 256 的整数
a, b = 257, 257
print(a is b) # False — 超出缓存范围,是两个独立对象
# === 字符串驻留(Interning)===
s1 = "hello"; s2 = "hello"
print(s1 is s2) # True — 编译期驻留(字面量、短字符串、合法标识符格式)
s3 = "hello world!"; s4 = "hello world!"
print(s3 is s4) # 一般是 False — 带空格/特殊字符不自动驻留
2.5 常用容器操作 —— 写 Agent 天天用
# ━━━ 列表 ━━━
lst = [1, 2, 3]
lst.append(4) # O(1) 尾部
lst.extend([5,6]) # O(k) 批量
lst.insert(0, 0) # O(n) ⚠️ 头部慢
lst.pop() # O(1) 尾部
lst.pop(0) # O(n) ⚠️ 头部慢
# 切片 = 全新列表
sub = lst[::-1] # 逆序
# ━━━ 字典 ━━━
d = {"a": 1}
d.get("z", 0) # 安全取值(Agent解析LLM返回必备)
d.setdefault("b", 2) # 没有则设默认值
from collections import defaultdict, Counter
dd = defaultdict(list) # 不存在 key 自动 []
c = Counter("abracadabra") # 计数神器
# ━━━ 集合 ━━━
{1,2} & {2,3} # {2} 交集
{1,2} | {2,3} # {1,2,3} 并集
{1,2} - {2,3} # {1} 差集
三、流程控制 & 推导式:Python 的优雅所在(高频)
3.1 那些不常用但面试爱考的控制流
# for-else:循环正常跑完(没被 break)触发 else
for item in data:
if matches(item):
break
else:
print("没找到匹配项") # ← 没 break 才执行
# match-case(3.10+,替代多层 if-elif)
def handle_http(code):
match code:
case 200: return "OK"
case 301 | 302: return "Redirect" # | = 或
case 404: return "Not Found"
case code if 500 <= code < 600: return "Server Error" # 守卫
case _: return "Unknown" # _ = 通配
# 海象运算符 := (在表达式里赋值,3.8+)
if (n := len(data)) > 100:
print(f"数据偏大: {n} 条")
3.2 推导式四兄弟 —— Agent 处理数据的瑞士军刀
# ━━━ ① 列表推导式 ━━━
squares = [x**2 for x in range(10)] # [0,1,4,9,...]
evens = [x for x in range(20) if x % 2 == 0] # 带过滤
# Agent 实战:提取 LLM 响应中所有 tool_calls
calls = [c for c in response if c["type"] == "function"]
# ━━━ ② 字典推导式 ━━━
d = {k: v**2 for k, v in zip("abc", range(3))} # {'a':0,'b':1,'c':4}
# 翻转键值
reversed_dict = {v: k for k, v in d.items()}
# ━━━ ③ 集合推导式 ━━━
unique = {x % 3 for x in range(100)} # {0, 1, 2}
# ━━━ ④ 生成器表达式 ━━━
gen = (x**2 for x in range(10**7)) # 内存几乎 0 占用!
# 常跟 sum/max/min/any/all 搭配
total = sum(x**2 for x in range(10**7))
# ⚠️ 关键区别:
# 列表推导式:[] → 立即求值 → 占内存 → 可多次遍历
# 生成器表达式:() → 惰性求值 → 省内存 → 只能遍历一次
3.3 解包技巧
# 星号解包
first, *middle, last = range(10) # first=0, middle=[1..8], last=9
a, b = b, a # 一行交换(右边先算成 tuple)
# 合并字典
merged = {**d1, **d2} # 重复 key 以后者为准
# 函数参数展开
args = [1, 2, 3]
print(*args, sep=", ") # → 1, 2, 3
四、函数:从入门到装饰器(必考)
4.1 参数传递——到底传值还是传引用?
标准答案:Python 只有一种传递方式——传对象引用(Pass by Object Reference / Call by Sharing)。
def explain_params(lst, num):
lst.append(4) # ① 原地修改 → 外部可见
lst = [7, 8] # ② 重新赋值 → 只改变局部变量指向,外部不可见!
num += 1 # ③ 不可变对象 → 创建新对象,外部不可见
my_list = [1, 2, 3]
my_num = 10
explain_params(my_list, my_num)
print(my_list) # [1, 2, 3, 4] — ①生效了,②没生效
print(my_num) # 10 — ③没生效
🧠 记忆口诀:「传引不传值,原地改可见,重指不管用」
4.2 args 和 kwargs
def universal(a, b, *args, default=0, **kwargs):
"""
a, b → 位置参数(必填)
*args → 多余位置参数 → tuple(可选)
default → 关键字参数(可选)
**kwargs → 多余关键字参数 → dict(可选)
"""
print(a, b, args, default, kwargs)
universal(1, 2, 3, 4, 5, default=10, x=99)
# a=1, b=2, args=(3,4,5), default=10, kwargs={'x':99}
# 强制关键字参数(* 后面的只能按关键字传)
def configure(host, *, port=80, debug=False):
pass
configure("localhost", port=8080) # ✅
# configure("localhost", 8080) # ❌ port 必须关键字传
4.3 闭包 & 闭包陷阱
# 闭包 = 函数 + 它捕获的外部变量
def make_counter():
count = 0 # 外部变量,被内部函数"捕获"
def counter():
nonlocal count # 声明要修改外层变量(不可变对象必须 nonlocal)
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c()) # 1, 2, 3
# ⚠️ 经典闭包陷阱(面试极高频!)
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] ← 不是 [0, 1, 2]!
# 原因:lambda 捕获的是变量 i 本身,不是值。循环结束时 i = 2
# 修复:用默认参数在定义时"快照"值
funcs = [lambda i=i: i for i in range(3)] # [0, 1, 2]
4.4 装饰器 —— 面试手写高频题!
import functools
import time
# ═══════════════════════════════════════
# ① 最简装饰器(无参)
# ═══════════════════════════════════════
def timer(func):
@functools.wraps(func) # ← 保留原函数 __name__/__doc__,面试必加!
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时: {time.time()-start:.2f}s")
return result
return wrapper
@timer
def slow_api():
time.sleep(0.5)
return "done"
# @timer 等价于 slow_api = timer(slow_api)
# ═══════════════════════════════════════
# ② 带参数装饰器(三层嵌套)
# ═══════════════════════════════════════
def retry(max_times=3, delay=1):
"""失败自动重试,指数退避"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(max_times):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_times - 1:
raise # 最后一次还失败就抛出
print(f"[重试 {attempt+1}] {e}")
time.sleep(current_delay)
current_delay *= 2 # 指数退避
return None
return wrapper
return decorator
@retry(max_times=3, delay=0.5)
def call_llm_api(prompt):
"""模拟不稳定的 LLM API 调用"""
import random
if random.random() < 0.7:
raise ConnectionError("API 超时")
return "LLM response"
# ═══════════════════════════════════════
# ③ 类装饰器(单例模式)
# ═══════════════════════════════════════
class Singleton:
def __init__(self, cls):
self.cls = cls
self._instance = None
def __call__(self, *args, **kwargs):
if self._instance is None:
self._instance = self.cls(*args, **kwargs)
return self._instance
@Singleton
class AppConfig:
def __init__(self):
self.debug = False
c1 = AppConfig(); c2 = AppConfig()
print(c1 is c2) # True
🎤 面试官追问:“
@functools.wraps是干什么的?不加会怎样?”答:它把原函数的
__name__、__doc__、__module__等元信息复制到 wrapper 上。不加的话slow_api.__name__会变成'wrapper',调试、日志、依赖函数名做路由的场景(如 Flask/FastAPI 的 route 装饰器)全都会翻车。
五、面向对象:MRO、super、元类(高频 + 加分)
5.1 __new__ vs __init__ vs __call__
触发时机:
MyClass(...) → ① type.__call__ → ② __new__(创建对象) → ③ __init__(初始化对象)
一句话:
__new__ = 生(分配空间,返回实例)
__init__ = 养(填充属性,无返回值)
__call__ = 让实例能被"调用",obj()
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance # 每次返回同一个对象
def __init__(self, val):
self.val = val # ⚠️ 每次调用都会执行,会覆盖之前的值
s1 = Singleton(1)
s2 = Singleton(2)
print(s1 is s2) # True — 同一个对象
print(s2.val) # 2 — __init__ 每次都执行,1 被覆盖了
5.2 MRO(方法解析顺序)& super()
MRO = Method Resolution Order,Python 用 C3 线性化算法确定多继承时的属性查找顺序。
class A:
def method(self): print("A")
class B(A):
def method(self): print("B"); super().method()
class C(A):
def method(self): print("C"); super().method()
class D(B, C):
pass
print(D.__mro__) # D → B → C → A → object
D().method()
# 输出:B → C → A
# 关键认知:super() 不直接调用父类!它按 MRO 链找"下一个"。
# B 的 MRO 下一个是 C(不是 A),所以 B 的 super() 调到 C.method()
5.3 常用魔法方法速查
| 魔法方法 | 触发 | Agent 开发中的用途 |
|---|---|---|
__str__ |
print(obj), str(obj) |
日志输出 |
__repr__ |
调试, repr(obj) |
调试信息 |
__eq__ / __hash__ |
==, hash() |
对象比较、去重、dict key |
__call__ |
obj() |
让对象像函数(Agent 工具注册常用) |
__getitem__ |
obj[key] |
自定义容器 |
__enter__/__exit__ |
with obj: |
资源管理(Tool 生命周期) |
__iter__/__next__ |
for x in obj |
流式输出、数据管道 |
5.4 @property 装饰器
class Celsius:
def __init__(self, temp=0):
self._temp = temp
@property
def temp(self):
"""摄氏度"""
return self._temp
@temp.setter
def temp(self, value):
if value < -273.15:
raise ValueError("低于绝对零度!")
self._temp = value
@property
def fahrenheit(self):
"""华氏度(计算属性,只读)"""
return self._temp * 9/5 + 32
t = Celsius(25)
print(t.temp) # 25 — 像属性一样访问
print(t.fahrenheit) # 77.0 — 自动计算
t.temp = 30 # setter 校验
5.5 元类 —— 了解即加分
元类 = 创建类的类。type 是默认元类。什么时候用?ORM 框架、单例注册、API 自动注册。
# 元类版单例
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
pass
db1, db2 = Database(), Database()
print(db1 is db2) # True
# 面试可以这样说:
# "元类控制类的创建过程。Django ORM 的 ModelBase 就是元类,
# 它扫描 Field 定义自动生成数据库映射。一般业务代码用不到,但理解它有助于读懂框架源码。"
5.6 __slots__ —— 省内存
class Point:
__slots__ = ('x', 'y') # 禁止 __dict__,实例只能用 x, y
def __init__(self, x, y):
self.x, self.y = x, y
# p = Point(1, 2); p.z = 3 # AttributeError!
# 优势:省内存(无 __dict__),属性访问更快(直接偏移而非哈希查找)
# 代价:不能动态加属性,多继承受限
六、异常处理 & 上下文管理器(高频)
6.1 try-except-else-finally 执行顺序
def test_order():
try:
print("1. try")
# return "try返回" # 即使有 return...
# raise ValueError()
except ValueError:
print("2. except")
else:
print("3. else") # 无异常才执行
finally:
print("4. finally") # 无论如何都执行(即使 try 有 return!)
return "函数返回"
# 输出顺序: try → else → finally → 函数返回
# 如果有异常: try → except → finally → 函数返回
⚠️ 面试陷阱:
finally中有 return 会覆盖 try/except 中的 return!
6.2 上下文管理器 —— 两种写法
# 方式一:类(经典)
class ManagedFile:
def __init__(self, name):
self.name = name
def __enter__(self):
self.file = open(self.name, 'w')
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
# return False → 不抑制异常;return True → 吞掉异常
# 方式二:contextlib(推荐)
from contextlib import contextmanager, suppress
@contextmanager
def managed_file(name):
f = open(name, 'w')
try:
yield f # ← 产出给 with...as 的变量
finally:
f.close() # ← 无论是否异常都会执行
# 忽略特定异常
with suppress(FileNotFoundError):
os.remove("maybe_not_exist.txt") # 文件不存在也不报错
七、迭代器 & 生成器(高频)
7.1 概念区分
| 概念 | 协议 | 特点 |
|---|---|---|
| Iterable(可迭代对象) | 有 __iter__ |
能被 for 遍历,可复用 |
| Iterator(迭代器) | 有 __iter__ + __next__ |
惰性、一次性、耗尽即止 |
| Generator(生成器) | yield 语法糖 | 自动实现 Iterator 协议 |
from collections.abc import Iterable, Iterator
lst = [1, 2, 3]
print(isinstance(lst, Iterable)) # True — list 是可迭代对象
print(isinstance(lst, Iterator)) # False — list 不是迭代器
print(isinstance(iter(lst), Iterator)) # True — iter() 返回迭代器
7.2 生成器:yield、send、yield from
# ═══ yield 基础 ═══
def gen123():
print("开始")
yield 1
print("继续")
yield 2
print("再继续")
yield 3
print("结束")
g = gen123() # 不执行函数体!只返回 generator 对象
print(next(g)) # "开始" → 1(函数执行到 yield,暂停)
print(next(g)) # "继续" → 2
print(next(g)) # "再继续" → 3
# next(g) # "结束" → StopIteration
# ═══ send:双向通信 ═══
def accumulator():
total = 0
while True:
value = yield total # 右边接收 send 的值,左边产出 total
if value is None:
break
total += value
acc = accumulator()
print(next(acc)) # 0 — 启动生成器(只能传 None)
print(acc.send(5)) # 5
print(acc.send(10)) # 15
# ═══ yield from:子生成器委托 ═══
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # 递归委托
else:
yield item
print(list(flatten([1, [2, [3, 4]], 5]))) # [1, 2, 3, 4, 5]
八、asyncio:Agent 高并发的底座(必考)
8.1 为什么需要 asyncio?
场景:Agent 需要同时调用 3 个工具(搜索、计算、查库)
同步方式:
搜索(1s) → 计算(0.5s) → 查库(0.8s) = 总耗时 2.3s ❌
asyncio 方式:
搜索(1s) ┐
计算(0.5s) ├─ 并发 = 总耗时 max(1, 0.5, 0.8) = 1s ✅
查库(0.8s) ┘
原理:在 await 处让出 CPU,事件循环调度另一个协程运行。
单线程 + 协程 = 没有 GIL 困扰 = 没有锁竞争。
8.2 核心 API
import asyncio
async def fetch(url: str) -> str:
print(f"→ {url}")
await asyncio.sleep(1) # 模拟 IO
print(f"← {url}")
return f"data_from_{url}"
async def main():
# ═══ gather:并发 + 保序 ═══
results = await asyncio.gather(
fetch("url1"),
fetch("url2"),
fetch("url3"),
return_exceptions=True # 某个失败不拖累其他
)
print(results) # ["data_from_url1", "data_from_url2", "data_from_url3"]
# ═══ create_task:更灵活的控制 ═══
task1 = asyncio.create_task(fetch("url4"))
task2 = asyncio.create_task(fetch("url5"))
# 中间可以干别的事...
r1, r2 = await task1, await task2
# ═══ as_completed:谁先回来先处理谁 ═══
tasks = [fetch(f"url{i}") for i in range(5)]
for coro in asyncio.as_completed(tasks):
result = await coro
print(f"最先完成: {result}")
asyncio.run(main())
8.3 Agent 实战:并发工具调用
# 这几乎是 Agent 开发中最常用的模式
class AgentToolExecutor:
def __init__(self):
self.tools: dict[str, callable] = {}
def register(self, name: str, func):
self.tools[name] = func
async def execute(self, name: str, **params):
"""调用单个工具"""
if name not in self.tools:
raise ValueError(f"未知工具: {name}")
return await self.tools[name](**params)
async def execute_all(self, calls: list[dict]) -> list:
"""并发调用多个工具——Agent 的刚需"""
tasks = [
asyncio.create_task(
self.execute(c["name"], **c.get("params", {}))
)
for c in calls
]
return await asyncio.gather(*tasks, return_exceptions=True)
8.4 asyncio vs threading vs multiprocessing
| 维度 | asyncio | threading | multiprocessing |
|---|---|---|---|
| 并发模型 | 单线程 + 协程 | 多线程 + 抢占式 | 多进程 + 独立 GIL |
| 适用场景 | IO 密集 | IO 密集 | CPU 密集 |
| 切换开销 | 极小(函数调用级) | 中等(系统调用) | 大(进程切换) |
| 内存占用 | 极低(KB 级) | 中等(MB 级) | 大(GB 级,独立内存) |
| 数据共享 | 天然安全(无竞态) | 需要锁 | 需要 IPC |
| Agent 开发 | ✅ 首选 | 偶尔用(run_in_executor) | 极少用 |
🎤 面试话术:“Agent 开发中并发调用多个工具是刚需,asyncio 是首选方案:轻量、无锁、天然契合 IO 密集型场景。同步阻塞代码通过
loop.run_in_executor()放到线程池执行,不阻塞事件循环。CPU 密集的计算任务交给 multiprocessing 或者直接下推到 C 扩展层。”
九、typing + Pydantic:Agent 工具系统的基石(高频 + 加分)
9.1 typing 核心语法
from typing import List, Optional, Union, Literal, Any, Callable, TypeVar
# 3.10+ 的简化语法(推荐)
def process(
items: list[str], # 字符串列表
timeout: int | None = None, # 可能为 None
mode: Literal["fast", "thorough"] = "fast", # 限定值
callback: Callable[[str], None] | None = None, # 可选回调
) -> str | None: # 返回值可能为 None
...
# TypeVar: 泛型
T = TypeVar('T')
def first(items: list[T]) -> T: return items[0] # 返回值类型跟参数一致
# Protocol: 结构化子类型(Goose Typing)
from typing import Protocol
class Closable(Protocol):
def close(self) -> None: ...
# 任何实现了 close() 的对象都符合 Closable 协议,无需显式继承
9.2 Pydantic V2 —— Agent 参数校验的标准答案
为什么 Agent 开发必须学 Pydantic?因为 LLM 的 Function Calling 需要 JSON Schema 来定义工具参数,Pydantic 能自动生成这个 Schema。
from pydantic import BaseModel, Field, model_validator
from typing import Literal, Optional
from enum import Enum
# ═══════════════════════════════════════
# ① 工具参数定义(Agent 核心场景)
# ═══════════════════════════════════════
class SearchToolParams(BaseModel):
"""搜索工具参数"""
query: str = Field(..., description="搜索关键词", min_length=1, max_length=500)
source: Literal["web", "news", "scholar"] = Field(default="web")
top_k: int = Field(default=5, ge=1, le=20, description="返回结果数")
language: Optional[str] = Field(default=None, pattern=r"^[a-z]{2}$")
# 一行生成 JSON Schema——直接给 LLM Function Calling 用!
schema = SearchToolParams.model_json_schema()
# {'type': 'object', 'properties': {'query': {'type': 'string', ...}}, ...}
# ═══════════════════════════════════════
# ② 带验证的消息模型
# ═══════════════════════════════════════
class Role(str, Enum):
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
TOOL = "tool"
class Message(BaseModel):
role: Role
content: str = Field(..., min_length=1)
tool_call_id: Optional[str] = None
name: Optional[str] = None
class AgentRequest(BaseModel):
messages: list[Message] = Field(..., min_length=1)
temperature: float = Field(default=0.7, ge=0, le=2.0)
max_tokens: int = Field(default=4096, gt=0, le=128000)
@model_validator(mode="after")
def validate_alternation(self):
"""确保 system 消息在最前面"""
if self.messages and self.messages[0].role != Role.SYSTEM:
raise ValueError("第一条消息必须是 system")
return self
# ═══════════════════════════════════════
# ③ 序列化与反序列化
# ═══════════════════════════════════════
req = AgentRequest(
messages=[Message(role=Role.SYSTEM, content="你是助手")],
temperature=0.5,
)
print(req.model_dump()) # → dict
print(req.model_dump_json()) # → JSON 字符串
# 从 JSON 恢复
req2 = AgentRequest.model_validate_json(
'{"messages":[{"role":"system","content":"你好"}]}'
)
十、今日面试 10 题精练(附公司标签)
每题建议先遮住答案自己说一遍,再看标准回答。
Q1. Python 是值传递还是引用传递?(字节/阿里/腾讯)
标准回答:
Python 是"传对象引用"(Pass by Object Reference / Call by Sharing),既不是 C 的传值(不复制数据),也不是 C++ 的传引用(没有取地址操作)。函数参数获得的是对象引用的副本,指向同一块内存:
- 不可变对象(int/str/tuple):任何"修改"都是创建新对象,外部不受影响
- 可变对象(list/dict/set):原地修改(append、赋值元素)外部可见,但重新赋值(
lst = [...])只改变局部变量指向,外部不可见
Q2. is 和 == 有什么区别?什么时候必须用 is?(美团/字节)
标准回答:
is比较身份(id()是否相等 → 是否同一个对象)==比较值(调用__eq__方法)- 必须用
is:判断None(单例)、True/False(单例)、枚举成员 - CPython 优化:小整数 [-5, 256] 启动时缓存,短字符串编译时驻留,导致某些情况
is意外为 True,不可依赖此行为
Q3. 深浅拷贝的区别?各自怎么实现?(字节高频)
标准回答:
- 浅拷贝(
copy.copy()):创建新容器对象,但容器内元素仍是原对象的引用。内层可变对象修改会互相影响。 - 深拷贝(
copy.deepcopy()):递归复制所有嵌套对象,内外完全独立,通过__deepcopy__协议支持自定义。 - 赋值(
=):不拷贝,两个变量指向同一对象,所有修改都互相影响。
记忆:「复制外壳 = 浅,连内脏都复制 = 深,共享同一具身体 = 赋值」
Q4. Python 的垃圾回收机制?(字节/阿里)
标准回答:
三管齐下——① 引用计数(主力,实时,del 或引用归零立即释放);② 标记清除(兜底,从 GC Roots 标记可达对象,清除不可达对象,专门解决循环引用);③ 分代回收(优化,对象分 Gen0/1/2 三代,新生代高频扫描,老年代低频扫描,基于"大多数对象朝生夕死"的假设)。
gc.collect() 可手动触发;gc.get_threshold() 查看分代阈值;sys.getrefcount() 查看引用计数。
Q5. 装饰器的原理?@functools.wraps 有什么用?(拼多多/快手)
标准回答:
装饰器本质是高阶函数,接受函数作为参数,返回新的函数。@decorator 是语法糖,等价于 func = decorator(func)。
@functools.wraps(func) 将原函数的 __name__、__doc__、__module__ 等元信息复制到 wrapper 上。不加的话所有被装饰的函数 __name__ 都变成 'wrapper',导致 Flask/FastAPI 路由注册失败、调试困难、文档丢失。
Q6. __new__ 和 __init__ 的区别?(阿里/百度)
标准回答:
__new__:类方法(接收 cls),负责创建并返回实例对象,在__init__之前调用。单例模式在__new__中控制。__init__:实例方法(接收 self),负责初始化已创建的对象,不能返回非 None 值。- 流程:
MyClass(...)→type.__call__→__new__(创建)→__init__(初始化)→ 返回实例。
Q7. 迭代器和生成器的区别?yield from 做什么?(字节/腾讯)
标准回答:
- 迭代器:实现了
__iter__和__next__的对象,惰性取值,一次性,不可复用。 - 生成器:用
yield语法糖自动实现迭代器协议的函数。暂停执行、保存状态、双向通信(send())。 yield from:将迭代委托给子生成器,自动处理send()、throw()、close()的透传,简化嵌套生成器写法。
Q8. asyncio 和多线程有什么区别?Agent 开发为什么选 asyncio?(字节/腾讯)
标准回答:
| 维度 | asyncio | threading |
|---|---|---|
| 并发模型 | 单线程协程,await 处主动切换 | 多线程抢占式调度 |
| 切换开销 | 极低 | 中等 |
| GIL 影响 | 无(单线程) | CPU 密集时阻塞 |
| 竞态条件 | 无(非抢占) | 需要锁保护 |
| 内存 | 一个协程几 KB | 一个线程几 MB |
Agent 开发选 asyncio 的原因:① 工具调用是 IO 密集场景;② 单线程无竞态,代码简洁;③ FastAPI 原生支持,生态完善;④ 协程极轻量,轻松支持数千并发工具调用。
Q9. 列表推导式和生成器表达式有什么区别?(字节/美团)
标准回答:
- 列表推导式
[x for x in data]:立即计算,返回完整 list,占内存,可多次遍历 - 生成器表达式
(x for x in data):惰性求值,省内存,只能遍历一次
大数据量优先用生成器。sum()、any()、all() 等聚合函数天然适合传入生成器表达式。
Q10. Pydantic 的 field_validator 和 model_validator 有什么区别?(字节)
标准回答:
field_validator(V2,替代旧版validator):在单个字段解析后运行,修饰器绑定特定字段名model_validator:在整个模型所有字段解析完成后运行,能做跨字段验证(如 password 和 confirm_password 一致性校验)
Agent 开发中:field_validator 校验单个工具参数,model_validator 校验工具参数组合的合理性。
十一、今日代码实战(4 道综合题)
# ═══════════════════════════════════════════════
# 练习 1:基于 OrderedDict 实现 LRU 缓存
# ═══════════════════════════════════════════════
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cap = 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.cap:
self.cache.popitem(last=False) # 淘汰最久未使用
# ═══════════════════════════════════════════════
# 练习 2:异步工具调度器(Agent 核心模式)
# ═══════════════════════════════════════════════
import asyncio
class AsyncToolScheduler:
def __init__(self):
self._tools = {}
def register(self, name: str, func):
self._tools[name] = func
async def call(self, name: str, **params):
if name not in self._tools:
raise ValueError(f"未知工具: {name}")
return await self._tools[name](**params)
async def call_all(self, calls: list[dict]) -> list:
"""并发执行多个工具调用"""
tasks = [
asyncio.create_task(self.call(c["name"], **c.get("params", {})))
for c in calls
]
return await asyncio.gather(*tasks, return_exceptions=True)
# ═══════════════════════════════════════════════
# 练习 3:带指数退避的重试装饰器
# ═══════════════════════════════════════════════
import functools, time
def retry(max_tries=3, base_delay=0.5):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
delay = base_delay
for i in range(max_tries):
try:
return func(*args, **kwargs)
except Exception as e:
if i == max_tries - 1:
raise
print(f"[重试 {i+1}] {e}")
time.sleep(delay)
delay *= 2 # 指数退避
return wrapper
return decorator
# ═══════════════════════════════════════════════
# 练习 4:Pydantic 定义 Agent 工具 Schema
# ═══════════════════════════════════════════════
from pydantic import BaseModel, Field
from typing import Literal, Optional
class WeatherParams(BaseModel):
"""天气查询工具的参数定义"""
city: str = Field(..., description="城市名,如 Beijing", min_length=1, max_length=100)
unit: Literal["celsius", "fahrenheit"] = Field(default="celsius")
forecast_days: int = Field(default=1, ge=1, le=7, description="预报天数(1-7)")
# 自动生成 Function Calling 需要的 JSON Schema
print(WeatherParams.model_json_schema())
# → {'type': 'object', 'properties': {
# 'city': {'type': 'string', 'description': '城市名,如 Beijing', ...},
# 'unit': {'type': 'string', 'enum': ['celsius', 'fahrenheit'], ...},
# 'forecast_days': {'type': 'integer', 'minimum': 1, 'maximum': 7, ...}
# }, 'required': ['city']}
📊 今日知识图谱(睡前对着这张图过一遍)
Python 基础全通 DAY 1
│
├── 🔴 必考四天王
│ ├── GIL:单线程锁 → CPU用多进程、IO用asyncio
│ ├── is(身份) vs ==(值):None必须用is
│ ├── 可变/不可变:4大陷阱(默认参数/list*=/tuple内list/+=差异)
│ └── GC三部曲:引用计数→标记清除→分代回收
│
├── 🟡 高频考点
│ ├── 装饰器手写:无参/有参/类装饰器/wraps
│ ├── 闭包陷阱:lambda捕获变量非值 → 默认参数快照修复
│ ├── __new__ vs __init__:生vs养
│ ├── super()/MRO:C3线性化,super不=父类
│ ├── 生成器:yield暂停/send通信/yield from委托
│ └── asyncio:gather保序/as_completed先到先得
│
└── 🟢 加分项
├── 元类:类的类,ORM/单例用
├── __slots__:省内存
├── Pydantic:自动JSON Schema → LLM Function Calling
└── typing:Protocol结构化子类型
🔜 明日预告
Day 2 — Python 工程化 & FastAPI & 数据库速通
- FastAPI 路由 / 中间件 / 依赖注入 / 异常处理
- SQLAlchemy 异步 ORM + Alembic 迁移
- Redis 五种数据结构 + 缓存穿透/击穿/雪崩方案
- Docker 容器化 + docker-compose
- pytest 测试 + Gunicorn + Nginx 部署
- 面试题:6 道工程化真题
💡 速通心法:今天的目标不是背下所有细节,而是建立知识框架——面试官抛出任何一个点,你都能立刻定位到它属于哪个模块、核心概念是什么。细节在后续每天敲代码中自然内化。睡前对着上面的知识图谱,遮住右边,看左边能不能说出核心内容。说不出的,明天早上第一件事就复习它。
更多推荐



所有评论(0)