秋招大幕拉开,作为今年的应届毕业生,我也正式迎来了自己的‘战场’。作为一名技术岗求职者,我深知除了日常的代码编写与文档阅读,将知识内化并灵活运用才是核心竞争力。尽管在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 argskwargs

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_validatormodel_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 道工程化真题

💡 速通心法:今天的目标不是背下所有细节,而是建立知识框架——面试官抛出任何一个点,你都能立刻定位到它属于哪个模块、核心概念是什么。细节在后续每天敲代码中自然内化。睡前对着上面的知识图谱,遮住右边,看左边能不能说出核心内容。说不出的,明天早上第一件事就复习它。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐