资讯详情

Python面试55题精讲:高频考点、易错点与手写代码解析

📅 2026/11/4 3:31:16 | 华诺云谱 👁 阅读
Python面试55题精讲:高频考点、易错点与手写代码解析
简介面向准备Python技术面试的初中级开发者与求职者这份文档围绕常见考点整理出55道题目及参考答案帮助在有限时间内系统梳理语法、数据结构与运行机制等易错知识。内容覆盖数组去重求和、变量作用域与global、循环嵌套条件筛选、装饰器执行顺序、try-except-finally、字典引用传递、os.walk目录遍历、正则match与search及贪婪匹配、文件与文件夹删除、logging应用场景、json序列化与datetime定制等题目多配有代码片段和输出解释便于对照理解。资源包仅含1个docx文件大小约80KB轻量易读适合按主题刷题或面试前快速复盘。目前已有1108人学习下载可作为查漏补缺的面试题清单使用。1. 五十五道题里真正拉开差距的不是算法Python 面试题及答案共 55 道这个数字听起来像个题库的量但真坐在面试桌对面你会发现考官根本不按顺序问。他随手甩一段代码让你说输出或者问你match和search差在哪考的不是你刷过多少题而是你对语言细节有没有形成条件反射。这 55 道题覆盖语法、数据结构、函数作用域、装饰器、异常、面向对象、文件与正则、手写结构八个方向其中约六成是“看代码说结果”和“现场手写”两类剩下四成是概念口述。想拿分靠的是把高频考点吃透到能当场写出正确代码再把概念答成面试官想听的版本。这篇适合已经会写 Python、但一被追问细节就发虚的人也适合刷题刷了很多却总在同一个坑里翻车的人。2. 语法与作用域那些一看就会、一写就错的题2.1 全局变量在函数里被修改为什么直接报错先看这道出场率极高的题a 1 def bar(): a 3 bar() print(a)答案是运行出错抛UnboundLocalError。原因不在a 3这行写错了而在 Python 的变量绑定规则函数体内只要对某个名字发生了赋值行为这个名字在整个函数作用域内就被判定为局部变量编译阶段就定下来了。于是a 3展开成“读取局部变量 a加 3再赋回局部变量 a”读取那一步发现局部a还没绑定值直接炸。面试官问这题想听的是三层第一层说结论报错第二层说原因是赋值使a成为局部变量第三层给出两种改法用global a声明或者改成a a 3之外的不修改写法比如只读不写就不会触发。只答“因为没有 global”只算及格答出“赋值即声明局部”才是加分项。a 1 def bar(): global a # 显式声明这个名字指向模块级变量 a 3 # 此时读写的是全局 a bar() print(a) # 输出 4global的作用是告诉编译器跳过局部绑定判定a直接查模块命名空间。要注意global只影响当前函数体嵌套函数里想改外层函数的变量得用nonlocal这两个关键字别混。还有一个常被追问的点如果函数里只有print(a)没有赋值那就不会报错因为没触发局部绑定判定走的是 LEGB 查找顺序从局部到闭包到全局到内置逐层找。2.2 参数传的是引用还是值紧接着的一道题mydict {a: 1, b: 2} def func(d): d[a] 0 return d func(mydict) mydict[c] 2 print(mydict)输出{a: 0, b: 2, c: 2}。这题考的是“Python 传对象引用”这个说法到底怎么落地。函数收到的是字典对象的引用副本通过这个引用修改字典内容外部看到的就是改后的字典但如果你在函数里写d {x: 1}重新绑定外部原字典不受影响。答题时把这两种情况分开说面试官就知道你不是背概念而是真懂。可以顺手补一个对比不可变对象如整数、字符串、元组函数内“修改”本质是重新绑定外部不变可变对象如列表、字典、集合原地修改会体现在外部。这类题的标准答法是先给输出再说可变与不可变的区别最后补一句“所以函数里想返回新对象就别原地改”。2.3 异常与 finally 的执行顺序def test(): try: raise ValueError(something wrong) except ValueError as e: print(error occured) return finally: print(ok) test()输出两行先error occured再ok。这题的陷阱在于return出现在except里很多人以为return会直接结束函数、跳过finally实际finally在return之前执行。更狠的追问是如果finally里也写return会怎样答案是finally的return会覆盖try或except里的返回值这也是一类经典失分点实际开发里几乎没人这么写但面试爱问。回答时按执行链路说清楚try抛异常 →except捕获并打印 → 执行return前先跑finally→ 打印 ok → 真正返回。finally的定位是清理资源文件关闭、锁释放、连接归还都放这里和with语句的__exit__是同一个思路这层联系说出来面试官会觉得你会迁移知识。2.4 装饰器的执行顺序别背反了def decorator_a(func): print(Get in decorator_a) def inner_a(*args, **kwargs): print(Get in inner_a) return func(*args, **kwargs) return inner_a def decorator_b(func): print(Get in decorator_b) def inner_b(*args, **kwargs): print(Get in inner_b) return func(*args, **kwargs) return inner_b decorator_b decorator_a def f(x): print(Get in f) return x * 2 f(1)输出顺序是Get in decorator_a、Get in decorator_b、Get in inner_b、Get in inner_a、Get in f。两个阶段必须分开讲这是这题唯一的得分点装饰阶段从上往下写、从下往上套。decorator_a先贴近函数所以先执行它返回的inner_a再交给decorator_b包一层。装饰器在函数定义完成的那一刻就执行了不是调用时才执行。调用阶段从外往里进。f现在指向inner_b所以先打印inner_binner_b内部调inner_a打印inner_a最后一层才是原函数。把f decorator_b(decorator_a(f))这个等价写法写在草稿上顺序基本不会错。面试官常追加一问带参数的装饰器怎么写答题思路是外面再包一层接收参数的函数返回真正的装饰器三层嵌套最外层只管收参数。3. 数据结构与内置方法正则、JSON、文件操作的高频问法3.1 match 和 search、贪婪与非贪婪match只从字符串起始位置尝试匹配不成功就返回None相当于自带^的效果search扫描整个字符串找到第一个匹配就返回。答题时给个对照例子最省事同一个模式\d去匹配abc123match返回Nonesearch返回123。再补一句re.match想实现全串搜索得自己加.*但那样不如直接用search。贪婪与非贪婪的区别在量词后面那个?。默认贪婪.*会吃到最后一个能满足条件的位置加?变成非贪婪匹配到第一个满足的就停。经典例子是匹配 HTML 标签内容.*会把整行吃成一个匹配.*?才能逐个标签切出来。这题几乎必考答完机制再给场景比干说定义强得多。顺手可以带出邮箱正则的写法import re # 用户名允许字母数字下划线中划线域名部分至少一个点分段 pattern re.compile(r[a-zA-Z0-9_-][0-9A-Za-z](\.[0-9a-zA-Z])) if pattern.match(1403179190qq.com): print(ok)这段的关键在末尾的(\.[0-9a-zA-Z])保证域名后缀可以多级括号不捕获也没关系用(?:...)更规范。面试时老师常追问边界情况比如能不能匹配ab答案是能因为域名部分没有强制要求带点想更严就补上。3.2 JSON 序列化的边界日期和中文json.dumps原生支持的只有str、int、float、bool、None、list、tuple、dict遇到datetime直接抛TypeError。定制方式是继承JSONEncoder重写defaultimport json from json import JSONEncoder from datetime import datetime class ComplexEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.strftime(%Y-%m-%d %H:%M:%S) # 统一时间格式 return super().default(obj) # 其余类型交回父类处理 data {name: alexa, data: datetime.now()} print(json.dumps(data, clsComplexEncoder, ensure_asciiFalse))两个参数是考点cls指定自定义编码器ensure_asciiFalse让中文按原样输出而不是转成\uXXXX。这题的正确答法是先说原生支持的类型清单再说扩展方式最后补ensure_ascii这个易漏点。反序列化方向也有对应问法object_hook可以把字典转回对象和default是一对。3.3 遍历目录与删除文件考的是你有没有真用过遍历文件夹下所有文件路径标准答案是os.walkimport os def list_all(root_dir): for root, dirs, files in os.walk(root_dir): for d in dirs: print(os.path.join(root, d)) # 子目录完整路径 for f in files: print(os.path.join(root, f)) # 文件完整路径 list_all(r./demo)os.walk返回的是三元组迭代器root是当前目录dirs是子目录名列表files是文件名列表三者都只是名字不含路径所以必须os.path.join拼。追问点一般在“怎么只遍历不递归”答案是改dirs[:] []清空子目录列表。另一种写法是os.listdir加递归但代码更长且容易踩目录判断的坑答题时提一句“能用 walk 就别手写递归”就够。删除这块要分清三个函数的边界os.remove(path)只能删文件删目录报错os.removedirs(path)删空目录且会一路往上删空父目录容易误伤shutil.rmtree(path)删整棵树非空也行。三个答全并说清适用场景这题就满了。3.4 日期题与字符串切分“输入某年某月某日判断是这一年第几天”是笔试题常客。思路两条一是维护月份天数表判断闰年选表累加前几个月再加日二是用datetime直接减。def get_day(date_str): days_normal [31,28,31,30,31,30,31,31,30,31,30,31] days_leap [31,29,31,30,31,30,31,31,30,31,30,31] year, month, day [int(i) for i in date_str.split(.)] # 闰年判定能被 400 整除或能被 4 整除且不能被 100 整除 is_leap year % 400 0 or (year % 4 0 and year % 100 ! 0) days days_leap if is_leap else days_normal return sum(days[:month - 1]) day print(get_day(2023.03.01)) # 60手写版的价值在于面试官能看你的边界处理闰年条件漏掉% 400就是典型失分“能被 4 整除就是闰年”这个说法不完整。用datetime版是date(year,month,day).timetuple().tm_yday一行完事答题时两种都说能体现你知道工程里该怎么写。3.5 文件里的重复值求和考的是复杂度意识原题给了两个函数第一个用双列表去重第二个用集合加countdef func(lis): lis1, del_lis [], [] for i in lis: if i not in lis1: if i not in del_lis: lis1.append(i) else: del_lis.append(i) lis1.remove(i) return sum(lis1) def func2(lis): return sum([i for i in set(lis) if lis.count(i) 1]) print(func2([3,4,1,2,5,6,6,5,4,3,3])) # 1 2 3第一个写法逻辑上还有 bug重复元素出现第三次时会走错分支面试时如果能指出这一点就是亮点。第二个写法简洁但list.count是线性扫描整体退化成 O(n²)。更优解是用计数from collections import Counter def func3(lis): counter Counter(lis) return sum(k for k, v in counter.items() if v 1) # 只取出现一次的值复杂度 O(n)这才是面试官想听到的优化方向。答这类题的口径是先给能跑的答案再主动说复杂度最后给优化版别等被追问。4. 面向对象从三大特性到手写单例的完整链路4.1 三大特性、鸭子类型、super 与 MRO三大特性是所有 OOP 面试的开场白但答得好不好差别很大。继承说的是子类复用父类属性和方法、可扩展可重写封装是把属性和方法收进类内用双下划线做名字改写实现私有多态是同一接口不同实现Python 里因为动态类型用在列表、字符串、数字上行为各异本身就是多态的体现。三个特性各配一句“所以实际代码里我怎么用”比背定义强。鸭子类型常和动态类型一起问对象能不能用不看它继承自谁只看它有没有需要的方法和属性。答题时用文件对象举例很稳任何实现了read的对象都能当文件用。super()的考点在多重继承下它不是“调父类”而是按 MRO 顺序调下一个类class A: def hello(self): print(A) class B(A): def hello(self): print(B) super().hello() # 沿 MRO 找下一个此处是 A class C(A): def hello(self): print(C) super().hello() class D(B, C): def hello(self): print(D) super().hello() D().hello() # D B C A print([c.__name__ for c in D.__mro__]) # D B C A object输出D B C A这就是 C3 算法的线性化结果保证每个类只出现一次且子类排父类之前同时保持各父类在声明中的相对顺序。MRO 的答法是先给__mro__这个入口再说 C3 解决的是多继承下方法查找顺序和一致性问题不必手推算法细节。4.2 双下划线特殊方法与单双下划线约定常考的几个__new__负责创建实例、__init__负责初始化、__call__让实例能加括号调用、__str__决定print输出、__getattr__在属性找不到时触发、__setattr__在设置属性时触发、__delattr__在删除属性时触发、__doc__是类文档且不继承。单下划线和双下划线的区别要讲清是两种机制单下划线是约定表示“内部用、别在外面调”解释器不做强制双下划线会触发名字改写__x在类A里变成_A__x所以子类访问不到看起来像私有实际是改名而非真正的访问控制。class A: def __init__(self): self._protect 1 # 约定保护子类可用 self.__private 2 # 改写为 _A__private a A() print(a._protect) # 1 print(a._A__private) # 2知道了改写规则仍能访问面试官爱在这里追问“Python 有没有真正的私有”答案是靠改名实现接近私有的效果绕过规则依然能访问所以是约定加机制的组合。4.3 实例变量、类变量与三种方法类变量在所有实例间共享实例变量各自独有查找时先实例后类赋值时只要写self.x就创建实例属性、不影响类属性。三种方法的区分点在于第一个参数和调用方式实例方法第一个参数是self只能实例调用类方法用classmethod第一个参数是cls实例和类都能调常用于备选构造器静态方法用staticmethod不带self或cls就是挂在类命名空间下的普通函数。isinstance和type的差别也别漏type(obj) A对子类实例返回Falseisinstance(obj, A)会考虑继承链返回True。这题答完给一行代码对照就行不用长篇。4.4 with 语句与单例两道必写题with的价值在于异常安全不管块内出什么错都会执行清理前提是对象实现了__enter__和__exit__class W: def __enter__(self): print(进入 with 语句) return self def __exit__(self, exc_type, exc_val, exc_tb): print(退出 with 语句) return False # 返回 False 表示异常继续向上抛 with W() as w: print(之前) print(w) print(之后)__enter__的返回值绑定给as后面的变量__exit__收三个参数分别是异常类型、异常值、回溯对象返回True会吞掉异常返回False或None继续抛。这层细节说出来比只会写__enter__和__exit__两个方法名强一档。单例是手写题里出现频率最高的至少准备两种import threading class Singleton: _instance_lock threading.Lock() def __new__(cls, *args, **kwargs): if not hasattr(cls, _instance): with cls._instance_lock: # 双检加锁避免并发下重复创建 if not hasattr(cls, _instance): cls._instance super().__new__(cls) return cls._instance a, b Singleton(), Singleton() print(a is b) # True另一种是用元类在__call__里控制实例创建本质是拦截“类加括号”这个动作。答题时把__new__版作为主答案说明为什么加锁、为什么双重判断再提一句元类版和模块级单例直接用一个模块变量两种替代方案完整度就够了。5. 手写结构栈、队列、链表逆置考的是边界而不是难度5.1 栈与“两个队列实现栈”的效率差栈的核心就两条后进先出入栈出栈都要 O(1)。链表节点版写法class Node: def __init__(self, val): self.val val self.next None # 指向下一个节点栈顶用 head 表示 class Stack: def __init__(self): self.top None def push(self, n): node Node(n) node.next self.top # 新节点指向原栈顶 self.top node # 栈顶前移 return node.val def pop(self): if self.top is None: return None val self.top.val self.top self.top.next # 栈顶下移 return val用队列实现栈则是把 A 队列的元素依次挪到 B 只留最后一个弹出后再换回来单次弹出 O(n)。答题时的关键不是写出来而是主动比较链表版 O(1)队列版 O(n)面试官问“为什么”的时候你要能说清挪元素的成本在哪。5.2 队列实现与链表逆置队列版本的要素是is_empty、is_full、enqueue、dequeue四个方法配一个固定容量和列表存储。答题时说明insert(0, val)是 O(n) 的真正工程里用collections.deque这句话能救回不少分因为它说明你知道内置实现的取舍。单链表逆置是经典中的经典三指针迭代class ListNode: def __init__(self, val): self.val val self.next None def reverse_list(head): last None # 已反转部分的头 while head: tmp head.next # 先存下一个防止断链 head.next last # 当前节点指回前面 last head # last 前移 head tmp # head 前移 return last # 循环结束时 last 是新头三个指针的移动顺序不能乱tmp必须先存否则head.next一改就找不到后面。面试时边说边画把“存下一个、指回前面、两头前移”这三步念出来比闷头写更稳。递归版也要能说思路是反转后续再调整指向代价是 O(n) 栈空间。5.3 可调用对象与类的加载过程callable()判断一个对象能不能加括号调用。可调用对象包括用户自定义函数、内置函数、内置方法、类里定义的方法、类本身、实现了__call__的实例、生成器函数。最后一条是重点类实例默认不可调用类里定义了__call__之后就可以。类从定义到实例化的顺序也常考类体代码执行、生成类对象、父类与基类的查找建立在 MRO 上实例化时先调__new__创建对象再调__init__初始化。答题时把“先 new 后 init”这条主线抓住再补一句__new__必须返回实例不返回则__init__不执行这层细节就是区分度。6. 易错点这五处失分最多考前务必过一遍6.1 首字母大写题目里的循环范围写反那道九个字母填数字的题A、C、E、G、P 都从 1 开始B、D、F、H 从 0 开始因为前五个是两位数的十位和三位数的百位不能为 0。错就错在把十位也从 0 开始或者去重判断写成if A B: continue却忘了后面每个字母都要跟前面所有字母比。正确做法是每层用一个集合或列表做已用数字判断条件写成if x in used: continue这样每加一层只改一个变量不容易漏。写完之后必须代入验证(A*10B) - (C*10D) (E*10F)且(E*10F) (G*10H) P*111把 P 的三位表示写成P*100P*10P是为了和题目形式对齐别写成P*100漏掉个位。6.2 装饰器顺序背反最常见的错是把装饰阶段的执行顺序说成从上往下。记住“写在上面的包得晚、执行得晚”decorator_b在上面它最后执行贴近函数的那层先执行。另一个高频错是漏答“装饰器在定义时就执行”这一层只讲调用顺序考官一问“那两个 print 什么时候打的”就答不上来。答法固定成两段定义阶段自下而上、调用阶段自上而下再给等价写法。6.3 finally 里写 return 的后果很多人只知道finally一定执行不知道finally里的return会覆盖前面的返回值甚至吞掉异常。面试题里如果出现这种代码正确答法是先指出返回值会被覆盖再说这是反模式、工程里应避免。反过来还有一问try里return之后finally还执行吗执行且在返回前执行。这两条一起答才算完整。6.4 可变默认参数只初始化一次虽然题库里没直接考但和函数作用域同属高频追问def add(item, bucket[]): # 默认值在函数定义时求值只算一次 bucket.append(item) return bucket print(add(1)) # [1] print(add(2)) # [1, 2]不是 [2]默认参数在def执行时求值并绑定一次之后所有调用共享同一个列表。正确写法是默认值设None函数体内判空再建新列表。这题和全局变量那题是同一类错误思维把“看起来每次新建”想当然。答这类题的标准口径是先演示两次调用的输出差异再说机制最后给修复写法。6.5 概念题答成名词解释没有场景问 logging 作用时只答“记录日志”问 OSI 七层时只背层名都拿不到分。logging 要答出“分级、可输出到控制台和文件、可按模块配置、生产环境用它替代 print”最好补一句logging.getLogger(__name__)的模块化用法OSI 要答出分层是为了职责隔离并落到“应用层对应 HTTP 这类协议、传输层管端到端”这种可对应的说法别只报层名。概念题给一个真实场景比给一段定义值钱得多。7. 最后一周怎么练把 55 道题拆成三层过到这一步题目本身已经不重要了重要的是你用什么节奏把它们变成能当场输出的能力。我自己的做法是把题分三层。第一层是“看代码说输出”包括作用域、装饰器、异常、字典、可变默认参数这几类每天早上拿五道出来不看答案先手写输出写完对照错的那道标记出来隔天再写一遍。这一层的目标是形成条件反射看到a 3在函数里就想到UnboundLocalError看到两层装饰器就先写等价表达式。第二层是“现场手写”栈、队列、链表逆置、单例、with类、目录遍历一共七八个全部背到能默写重点是边界处理空栈pop返回什么、栈顶空了怎么判断、链表只有一个节点怎么办。手写题失分从来不在思路在边界。第三层是概念口述把三大特性、鸭子类型、MRO、单双下划线、isinstance与type、match与search、贪婪匹配、logging、JSON 扩展、爬虫模块这些挨个用手机录音讲一遍讲完回放听自己哪里卡壳。卡壳的地方基本都是只有名词没有场景补一个例子就通了。下面这张表可以当自查清单用每行能不看资料说满三十秒就算过模块自查问题过关标准作用域函数内修改全局变量为什么报错说出局部绑定判定 两种改法装饰器两层装饰器的打印顺序分定义、调用两阶段讲清异常finally 与 return 的先后说出执行链路和覆盖风险数据结构去重求和的复杂度从 O(n²) 优化到 O(n)面向对象super 与 MRO 的关系举多重继承例子说明查找顺序手写链表逆置三指针边写边讲每步作用正则贪婪与非贪婪的场景差别用标签匹配举例还有一个血泪经验面试时被问到不会的题别硬编。比如被问 C3 算法的完整推导答“我能说清 MRO 的线性化目标和__mro__怎么看具体推导过程我需要再确认”就比胡说一通强面试官对诚实边界的容忍度远高于对错误的容忍度。另一条是代码题写完主动跑一个用例哪怕只是口头跑[3,4,1,2,5,6,6,5,4,3,3]这一组也能暴露大部分边界问题。从那以后我每次准备这类面试都会强制走一遍“手写输出、默写结构、录音讲概念”这三步不跳步也不靠临场发挥。这份 55 道题的整理就是按这个顺序排的题和答案都在缺的只是你按三层节奏过一遍。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑