python实习面试基础题

📅 2026/7/20 15:44:45 👁️ 阅读次数 📝 编程学习
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,) # 加逗号才是元组

逗号是关键,不是括号。

源码级理解(加分项)

列表底层是PyListObject(C 数组 + 动态扩容策略 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.1002s

@timer等价于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(n=3) def greet(name): print(f"Hello, {name}!")

@repeat(n=3)等价于greet = repeat(n=3)(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/cls
@classmethod类方法,自动传 cls
@property把方法变成属性访问
@functools.wraps保留被装饰函数的元信息,写装饰器时务必加上
@functools.lru_cache缓存函数结果(LRU 策略)
@functools.singledispatch单分派泛函数(根据第一个参数类型重载)

真实项目中的应用

  • Flask/FastAPI 路由注册@app.get("/api/users")
  • Django 权限校验@login_required@permission_required
  • Django 缓存@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. 省内存— 不存结果,只存配方
  2. 一次性的— 不能后退,不能随机访问,用完即弃

三种创建方式

# 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
作用返回值并终止函数产出值并暂停函数
执行状态丢弃局部变量保留局部变量和指令指针
再次调用从头执行从暂停处恢复
是否可多次产出❌ 一次✅ 多次

生成器的常见应用

  1. 处理大文件(这是最经典的面试场景)
def read_large_file(file_path): """逐行读取,任何时候内存只有一行""" with open(file_path, 'r', encoding='utf-8') as f: for line in f: yield line.strip() # 即使文件 20GB,内存也只占用一行的大小 for line in read_large_file("huge_log.txt"): process(line)
  1. 无限序列
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
  1. 管道式数据处理(多生成器串联)
# 从日志文件读 → 过滤 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 from

def 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→ 节省内存、流式处理用的时候才算

📚 延伸阅读建议

  1. 列表 vs 元组:Python 官方文档 Data Structures
  2. 装饰器:查阅functools模块源码;读《Python Cookbook》第 9 章
  3. 生成器:PEP 255(生成器起源),PEP 380(yield from语法)
  4. 实战吃透这三个概念,推荐做一下 Python 官方教程的 Generator Tricks for System Programmers(David Beazley 的经典教程)

📝面试复盘心得
这三个问题是 Python 面试的"入门三件套",看似基础但层层深入可以考察到源码级别。回答时注意:

  • 先给最清晰的结论,再展开细节
  • 每个解释都配代码片段(面试官很看重你能不能把抽象概念讲清楚)
  • 如果你能自然地提到底层实现(C 源码层面)或者库级应用(Flask/Django),会是很大的加分项