Python后端面试题精选

2026年 阅读约 11 分钟 面试指南 · 后端面试 · Python

Python后端面试题精选:可变/不可变类型、深浅拷贝、GIL与并发模型、装饰器与生成器、asyncio事件循环、引用计数与分代GC、FastAPI与SQLAlchemy工程化,附代码示例与高频问答。

一句话总结

Python 后端 = 语言核心 + Web 框架 + 并发模型 + 数据层 + 工程化。面试重点不是背语法,而是吃透四件事:对象模型(可变/不可变、一切皆对象)、GIL 下的并发取舍、装饰器/生成器/上下文管理器这些一等公民特性,以及FastAPI/SQLAlchemy 的真实工程落地。Python 表达力强、生态全,但"写得快"和"写得对、跑得稳"之间差着一整套底层认知。

# 面试常作开场的三句话: # 1. 一切皆对象:连函数、类、模块都是对象,可被赋值、传参、动态增删属性 # 2. 对象分可变(list/dict/set)与不可变(str/int/tuple/frozenset),这决定了默认传引用还是"看起来像值" # 3. CPython 有 GIL:单进程内同一时刻只有一个线程跑字节码——CPU 密集别指望多线程

初级理解

可变类型与不可变类型(最高频)

类别典型类型能否原地修改
可变(mutable)list、dict、set、bytearray能:append/pop/update 后 id 不变
不可变(immutable)int、float、str、tuple、frozenset、bytes不能:任何"修改"都返回新对象
# 不可变对象的"修改"其实是新建: s = "abc"; print(id(s)); s += "d"; print(id(s)) # 两个 id 不同 # 经典坑:用可变对象做函数默认参数——它只初始化一次,跨调用共享! def add(x, buf=[]): # buf 是同一个列表,多次调用会累积 buf.append(x); return buf add(1); add(2) # 第二次调用得到 [1, 2],不是 [2]

is 与 == 的区别

# == 比值(调用 __eq__);is 比身份(两个变量是否指向同一个对象,即 id 相同) a = [1, 2]; b = [1, 2] print(a == b) # True(值相等) print(a is b) # False(两个不同对象) # 小整数池与字符串驻留:解释器对常用小对象做缓存,导致 is 偶尔为 True(不要依赖!) x = 256; y = 256; print(x is y) # True(小整数池 -5~256) m = 257; n = 257; print(m is n) # False(超出缓存范围)

浅拷贝与深拷贝

import copy a = [1, [2, 3]] b = copy.copy(a) # 浅拷贝:只拷顶层,嵌套的 [2,3] 仍共享 c = copy.deepcopy(a) # 深拷贝:递归拷贝,所有层级都独立 b[1].append(4); print(a) # a 也变成 [1, [2,3,4]]——浅拷贝的副作用 c[1].append(4); print(a) # a 不受影响

*args 与 **kwargs

def f(*args, **kwargs): print(args) # 多余位置参数收成元组:(1, 2, 3) print(kwargs) # 多余关键字参数收成字典:{'a': 4, 'b': 5} f(1, 2, 3, a=4, b=5)

装饰器与生成器(基础形态)

# 装饰器:接收函数、返回新函数,@ 是语法糖 def log(fn): def wrapper(*a, **k): print("call", fn.__name__); return fn(*a, **k) return wrapper @log def add(x, y): return x + y # 生成器:用 yield 惰性产出,省内存、可表达无限序列 def countdown(n): while n > 0: yield n; n -= 1

中级深入

GIL:为什么 Python 还有多线程

# GIL(全局解释器锁)= CPython 的一把进程级锁: # 同一进程内任意时刻只有一个线程能执行字节码。 # 原因:CPython 的内存管理(引用计数)不是线程安全的,加锁最省事。 # # 影响与对策: # - CPU 密集:多线程无效(线程轮流抢锁),用 multiprocessing 多进程绕过 # - IO 密集:线程在等待 IO 时释放 GIL,所以多线程/asyncio 仍能并发 # - C 扩展(numpy/cython)可在计算时释放 GIL # - Python 3.2+ 的 GIL 改为"尽量公平",IO 密集下几乎无感

多线程 / 多进程 / asyncio 怎么选

模型适用开销并行度
多线程 threadingIO 密集(网络/文件)低并发(受 GIL)
多进程 multiprocessingCPU 密集高(每进程独立内存)真并行
协程 asyncio高并发 IO(万级连接)极低单线程并发

迭代器 vs 生成器

# 迭代器:实现 __iter__ 返回自身、__next__ 产出下一个值,遍历完抛 StopIteration # 生成器:含 yield 的函数,调用即返回生成器对象,自动实现迭代器协议 # 区别:生成器是"写出迭代器的最简方式",且惰性、不占全量内存 nums = (x*x for x in range(10)) # 生成器表达式,按需计算,不存 10 个数 print(next(nums), next(nums)) # 0 1

闭包与 LEGB 作用域

# 变量查找顺序:Local → Enclosing → Global → Builtin def outer(x): def inner(): return x # 闭包:inner 捕获了外层 x return inner f = outer(10); print(f()) # 10 # 经典坑:循环里定义闭包引用循环变量,要用默认参数"冻结" funcs = [lambda: i for i in range(3)] print([g() for g in funcs]) # [2, 2, 2]——都引用了同一个最终 i funcs = [lambda i=i: i for i in range(3)] print([g() for g in funcs]) # [0, 1, 2]——默认参数在定义时绑定

垃圾回收:引用计数 + 分代回收

# 1. 引用计数为主:对象引用归零立即回收(快、确定性好) # 2. 分代回收兜底:解决循环引用(a 引用 b、b 引用 a),0/1/2 三代,年轻代更频繁扫描 # 3. gc 模块可手动 gc.collect()、调阈值、排查不可达循环 import gc; print(gc.get_threshold()) # (700, 10, 10) # 循环引用示例: class Node: def __init__(self): self.peer = None a, b = Node(), Node(); a.peer, b.peer = b, a # 引用计数都到不了 0,靠分代回收

高级进阶

asyncio 事件循环与协程

import asyncio async def fetch(n): await asyncio.sleep(1) # 遇到 await 让出控制权,事件循环去跑别的协程 return n * n async def main(): # asyncio.gather 并发跑多个协程,总耗时约等于最慢的一个 return await asyncio.gather(fetch(1), fetch(2), fetch(3)) asyncio.run(main()) # 3 个 1s 任务并发,约 1s 完成(非 3s) # 要点:await 只能在 async 函数内用;CPU 密集别用 asyncio(仍受 GIL), # 应交由线程/进程池 run_in_executor

FastAPI:异步端点、依赖注入与 pydantic

from fastapi import FastAPI, Depends from pydantic import BaseModel app = FastAPI() class Item(BaseModel): # pydantic 模型:自动校验、序列化、生成 OpenAPI name: str; price: float; stock: int = 0 def get_db(): db = {"conn": "..."} # 真实项目里是连接池 try: yield db finally: db.clear() @app.post("/items/") async def create_item(item: Item, db=Depends(get_db)): return {"ok": True, "item": item.model_dump()} # 依赖注入 Depends:把"取数据库连接""鉴权""公共参数"抽成可复用单元,测试时可轻松 mock

SQLAlchemy:Session、N+1 与连接池

# ORM 让 Python 对象与表行互转,但隐藏了 SQL,容易写出 N+1 # N+1:查 1 次拿 N 个对象,再对每对象各查 1 次关联 → 共 N+1 条 SQL # 解决: joinedload(JOIN 一次取出)/ subqueryload / 显式 selectinload from sqlalchemy.orm import joinedload query = select(User).options(joinedload(User.orders)) # 1 条 SQL 搞定 # 连接池:engine = create_engine(url, pool_size=10, max_overflow=20, pool_pre_ping=True) # pool_pre_ping 在借连接前 ping 一下,规避"MySQL 八小时断开"导致的失效连接

描述符与 property 原理

# 描述符:定义了 __get__/__set__/__delete__ 中至少一个的对象,接管属性访问 # property 就是用描述符实现的"托管属性" class Celsius: def __get__(self, obj, owner): return obj._c def __set__(self, obj, val): if val < -273.15: raise ValueError("低于绝对零度") obj._c = val class Temp: c = Celsius() # c 是个描述符,赋值会被 __set__ 拦截校验

实战场景

后端项目分层结构

app/ routers/ # 路由层:只做参数解析与响应封装,不含业务 services/ # 服务层:业务逻辑、事务边界 repositories/ # 仓储层:SQLAlchemy 查询、ORM 交互 schemas/ # pydantic 模型:请求/响应契约 core/ # 配置、依赖、数据库引擎、安全 main.py # 组装 FastAPI 应用 # 关键:路由不直连 DB,业务放在 service,便于单测与复用

配置管理与类型安全

# pydantic-settings:配置从环境变量/`.env` 读取并校验类型 from pydantic_settings import BaseSettings class Settings(BaseSettings): database_url: str redis_url: str = "redis://localhost:6379" debug: bool = False class Config: env_file = ".env" settings = Settings() # 缺失必填项会启动即报错,避免线上才暴露配置错

日志、测试与类型检查

# 日志用标准 logging,结构化(JSON)便于采集;别用 print 上生产 import logging; logging.basicConfig(level=logging.INFO) # pytest + fixtures:用依赖注入思想准备测试上下文 def test_create_item(client, db_session): ... # 类型注解 + mypy:大型项目几乎必备,捕捉"传错类型"这类低级 bug def add(a: int, b: int) -> int: return a + b

依赖与部署

# 依赖管理:venv 隔离 + pip-tools/poetry/uv 锁版本(避免"我本地能跑") python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt # 部署:gunicorn + uvicorn worker(多进程扛 GIL,每个 worker 一个事件循环) # gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 # 容器化:把上述命令写进 Dockerfile 的 CMD,配合健康检查与优雅停机
注意:FastAPI 的 Depends 与同步/异步混用要当心——在 async 路由里调用阻塞的同步 DB 驱动会卡住整个事件循环;要么用异步驱动(asyncpg/aiomysql),要么用 run_in_executor 把阻塞调用丢到线程池。

面试模拟

面试官:Python 有 GIL,那多线程还有意义吗?什么时候该用多进程?

你:有意义,但要分场景。GIL 只在执行字节码时互斥,线程在等待网络/文件 IO 时会释放 GIL,所以 IO 密集任务(Web 请求、爬虫)用多线程或 asyncio 依然能高并发。只有 CPU 密集(大量计算、图像处理)才受 GIL 限制,此时多线程帮不上忙,要用 multiprocessing 多进程拿到真并行,或用 C 扩展释放 GIL。一句话:IO 密集靠并发,CPU 密集靠并行。

面试官:深拷贝和浅拷贝的区别?什么场景必须用深拷贝?

你:浅拷贝只复制顶层对象,内嵌的可变对象(如列表里的列表)仍与原对象共享同一份;深拷贝递归复制所有层级,完全独立。当数据结构嵌套了可变对象、且你修改副本时不想影响原对象(比如复制一份配置再改、复制图结构做变换),就必须用 deepcopy,否则会产生"改一个另一个也变"的隐蔽 bug。

面试官:is 和 == 有什么不同?为什么有时候 a is b 是 True?

你:== 比较值(走 __eq__),is 比较身份——两个变量是否指向同一个对象(id 相同)。偶尔为 True 是因为解释器的对象缓存:小整数(-5~256)和短字符串会被驻留复用,所以两个 256 指向同一对象。但这是实现细节,不应依赖;比较值永远用 ==。

面试官:装饰器原理是什么?functools.wraps 解决什么问题?

你:装饰器本质是高阶函数:接收被装饰的函数,包一层新函数返回,@ 只是语法糖。问题是直接返回 wrapper 会丢失原函数的 __name__/__doc__ 等元信息,导致日志、调试、反射拿到的是 wrapper。functools.wraps 是个装饰器,它把原函数的元信息复制到 wrapper 上,相当于"保留身份证"。