python实习面试基础题

发布时间:2026/7/21 22:18:56
python实习面试基础题 面试日期2026-07-19考察方向Python 基础数据结构、语言特性难度等级⭐ 基础必问一、列表list和元组tuple的区别核心答案对比维度列表list元组tuple可变性✅ 可变mutable——可增删改元素❌ 不可变immutable——创建后不可修改语法[1, 2, 3]方括号(1, 2, 3)圆括号性能相对较慢动态扩容内存分配更复杂相对较快内存连续紧凑Python 可缓存复用哈希性❌ 不可哈希不能做字典 key✅ 可哈希元素也需可哈希可做字典 key内存占用更大预留额外空间供 append更小固定大小使用场景同类型元素的集合需要增删改异构数据如坐标、数据库一条记录不该变的深入追问点Q: 元组不可变为什么还能t (1, 2); t (3,)这种操作那不是修改原元组而是创建一个新元组重新赋值给变量。原元组并没有变可以用id()验证前后变化。Q: 元组里有可变对象如列表会怎样元组的不可变是指引用的不可变——你不能让元组里的某个元素指向另一个对象。但如果元素本身是可变对象如列表你仍然可以修改那个列表的内容。t ([1, 2], 3) t[0].append(99) # ✅ 可以t 变成 ([1, 2, 99], 3) t[0] [] # ❌ TypeError不能重新赋值Q: 创建单元素元组要注意什么a (1) # 这是 int不是元组 b (1,) # 加逗号才是元组逗号是关键不是括号。源码级理解加分项列表底层是PyListObjectC 数组 动态扩容策略 over-allocate ~1/8元组底层是PyTupleObject固定数组Python 解释器对小元组有缓存池。这也是元组创建比列表快的原因之一。 面试回答技巧先旗帜鲜明地摆出核心区别可变 vs 不可变再从可变性推导出衍生区别可哈希性、性能、使用场景如果面试官追问底层可以提到PyListObject的 over-allocate 机制二、装饰器Decorator核心答案装饰器是一种高阶函数它接收一个函数作为参数返回一个新的函数或可调用对象在不修改原函数代码的前提下为其附加功能。最简示例import functools import time def timer(func): 装饰器打印函数执行时间 functools.wraps(func) # 保留原函数的元信息name、doc 等 def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) elapsed time.perf_counter() - start print(f{func.__name__} 耗时 {elapsed:.4f}s) return result return wrapper timer def slow_add(a, b): 计算两数之和模拟耗时 time.sleep(0.1) return a b # 调用 slow_add(1, 2) # 输出: slow_add 耗时 0.1002stimer等价于slow_add timer(slow_add)这就是语法糖的本质。带参数的装饰器def repeat(n: int): 装饰器工厂让函数重复执行 n 次 def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(n - 1): func(*args, **kwargs) return func(*args, **kwargs) # 最后一次返回结果 return wrapper return decorator repeat(n3) def greet(name): print(fHello, {name}!)repeat(n3)等价于greet repeat(n3)(greet)——外层先调用工厂拿到真正的装饰器再把函数传进去。类装饰器class CountCalls: def __init__(self, func): self.func func self.count 0 def __call__(self, *args, **kwargs): self.count 1 return self.func(*args, **kwargs) CountCalls def say_hi(): print(Hi!) say_hi() # count 1 say_hi() # count 2如果类实现了__call__它就是可调用对象可以当作装饰器用。适合需要维护状态的场景。多个装饰器的执行顺序decorator_a decorator_b decorator_c def f(): pass # 等价于 f decorator_a(decorator_b(decorator_c(f))) # 执行顺序离函数最近的最先装饰最远的最先执行常用内置装饰器装饰器用途staticmethod静态方法不传 self/clsclassmethod类方法自动传 clsproperty把方法变成属性访问functools.wraps保留被装饰函数的元信息写装饰器时务必加上functools.lru_cache缓存函数结果LRU 策略functools.singledispatch单分派泛函数根据第一个参数类型重载真实项目中的应用Flask/FastAPI 路由注册app.get(/api/users)Django 权限校验login_required、permission_requiredDjango 缓存cache_page(60 * 15)日志/监控自动记录函数入参、出参、耗时事务管理transaction.atomic重试机制函数执行失败时自动重试 面试回答技巧三步法先说是高阶函数→ 再说语法糖→ 再给一个最简例子用functools.wraps展示专业度面试官追问带参数装饰器时自然地引出三层嵌套结构外层工厂 → 中层装饰器 → 内层 wrapper加分点提到functools.wraps的重要性不写的话原函数的__name__、__doc__都会被 wrapper 覆盖三、生成器Generator核心答案生成器是一个可迭代对象但它不一次性把所有值都算出来而是按需逐个产出yield因此特别节省内存。本质上是惰性求值lazy evaluation。个人总结生成器简单来说就是一种不会一次性算出所有结果而是用到时才计算一个值返回的特殊迭代器。它最大的好处就是省内存特别适合处理大数据流或无限序列以及进行惰性计算。 用一个生活例子彻底搞懂惰性求值普通列表list像去自助餐厅——厨师一次性把所有菜都做好摆出来。如果餐厅有 100 道菜你哪怕只吃 3 道厨房也得先做完 100 道、占用 100 个餐盘的位置。# 列表一次性算完所有值全部存在内存里 nums [x for x in range(1_000_000)] # ✅ 马上生成 100 万个数字占用约 28MB 内存 print(nums[0]) # 1 —— 但其实你已经把 100 万个都算完了生成器generator像去日式 omakase 餐厅——厨师等你吃完一道才做下一道。不管菜单上有多少道菜任何时候厨房里只有一道菜在准备。# 生成器用到的时候才算算完就丢 nums (x for x in range(1_000_000)) # ✅ 瞬间返回几乎不占内存 print(next(nums)) # 1 —— 到现在为止只算了 1 个数字 print(next(nums)) # 2 —— 继续算下一个验证内存差异import sys # 列表——所有值已在内存 list_nums [x for x in range(100_000)] print(sys.getsizeof(list_nums)) # ~824KB # 生成器——只有代码和状态没有值 gen_nums (x for x in range(100_000)) print(sys.getsizeof(gen_nums)) # ~112 字节固定不管 range 多大列表每多一个元素就多占一份内存生成器无论迭代 100 个还是 10 亿个本身的大小几乎不变因为它不存值只存怎么算下一个值的规则。 打个比方两本书的区别普通列表生成器像一本已经印好的书像一本读一页印一页的书你可以翻到任意页码你只能一直往前读不能回翻书已经完整存在占书架位置只保留了怎么印下一页的配方可以反复读开头读完了就没了不能回头所以生成器的两个核心特征就很好记了省内存— 不存结果只存配方一次性的— 不能后退不能随机访问用完即弃三种创建方式# 1. 生成器函数用 yield def count_up_to(n): i 1 while i n: yield i i 1 gen count_up_to(5) for num in gen: print(num) # 1 2 3 4 5 # 2. 生成器表达式 squares (x * x for x in range(10)) # 注意是 () 不是 [] print(list(squares)) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # 3. 第三方/内置函数返回生成器 lines (line.strip() for line in open(file.txt))yield 的执行机制关键def demo(): print(→ 开始) yield 1 print(→ 继续) yield 2 print(→ 结束) g demo() result next(g) # 输出 → 开始result 1 result next(g) # 输出 → 继续result 2 next(g) # 输出 → 结束抛出 StopIteration每次调用next()函数从上次yield的地方继续执行直到下一个yield。这就是协程的雏形。yield 和 return 的对比returnyield作用返回值并终止函数产出值并暂停函数执行状态丢弃局部变量保留局部变量和指令指针再次调用从头执行从暂停处恢复是否可多次产出❌ 一次✅ 多次生成器的常见应用处理大文件这是最经典的面试场景def read_large_file(file_path): 逐行读取任何时候内存只有一行 with open(file_path, r, encodingutf-8) as f: for line in f: yield line.strip() # 即使文件 20GB内存也只占用一行的大小 for line in read_large_file(huge_log.txt): process(line)无限序列def fibonacci(): a, b 0, 1 while True: yield a a, b b, a b fib fibonacci() for _ in range(10): print(next(fib), end ) # 0 1 1 2 3 5 8 13 21 34管道式数据处理多生成器串联# 从日志文件读 → 过滤 ERROR → 提取 IP → 统计 lines (line for line in open(app.log)) errors (line for line in lines if ERROR in line) ips (line.split()[0] for line in errors) seen set() unique_ips (ip for ip in ips if ip not in seen and not seen.add(ip)) for ip in unique_ips: print(ip) # 整个处理过程是流式的内存恒定与迭代器的关系# 生成器自动实现了迭代器协议__iter__ 和 __next__ g (x for x in range(3)) print(iter(g) is g) # True —— 生成器本身就是自己的迭代器新增特性yield fromdef chain(*iterables): 串联多个可迭代对象 for it in iterables: yield from it # 等价于 for item in it: yield item list(chain([1, 2], ab)) # [1, 2, a, b]生成器的血条面试引导技巧生成器是一次性的遍历完就空了不能回头。需要重复用就转成 list但那样就失去内存优势了。 面试回答技巧用类比普通函数是餐馆一次性把菜全上齐生成器是吃一道上一道最加分的回答路径先说生成器是惰性求值 → 对比列表推倒式的内存差异 → 举大文件读取的例子 → 提一下yield from和协程方向面试官如果问生成器和协程的关系可以说yield是 Python 实现协程的基础在async/await出现前的原始协程展示你知识面的纵深 横向对比总结表问题关键词一句话记忆列表 vs 元组可变 vs 不可变→ 可哈希性、性能、用途列表变元组不变装饰器高阶函数 语法糖→ AOP、闭包给函数套个壳生成器惰性求值 yield→ 节省内存、流式处理用的时候才算 延伸阅读建议列表 vs 元组Python 官方文档 Data Structures装饰器查阅functools模块源码读《Python Cookbook》第 9 章生成器PEP 255生成器起源PEP 380yield from语法实战吃透这三个概念推荐做一下 Python 官方教程的 Generator Tricks for System ProgrammersDavid Beazley 的经典教程面试复盘心得这三个问题是 Python 面试的入门三件套看似基础但层层深入可以考察到源码级别。回答时注意先给最清晰的结论再展开细节每个解释都配代码片段面试官很看重你能不能把抽象概念讲清楚如果你能自然地提到底层实现C 源码层面或者库级应用Flask/Django会是很大的加分项