简介:面向Python技术面试的基础知识整理包,覆盖编程语法、数据结构、算法、面向对象、异常处理、标准库、文件、网络、数据库及项目实践,为求职者提供系统化复习指引。包体大小约33.16MB,文件目录需下载后查阅;目前已有508人参与学习,内容经他人使用验证。整理中针对每个模块给出简明易读的说明,例如列表推导式、字典键值操作、集合运算,以及冒泡/快速/归并排序、线性/二分查找的复杂度对比;同时涉及类与继承、类方法/静态方法、try-except-finally的异常清理,并结合os、sys、re、json等常用内置模块说明实际用法。数据库部分演示了sqlite3完成增删改查,网络部分介绍socket与HTTP请求,项目实践则涵盖需求分析、编码、测试到文档编写;还引入CS-Notes-master等配套练习题,方便读者边学边练,巩固算法与知识点。最终帮助面试者系统查漏补缺,建立完整知识框架,从容应对技术问答与笔试。
1. Python技术面试到底在考什么:不是背语法,是这三条主线
一次电话面试里,A同学把装饰器的概念背得很熟,但让他手写一个带重试的装饰器,他卡在了functools.wraps上;接着问列表去重保持顺序,他先写了sorted(set(nums)),却没意识到顺序已经变了。挂掉后他问我:Python技术面试到底要准备什么?我的答案是:基础知识可以拆成三条主线——数据结构的选型与边界、函数与对象模型的理解深度、以及能把一个知识点从“背定义”讲到“能落地”的能力。这里就按这三条主线,整理一份Python技术面试需要掌握的基础知识清单,覆盖常见手撕题、面试追问和现实翻车点,适合准备中高级开发岗的工程师照着练。
2. 数据结构与内置类型:手撕题的地基,先解决“选型”再谈“优化”
面试里数据结构是必考,但很多人上来就写代码,忽略了面试官更想听的是“为什么选它”。我把这一章拆成底层差异、去重手撕、可变对象三个高频考点,每个都能在十分钟内写代码验证。
2.1 列表、元组、字典、集合的底层差异:面试官问“为什么有set还要有list”时怎么答
Python面试里最基础也最容易被追问的是内置类型的底层结构。列表是动态数组,按索引访问是O(1),在中间插入会触发元素搬移;元组是不可变数组,能哈希且内存开销小,适合当固定记录;字典和集合都基于哈希表,查找是O(1)均摊,但要求键必须可哈希。这个差异直接决定了选型。
| 类型 | 底层结构 | 是否可变 | 是否可哈希 | 查找复杂度 | 典型场景 |
|---|---|---|---|---|---|
| list | 动态数组 | 是 | 否 | O(n) | 有序遍历、按下标访问 |
| tuple | 定长数组 | 否 | 是(元素可哈希时) | O(n) | 固定记录、字典键 |
| dict | 哈希表 | 是 | 键可哈希 | O(1)均摊 | 键值映射、计数 |
| set | 哈希表 | 是 | 元素可哈希 | O(1)均摊 | 去重、成员判断 |
面试官问“为什么有set还要有list”,不是考背复杂度,而是考你能否说清“哈希表牺牲了顺序和索引,换来O(1)成员判断;list保留顺序、允许重复、支持切片,代价是查找要线性扫”。所以选型先问三个问题:我需要顺序吗?需要索引吗?元素可哈希吗?这比直接背结论更有说服力。
import time n = 100_000 nums = list(range(n)) target = n - 1 # 用 list 做成员判断,最坏情况扫到最后一个 start = time.perf_counter() _ = target in nums print("list in:", time.perf_counter() - start) # 用 set 做成员判断,直接哈希定位 s = set(nums) start = time.perf_counter() _ = target in s print("set in:", time.perf_counter() - start)这个对比里target取最后一个元素,list的in要遍历整个列表,set的in只算一次哈希,n越大差距越明显。参数说明:n控制数据量,target取末尾是为了让list做最坏情况扫描;如果target靠前,list提前退出,对比就不公平。在面试现场跑这个脚本,比背复杂度更有说服力。
2.2 高频手撕:列表去重保持顺序,三种写法从慢到快
这道题在很多第一轮技术面试里都会出现。要求是:输入一个列表,去掉重复元素,且保持每个元素第一次出现的顺序。注意不是单纯去重,顺序是隐藏考点。
def dedup_with_set(items): """set 辅助,保持顺序,O(n) 时间""" seen = set() result = [] for item in items: if item not in seen: seen.add(item) result.append(item) return result def dedup_with_dict(items): """利用 dict 的插入顺序(Python 3.7+),一行搞定""" return list(dict.fromkeys(items)) def dedup_with_comprehension(items): """列表推导式 + set,利用 or 的短路,O(n)""" seen = set() return [x for x in items if not (x in seen or seen.add(x))]dedup_with_set是最标准的做法,用set记录已出现元素,result按原序收集第一次出现的元素,时间O(n)、空间O(n)。dedup_with_dict利用了dict.fromkeys会按插入顺序保存键,重复键自动被覆盖,再转list,代码最简洁,但要解释清楚依赖Python 3.7+的有序字典语义。dedup_with_comprehension把判断压缩进推导式,x in seen为False时继续执行seen.add(x),add返回None,所以not False为True保留元素;x已出现过时or短路,结果被过滤。
参数说明:三个函数都接受任何可迭代对象items,返回list。如果items里的元素不可哈希,比如嵌套list,set和dict方案会抛TypeError。这时可以先把元素转成tuple,或者写基于==的O(n^2)版本。面试时主动说出这个边界,比写完代码就停更容易加分。
2.3 可变对象作为函数默认参数:一个经典陷阱与正确写法
这道题考察的是“默认参数在定义时求值”这个规则。很多新手以为函数不传参数时每调用一次都生成新默认值,其实不是。
def add_item(item, bucket=[]): bucket.append(item) return bucket print(add_item(1)) # [1] print(add_item(2)) # [1, 2],同一个桶被复用了第二次调用返回[1,2]而不是[2]。原因是默认列表在函数定义时创建一次,后续所有调用共享它。正确写法是把默认值改为None,在函数体内创建新列表。
def add_item(item, bucket=None): if bucket is None: bucket = [] bucket.append(item) return bucketbucket不传时每次新建list;传了则复用调用方的list。面试时补充一个细节:不要写bucket = bucket or [],因为如果调用方传了空list,空list是False,会被误判为没传而新建,导致传入的空list被忽略。这种“可变默认参数”的坑在真实项目里也经常翻车,值得单独记一条。
3. 函数、作用域与闭包:把“装饰器”从背模板变成能现场推导
函数这块是Python技术面试的另一重灾区。背模板的人能写出装饰器,但一被问“为什么”就卡。这里从作用域、闭包、装饰器三个点依次推演,重点不是背代码,而是能把代码背后的设计逻辑讲出来。
3.1 作用域与LEGB:为什么在函数里给全局变量赋值会报错
Python查找变量的顺序是Local、Enclosing、Global、Built-in,简称LEGB。函数体内如果出现了对某个变量的赋值,编译器就把这个变量标记为局部变量,后续对它做读取操作时,还没赋值就会报UnboundLocalError。
count = 0 def increase(): count += 1 # UnboundLocalError: local variable 'count' referenced before assignment函数体内有count += 1,count被编译成局部变量,+=需要先读再写,局部count没有初始值就报错了。解决方法是显式声明:
count = 0 def increase(): global count count += 1 increase() print(count) # 1逻辑说明:global声明告诉解释器count引用全局作用域。对嵌套函数,要修改外层函数的局部变量则用nonlocal。参数说明:global和nonlocal只是声明,不能直接赋值。面试官常追问为什么Python要这样设计,答案是显式声明可以防止在大型代码里无意污染全局名称空间,让副作用对读者可见。
再给一个LEGB验证:
x = "global" def outer(): x = "outer" def inner(): x = "inner" print(x) inner() print(x) outer() print(x)输出是inner、outer、global,分别命中Local、Enclosing、Global。这个验证能直观展示作用域链,面试时也可以现场手写这个例子来演示。
3.2 闭包与延迟绑定:循环里用lambda为什么会全部等于最后一个值
这是面试官非常喜欢挖的题。先看现象:
funcs = [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) # [2, 2, 2]闭包捕获的不是创建时的值,而是外层变量i这个引用。循环结束后i停在2,所有lambda取值都是2,这就是延迟绑定。解决方法是把当前值绑定到默认参数:
funcs = [] for i in range(3): funcs.append(lambda val=i: val) print([f() for f in funcs]) # [0, 1, 2]参数说明:val=i中的默认参数在函数定义时求值,当前i被绑定到默认参数,闭包保存的是这个参数的值。val只是参数名,也可以叫item,但最好改一个名字,避免和循环变量i混淆。面试时如果被追问“列表推导式里写lambda: i for i in range(3)会怎样”,结果同样是[2,2,2],因为推导式作用域里i依然是同一个变量,延迟绑定没有被解决。
3.3 装饰器的标准模板与带参数装饰器:手写一个计时器
到这一节,面试已经进入第二轮。要求手写装饰器时,标准模板必须不假思索写出来,并解释为什么用它。
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper @timer def slow_add(a, b): time.sleep(0.01) return a + btimer是装饰器函数,返回内部wrapper。@wraps(func)会把原函数的__name__、doc、注解等元信息复制到wrapper上,否则被装饰的函数名字全变成wrapper,排查问题很痛苦。wrapper用*args和**kwargs转发所有参数,保证装饰器对任意签名通用。time.perf_counter是专门测短耗时的计时器,比time.time精度更高且不受系统时间调整影响。
带参数装饰器需要再多包一层:
def repeat(times): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator用法是@repeat(3)。参数说明:times在最外层被捕获,decorator接收真正要装饰的函数,wrapper是最终替换的函数对象。三层结构是带参数装饰器的固定套路,面试时最好能默写出来。
面试追问:多个装饰器的执行顺序。@a在上、@b在下时,先应用b再应用a,调用时先进入a的wrapper再进b的wrapper。建议现场写一个打印输出顺序的装饰器验证,不要凭记忆背顺序,因为很容易记反。
4. 对象模型与魔法方法:面试最爱问的“为什么”,用代码验证
Python技术面试到了中高级,一定会碰对象模型。这里的题目不是背语法,而是验证你理解“为什么这样设计”。选三个高频考点:eq__与__hash、new__与__init、上下文管理器。每个都能用一段小代码把关键行为复现出来。
4.1eq__与__hash:为什么重写相等必须考虑哈希
哈希表在做set去重和dict查键时,先算对象的__hash__来确定位置,再用__eq__处理哈希冲突。如果两个对象逻辑上相等,但哈希值不同,它们会被放进不同的桶,导致set里出现重复的“相等”对象。这个规则是面试官最爱挖的。
class Person: def __init__(self, name): self.name = name def __eq__(self, other): return isinstance(other, Person) and self.name == other.name def __hash__(self): return hash(self.name)重写__eq__之后,两个name相同的Person会相等;同时重写__hash__,让哈希值也基于name。这样p1 == p2为True时,hash(p1) == hash(p2)也为True,放入set不会重复。如果不重写__hash__,Python默认用id做哈希,两个不同对象即使相等也会被当成两个键。
参数说明:__hash__要返回整数,且不能依赖可变字段。如果对象的属性会被修改,而它被放进set了,修改后哈希值变化,set就再也查不到它。所以作为键的对象,参与哈希的字段应该设计为只读。面试官常追问:为什么list不能作为dict键?因为list可变,可变对象不能保证哈希稳定,这是设计使然。
4.2new__与__init:实现单例模式时到底用哪一个
讲到对象构造,绕不开__new__和__init__的区别。__new__是真正的创建实例方法,返回实例对象;__init__是在实例创建后负责初始化。单例要控制“只创建一个实例”,只能在__new__里拦。
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): self.value = value简单版不是线程安全的。多线程同时第一次调用时,两个线程都可能判断_instance is None为True,各创建一个实例。加锁版是双重检查锁:
import threading class SafeSingleton: _instance = None _lock = threading.Lock() def __new__(cls, *args, **kwargs): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance先不加锁读一次,确定为空再加锁,加锁后再检查一次,避免锁之外的线程已经创建了实例。参数说明:*args和**kwargs是为了兼容不同构造参数,__new__返回实例后Python会自动调用__init__传入相同参数。
要注意__init__每次调用都会执行。Singleton(1)后再Singleton(2),实例是同一个,但value会变成2。如果希望只初始化一次,需要额外加_initialized标志。面试官常问能不能用模块级单例,完全可以,模块在导入时天然只执行一次,是最简单的单例实现,手写__new__容易踩线程和初始化的坑。
4.3 上下文管理器:从 with open 到自己写一个带重试的数据库连接
基础知识点:with语句执行上下文管理器的__enter__和__exit__,即使块内抛异常,__exit__也会被调用,这是资源释放的关键。
class DatabaseSession: def __enter__(self): print("acquire connection") return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type: print("rollback, error:", exc_val) else: print("commit") return False__enter__返回self,这个返回值被赋给as后面的变量。__exit__接收异常信息:exc_type是异常类型,exc_val是异常实例,exc_tb是traceback对象。块内正常结束时三个参数都是None。__exit__返回False表示不吞异常,异常继续抛给上层;返回True表示异常被处理,with块外捕获不到。
用contextlib写同样语义:
from contextlib import contextmanager @contextmanager def db_session(): print("acquire") try: yield except Exception as e: print("rollback", e) raise else: print("commit")contextmanager把生成器函数变成上下文管理器,yield之前的代码是__enter__,yield之后的代码是__exit__。在except里重新raise,才能保持和return False一样的“不吞异常”语义。
面试追问:如果连接在执行中掉了,重试逻辑放哪?常见做法是在__exit__里检查异常类型,如果是连接中断类异常,则重新建立连接再执行一次;但要注意重试次数,避免死循环。这个扩展题能拉开差距,也是我们做基础整理时最愿意深挖的点之一。
5. Python技术面试避坑指南:我踩过的5个现实问题
下面这5个坑不是冷门题,而是我在模拟面试和实际项目里反复见过的翻车点。每条都按“现象、原因、解决”来写,建议你在面试前逐一自查,不要只看一遍就跳过。
5.1 手写快速排序时递归深度爆了
现象:用有序整数列表测试自己写的快速排序,数据量到一万左右直接抛RecursionError。
原因:代码固定取第一个元素作为pivot。当输入已经有序时,每次分区只能分离出一个元素,递归深度接近数据规模,超出Python默认递归上限。很多人只背快速排序的模板,没想过pivot选择会带来最差复杂度。
解决:随机选pivot,或者取首、中、尾三个数的中位数。随机化能让快排的期望复杂度回到O(n log n),不会陷入最坏情况。快速改造:
import random def quicksort(arr): if len(arr) <= 1: return arr pivot = random.choice(arr) left = [x for x in arr if x < pivot] mid = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quicksort(left) + mid + quicksort(right)这个写法用列表推导式分区,比原地交换好读,面试时更容易表达思路。参数说明:random.choice从arr里随机取pivot,避免有序输入导致最差递归深度。注意这个版本会额外占用内存,但面试场景更看重正确性和可读性,真做项目再优化成原地版本。
5.2 用 is 比较整数在 -5 到 256 之间正常,大于就翻车
现象:在交互式会话里分两行执行a = 257、b = 257,然后print(a is b),结果通常是False;改成a = 5; b = 5,结果又变成True。
原因:CPython对小整数做了缓存,范围是-5到256,这个范围内的整数是单例,所有引用都指向同一对象。超过这个范围,解释器不保证对象唯一,还受同一代码块内常量合并的影响,所以is结果不稳定。
解决:比较数值一律用==,不要用is。is只用于比较None、True、False这类单例对象,例如if x is None。面试时能把整数缓存范围说出来,说明你了解对象池的存在;接着再补充一句“所以is比较数值不可靠”,就能避开这道送命题。
5.3 深拷贝还是浅拷贝搞错,导致线上数据被改
现象:把一个配置list复制一份给新模块用,新模块改了副本里某个子列表的元素,老模块的数据也跟着变了。
原因:copy.copy是浅拷贝,只复制最外层list对象,内部元素仍然是原对象的引用。修改内层list时,原对象跟着变,这是项目里很常见的血泪教训。
解决:对嵌套结构用copy.deepcopy。下面的小实验能直观看出浅拷贝和深拷贝的差别,建议面试前亲手跑一遍:
import copy a = [[1, 2], [3, 4]] b = copy.copy(a) c = copy.deepcopy(a) b[0].append(99) c[0].append(100) print(a) # [[1, 2, 99], [3, 4]] print(c) # [[1, 2, 100], [3, 4]]b是浅拷贝,a的第一层子列表被共享,所以b[0]的修改影响a;c是深拷贝,整个结构都复制了,互不影响。参数说明:deepcopy也有代价,对大而复杂的结构递归复制可能慢,所以要评估是否真的需要完全隔离,不要无脑全用深拷贝。
5.4 说“GIL让多线程没用”被追问后翻车
现象:面试时斩钉截铁说“Python多线程是假的,因为GIL”,对方追问“那多线程加锁还有意义吗”,现场答不上来。
原因:GIL只保证同一个时刻只有一个线程执行Python字节码,但I/O操作会主动释放GIL,所以多线程在I/O密集型任务里依然能并发等待;真正受限的是CPU密集型任务。
解决:回答要分层。CPU密集用多进程或异步;I/O密集用多线程仍有优势。多线程里对共享数据必须加锁,因为GIL保护的是解释器内部状态,不是你的业务数据结构。这里要谨慎,不同实现和版本在调度细节上有差异,面试只说通用机制就可以。
5.5 文件读取后没有关闭导致资源泄漏
现象:程序循环处理大量小文件,跑了一段时间后报“Too many open files”,进程被系统杀掉。
原因:每次用open读取后没有close,文件句柄没有被回收。在Windows上更明显,句柄泄漏会卡住后续文件操作。
解决:用with open,或者直接用pathlib的read_text。下面是用pathlib的写法:
from pathlib import Path text = Path("data.txt").read_text(encoding="utf-8")read_text内部负责打开和关闭,适合一次性读取小文件。参数说明:encoding指定utf-8,避免在Windows上默认GBK导致乱码。如果文件很大,还是用with open逐块读,不要一次read_text读进内存。这个坑在真实项目中经常遇到,面试官问“你平时怎么读文件”时,能主动说出资源释放就很加分。
6. 把基础串成系统:一张自查清单与两个高频追问
基础铺到这里,剩下的是怎么把这些点串起来。我自己的做法是准备一张自查清单,每一条都可以在十分钟内写代码验证,答不上来的点就是面试前要补的优先级。
| 模块 | 自测问题 | 答不出来的补救方向 |
|---|---|---|
| 数据结构 | 去重保持顺序有哪几种写法? | 回到2.2重写一遍三个函数 |
| 函数 | 闭包为什么延迟绑定? | 用lambda循环例子现场跑 |
| 装饰器 | 带参数装饰器有几层函数? | 对着3.3三层模板抄写并默写 |
| 对象模型 | eq和hash为什么必须一起考虑? | 构造一个set重复的复现 |
| 资源管理 | with语句异常时exit做什么? | 自己写一个上下文管理器打印参数 |
自测时不要只背答案,要把每个问题当面试题说给一个懂技术的朋友听,说不顺的地方就是真正没理解的地方。
两个高频追问,供你准备综合题。第一个:如果让你用Python实现一个固定大小的LRU缓存,你会选什么数据结构?回答思路是:缓存需要O(1)的get和put,哈希表负责查找,双向链表负责维护访问顺序。Python里可以用collections.OrderedDict,它同时保留了插入顺序和键查改;也可以手写dict加双向链表。面试官想听的是你能否把数据结构选型和复杂度分析结合起来,而不是背代码。
第二个:isinstance(x, list)和type(x) is list有什么区别?回答思路:isinstance考虑继承,type是精确类型。如果定义class MyList(list),isinstance返回True,而type(x) is list返回False。基础题里常考这种“宽窄判断”的边界,答错了很容易暴露基础不稳。
这个方向能挖的还有很多,但能把上面几张表里的点都做到现场写、说清楚,已经超过大部分候选人。我自己面试前的习惯是把每个知识点做成一条带代码的笔记,隔天不看代码重写一遍,写不出来就回去看原文。这个习惯让我少吃了很多表面熟练的亏,希望帮到你。
本文还有配套的精品资源,点击获取