Python面试常见问题汇总
文章目录
- Python面试常见问题汇总
-
- 前言
- 基础语法篇
-
- [1. Python 中的可变类型和不可变类型有哪些?](#1. Python 中的可变类型和不可变类型有哪些?)
- [2. `==` 和 `is` 的区别是什么?](#2.
==和is的区别是什么?) - [3. 谈谈 Python 中的深拷贝与浅拷贝](#3. 谈谈 Python 中的深拷贝与浅拷贝)
- [4. 装饰器是什么?如何实现一个带参数的装饰器?](#4. 装饰器是什么?如何实现一个带参数的装饰器?)
- 高级特性篇
-
- [5. 迭代器与生成器的区别?](#5. 迭代器与生成器的区别?)
- [6. `new` 和 `init` 有什么区别?](#6.
__new__和__init__有什么区别?) - [7. GIL 是什么?如何规避?](#7. GIL 是什么?如何规避?)
- 数据结构篇
-
- [8. 字典的底层实现原理是什么?](#8. 字典的底层实现原理是什么?)
- [9. 如何合并两个字典?](#9. 如何合并两个字典?)
- [10. 列表推导式 vs map/filter 的选用](#10. 列表推导式 vs map/filter 的选用)
- 并发编程篇
-
- [11. 线程、进程、协程的区别?](#11. 线程、进程、协程的区别?)
- [12. 如何使用 ThreadPoolExecutor?](#12. 如何使用 ThreadPoolExecutor?)
- [13. asyncio 的核心概念](#13. asyncio 的核心概念)
- 底层原理篇
-
- [14. Python 的垃圾回收机制?](#14. Python 的垃圾回收机制?)
- [15. `slots` 的作用?](#15.
__slots__的作用?) - [16. `with` 语句的底层实现?](#16.
with语句的底层实现?) - [17. 闭包与 nonlocal 关键字](#17. 闭包与 nonlocal 关键字)
- 编码实践篇
-
- [18. 如何编写安全的异常处理?](#18. 如何编写安全的异常处理?)
- [19. Python 中如何实现依赖注入?](#19. Python 中如何实现依赖注入?)
- [20. 什么是 Pythonic 的代码风格?](#20. 什么是 Pythonic 的代码风格?)
- 总结
- 亮点总结
- 适用场景
- 扩展方向
前言
Python 作为最受欢迎的编程语言之一,其面试题涵盖了语法特性、数据结构、并发编程、内存管理等诸多领域。不论是校招面试还是社招跳槽,系统地复习 Python 核心知识点都是必不可少的一环。很多求职者面对"海量"面试题时不知道从何下手,本文按照基础语法 → 高级特性 → 数据结构 → 并发编程 → 底层原理 → 编码实践的逻辑梳理了20道高频面试题,既覆盖广度也体现深度。
面试策略建议:初级岗位重点在前10题(基础语法 + 数据结构),中高级岗位需要全面掌握所有20题并能深入底层原理。回答面试题时不要只给出定义------举例代码永远是加分项。说到装饰器时能手写出带参数的装饰器,说到GIL时能给出多进程和asyncio两种规避方案,这比只背概念有说服力得多。每题都配有核心答案、代码示例和深度解析,帮助你在面试中脱颖而出。
基础语法篇
1. Python 中的可变类型和不可变类型有哪些?
答案:不可变类型包括 int、float、str、tuple、frozenset(值不能修改);可变类型包括 list、dict、set(值可以修改)。
这道题几乎所有Python面试都会碰到,但回答的深度决定了你的分数。刚入门的回答是"列举类型",中级的回答是"解释函数参数传递的影响",高级的回答是"结合dict的key必须是hashable这个实际约束来说明为什么区分可变/不可变如此重要"。
python
# 不可变类型:修改变量实际是创建新对象
a = "hello"
print(id(a)) # 某个地址
a += " world"
print(id(a)) # 地址改变,创建了新字符串
# 可变类型:原地修改
b = [1, 2, 3]
print(id(b)) # 某个地址
b.append(4)
print(id(b)) # 地址相同,列表被原地修改
深度解析:这直接影响函数传参行为------不可变类型作为参数传递是值传递语义,可变类型则是引用传递。此外,dict 的 key 必须是不可变类型,因为 key 需要 hashable。
2. == 和 is 的区别是什么?
这道题看似简单,但你能在人前快速区分的场景并不多。记一个实用口诀 :比较值与 None 永远用 is(因为 None 是单例),比较值内容永远用 ==。
python
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True,值相等
print(a is b) # False,不是同一个对象
print(a is c) # True,c 就是 a 的引用
== 调用 __eq__ 方法比较值 是否相等,is 比较两个对象在内存中的地址 是否相同。通常比较值与 None 时使用 is:if x is None。
3. 谈谈 Python 中的深拷贝与浅拷贝
python
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original) # 浅拷贝
deep = copy.deepcopy(original) # 深拷贝
original[0][0] = 99
print(shallow) # [[99, 2], [3, 4]] ← 受影响
print(deep) # [[1, 2], [3, 4]] ← 不受影响
浅拷贝只复制第一层对象的引用,深拷贝递归复制所有嵌套对象。列表的切片 [:] 和 list.copy() 都是浅拷贝。
4. 装饰器是什么?如何实现一个带参数的装饰器?
装饰器是Python面试中必考的高级特性。大部分面试者能说出装饰器的基本概念,但只有少数人能正确写出带参数的装饰器。带参数装饰器其实是一个"装饰器工厂"------外层函数接收装饰器参数,返回一个标准的单参数装饰器。这层额外的函数嵌套是理解装饰器的关键拐点。
python
import time
import functools
# 基础装饰器
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时:{time.time() - start:.4f}s")
return result
return wrapper
# 带参数的装饰器
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
time.sleep(delay)
return None
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5)
def unstable_network_call():
import random
if random.random() < 0.7:
raise ConnectionError("网络错误")
return "成功"
print(unstable_network_call())
装饰器的本质是闭包,接收一个函数并返回一个新函数。带参数的装饰器需要额外嵌套一层,形成"装饰器工厂"。
高级特性篇
5. 迭代器与生成器的区别?
python
# 生成器:使用 yield 的函数
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
# 生成器表达式
squares = (x * x for x in range(100))
# 自定义迭代器类
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
生成器是迭代器的一种特殊形式,使用 yield 关键字实现惰性计算。两者都实现迭代器协议(__iter__ 和 __next__),但生成器代码更简洁,且天然节省内存。
6. __new__ 和 __init__ 有什么区别?
这两个方法的作用完全搞混的求职者不在少数。一个简单的类比:__new__ 是"生孩子"(创建对象),__init__ 是"打扮孩子"(初始化对象)。__new__ 先执行,__init__ 后执行。单例模式是用 __new__ 控制只有第一个实例被创建,后续请求返回同一个实例------这是面试中考察"你理解 __new__"的最典型场景。
python
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, value=None):
if value is not None: # 防止重复初始化覆盖
self.value = value
a = Singleton(1)
b = Singleton(2)
print(a is b) # True,单例模式
print(a.value) # 1,未被 __init__ 覆盖
__new__ 负责创建实例并返回,是静态方法;__init__ 负责初始化实例,是实例方法。__new__ 先于 __init__ 调用。单例模式、不可变类型的子类化(如自定义 str)都需要重写 __new__。
7. GIL 是什么?如何规避?
GIL(全局解释器锁)是 CPython 解释器中使用的一把互斥锁,确保同一时刻只有一个线程执行 Python 字节码。规避方法:CPU 密集型任务使用 multiprocessing ;IO 密集型任务使用 asyncio 或线程池。
python
# CPU 密集型 → 多进程
from multiprocessing import Pool
def cpu_task(n):
return sum(i * i for i in range(n))
with Pool(4) as pool:
results = pool.map(cpu_task, [1000000] * 4)
# IO 密集型 → 异步
import asyncio
async def io_task(url):
print(f"处理 {url}")
await asyncio.sleep(0.5)
return f"{url} 完成"
async def main():
urls = [f"https://api.example.com/{i}" for i in range(10)]
results = await asyncio.gather(*[io_task(url) for url in urls])
数据结构篇
8. 字典的底层实现原理是什么?
这是一个典型的"从知道到理解"的面试题。很多人只知道"哈希表、O(1)",但这只说了结果。面试官想听的是:Python 3.6+的紧凑字典如何通过分离索引和条目来节省内存并保持插入顺序,索引数组是紧凑的整数稀疏数组,条目数组存储实际的键值对。解释到这个层次,面试官就知道你不是在背书。
Python 3.6+ 中的字典使用紧凑字典(compact dict)实现,核心结构包括一个索引数组 和多个条目数组。哈希碰撞使用开放寻址法解决。查找、插入、删除的平均时间复杂度都是 O(1)。
python
# 演示:字典保持插入顺序(3.7+ 语言保证)
d = {}
d["z"] = 1
d["a"] = 2
d["c"] = 3
print(list(d.keys())) # ['z', 'a', 'c'],保持插入顺序
9. 如何合并两个字典?
python
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
# 方式一:使用 | 运算符(3.9+)
merged = d1 | d2
print(merged) # {'a': 1, 'b': 3, 'c': 4}
# 方式二:解包
merged2 = {**d1, **d2}
# 方式三:update(原地修改)
d1_copy = d1.copy()
d1_copy.update(d2)
10. 列表推导式 vs map/filter 的选用
python
# 列表推导式(可读性更好)
result = [x * 2 for x in range(10) if x % 2 == 0]
# map + filter(适合已有具名函数)
result2 = list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, range(10))))
# 更快:直接用生成器表达式配合 list()
result3 = list(x * 2 for x in range(10) if x % 2 == 0)
简洁操作用推导式,复杂转换用 map/filter,大数据量用生成器表达式。
并发编程篇
11. 线程、进程、协程的区别?
这道题几乎是后端Python面试的"必问全家桶"。回答时要避免混淆:线程是操作系统调度的最小单位,进程是资源分配的最小单位,协程是用户态调度的轻量级并发。在Python的上下文中,提到线程一定要提GIL------不提GIL的线程相关回答在面试中会被视为理解不完全。
| 特性 | 线程 | 进程 | 协程 |
|---|---|---|---|
| 调度方式 | 操作系统 | 操作系统 | 用户代码调度 |
| 内存共享 | 共享 | 不共享 | 共享 |
| 创建开销 | 小 | 大 | 极小 |
| 适用场景 | IO 密集型 | CPU 密集型 | 高并发 IO |
| Python 中的 GIL | 受限 | 不受限 | 单线程 |
python
# 协程示例:处理数千并发连接
import asyncio
async def handle_client(reader, writer):
data = await reader.read(1024)
response = f"Echo: {data.decode()}"
writer.write(response.encode())
await writer.drain()
writer.close()
async def server():
server = await asyncio.start_server(handle_client, "localhost", 8888)
async with server:
await server.serve_forever()
12. 如何使用 ThreadPoolExecutor?
python
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
def download_url(url):
resp = requests.get(url, timeout=10)
return url, len(resp.content)
urls = [
"https://www.python.org",
"https://www.github.com",
"https://www.stackoverflow.com",
]
with ThreadPoolExecutor(max_workers=5) as executor:
# 方式一:按完成顺序处理
futures = {executor.submit(download_url, url): url for url in urls}
for future in as_completed(futures):
url, size = future.result()
print(f"{url}: {size} bytes")
# 方式二:批量 map
results = executor.map(download_url, urls)
for url, size in results:
print(f"{url}: {size} bytes")
13. asyncio 的核心概念
python
import asyncio
async def fetch(id, semaphore):
async with semaphore: # 并发控制
print(f"开始请求 {id}")
await asyncio.sleep(1) # 模拟网络请求
return f"结果-{id}"
async def main():
semaphore = asyncio.Semaphore(3) # 最多3个并发
tasks = [fetch(i, semaphore) for i in range(10)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
核心概念:事件循环 (event loop)是调度器,协程 (coroutine)是执行单元,Task 将协程提交到事件循环,await 挂起当前协程等待异步操作完成。
底层原理篇
14. Python 的垃圾回收机制?
这是一道"基础但深不见底"的题。入门答案"引用计数+分代回收"只能及格,进一步需要解释为什么需要两套机制(引用计数实时但无法处理循环引用,分代回收周期性处理循环引用),以及弱引用、__slots__ 如何帮助减少GC压力。如果能提到 gc.set_threshold() 的参数含义和实际调优经验,面试官会对你刮目相看。
Python 采用引用计数 为主、分代垃圾回收为辅的机制。引用计数实时回收引用数为零的对象;分代回收定期检查三代对象(0/1/2代),清理循环引用。
python
import gc
import weakref
# 手动控制 GC
gc.disable() # 禁用自动回收
gc.collect() # 手动触发全回收
gc.set_threshold(700, 10, 10) # 自定义阈值
# 避免循环引用的三种方式
# 1. 弱引用
class Node:
def __init__(self, value):
self.value = value
self.parent = None
self.children = weakref.WeakSet()
# 2. 显式断开
class Node2:
def cleanup(self):
self.parent = None
self.children.clear()
# 3. 上下文管理器
class Resource:
def __enter__(self):
return self
def __exit__(self, *args):
self.cleanup()
15. __slots__ 的作用?
python
import sys
class PointNormal:
def __init__(self, x, y):
self.x = x
self.y = y
class PointSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
# 禁止动态添加属性
p = PointSlots(1, 2)
try:
p.z = 3 # AttributeError
except AttributeError as e:
print(f"无法添加属性:{e}")
# 内存对比
import sys
normal = PointNormal(1, 2)
slots = PointSlots(1, 2)
print(sys.getsizeof(normal.__dict__)) # dict 占用额外内存
# PointSlots 没有 __dict__,更省内存
__slots__ 使用元组声明允许的实例属性,阻止 __dict__ 创建,从而减少内存开销(约 40-50%),但会失去动态添加属性的能力。
16. with 语句的底层实现?
python
class DatabaseConnection:
def __init__(self, db_url):
self.db_url = db_url
def __enter__(self):
print(f"连接数据库:{self.db_url}")
self.connection = self.db_url # 模拟连接
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("关闭数据库连接")
if exc_type:
print(f"发生异常:{exc_type.__name__}: {exc_val}")
return False # False 表示不抑制异常
with DatabaseConnection("postgresql://localhost/mydb") as conn:
print(f"执行查询,使用 {conn.connection}")
with 语句调用对象的 __enter__ 和 __exit__ 方法,无论代码块是否抛异常,__exit__ 都会执行。这是实现资源安全释放的标准模式。
17. 闭包与 nonlocal 关键字
python
def counter():
count = 0
def inner():
nonlocal count # 声明对外层变量的引用
count += 1
return count
return inner
c = counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
闭包是携带了外部作用域变量的函数。在嵌套函数中修改外部不可变类型变量时,必须使用 nonlocal 声明。
编码实践篇
18. 如何编写安全的异常处理?
异常处理说简单也简单(加个try),说难也难(写得专业)。面试中经常问的不是"try怎么写",而是"这段异常处理代码有什么问题"。最常见的问题包括:except: 裸捕获所有异常(包括KeyboardInterrupt)、except Exception: pass 静默吞掉异常、在finally中又抛出一个异常覆盖了原始异常。
python
# 不推荐:过于宽泛的捕获
try:
result = process_data(raw_input)
except: # 捕获所有异常包括 KeyboardInterrupt、SystemExit
pass # 吞掉异常,无法排查问题
# 推荐:具体的异常捕获
try:
result = process_data(raw_input)
except ValueError as e:
logger.warning(f"数据格式错误:{e}")
result = default_value
except (ConnectionError, TimeoutError) as e:
logger.error(f"网络请求失败:{e}")
raise # 重新抛出,让上层处理
finally:
cleanup_resources()
原则:捕获具体异常而非 Exception 裸类;不要静默吞掉异常;使用 finally 清理资源(或直接用 with)。
19. Python 中如何实现依赖注入?
python
# 简单方式:构造函数注入
class UserService:
def __init__(self, db_session, email_sender):
self.db = db_session
self.email = email_sender
def register(self, user_data):
user = self.db.save(user_data)
self.email.send_welcome(user.email)
return user
# 测试时轻松 Mock
from unittest.mock import MagicMock
mock_db = MagicMock()
mock_db.save.return_value = {"id": 1, "name": "Test"}
mock_email = MagicMock()
service = UserService(mock_db, mock_email)
service.register({"name": "Test", "email": "test@test.com"})
mock_db.save.assert_called_once()
mock_email.send_welcome.assert_called_once()
依赖注入让代码解耦、方便单元测试。Python 中不需要复杂的 DI 框架,构造函数注入是最直接有效的方式。
20. 什么是 Pythonic 的代码风格?
这道开放式问题最能体现面试者的Python功底。面试官要的不是你背出PEP 8的条文,而是给你一段"不Pythonic"的代码让你改进。典型的改进方向:for i in range(len(list)) → for item in list 或 for i, item in enumerate(list);if len(list) > 0 → if list;多行if-elif中对变量赋值 → 字典映射。
Pythonic 的核心是充分利用语言特性写出简洁、可读、高效的代码:
python
# 解包赋值
a, b, *rest = [1, 2, 3, 4, 5]
# 三元表达式
status = "成年" if age >= 18 else "未成年"
# 枚举迭代
for index, item in enumerate(items, start=1):
pass
# 字典推导
squares = {x: x*x for x in range(10)}
# 上下文管理器(资源安全)
with open("data.txt") as f:
content = f.read()
# 使用 f-string
name, age = "Alice", 25
print(f"{name}今年{age}岁")
# 链式比较
if 0 < x < 100:
pass
# 真假值判定
if my_list: # 而非 if len(my_list) > 0
pass
if not data: # 而非 if data is None or data == ""
pass
总结
本文覆盖的 20 道面试题从基础语法 (可变不可变、== vs is、深浅拷贝、装饰器)到高级特性 (迭代器生成器、__new__ vs __init__、GIL),再到数据结构 (字典原理、魔术方法)、并发编程 (线程/进程/协程、线程池、asyncio)和底层原理 (GC、__slots__、上下文管理器),全面梳理了 Python 面试的核心考点。面试不仅考察知识广度,更看重对原理的理解深度------知道"是什么"只是起点,能解释"为什么"和"怎么用"才是加分项。建议对照每道题中给出的代码示例动手实践,形成自己的知识体系。
亮点总结
- 20 道高频面试题系统覆盖:按基础语法、高级特性、数据结构、并发编程、底层原理、编码实践六大模块分类,形成完整的面试知识地图。
- 每题附可运行代码示例 :不仅给出概念解释,还配有完整的代码演示(如装饰器带参数实现、单例模式 via
__new__、分布式锁、依赖注入),方便动手验证。 - 深度解析贯穿全文:从"是什么"延伸到"为什么"和"怎么用",如解释 GIL 后给出多进程和 asyncio 两种规避方案,让面试回答更有说服力。
- Pythonic 编程技巧集锦:解包赋值、三元表达式、字典推导、f-string、上下文管理器和链式比较等 Pythonic 写法集中展示。
- 安全编码与设计模式:异常处理最佳实践、依赖注入实现可测试代码、分布式锁解决竞态条件,体现了工程化思维。
适用场景
- 面试前系统复习:按模块快速过一遍高频考点,每道题的代码示例可以当场手写验证,建立肌肉记忆。
- 日常工作中的查漏补缺 :当遇到某个知识点的盲区时(如
__slots__内存优化、nonlocal 闭包变量),回看对应章节即可快速掌握。 - 编写技术分享与培训材料:本文的结构(概念 + 代码 + 深度解析)可以直接作为团队内部分享的提纲使用。
扩展方向
- 算法与数据结构深入学习:面试不仅考察语言特性,算法题同样是重头戏,建议结合 LeetCode 热门题库,用 Python 练习常见数据结构(树、图、堆)和算法(DP、回溯、滑动窗口)。
- Python 源码分析(CPython Internals):对于底层原理题(dict 实现、垃圾回收、GIL),阅读 CPython 源码能获得更深入的理解,在面试中脱颖而出。
- 系统设计与分布式面试准备:中高级岗位面试会涉及系统设计(如设计一个短链服务、分布式 ID 生成器),可以结合本系列第 99 篇(Redis)等文章积累分布式系统的实战经验。