Python 基础面试题
list 和 tuple 有什么区别?
难度:简单 | 标签:数据类型, list, tuple
- 可变性:
list可变,可以增删改元素;tuple不可变,创建后不能修改元素。 - 语法:
list使用[],tuple使用();单元素 tuple 需要写成(1,)。 - 性能:tuple 结构更紧凑,创建和访问略快,占用内存更少。
- 哈希:只有元素都可哈希的 tuple 才能作为 dict 的 key 或 set 的元素,list 不行。
- 语义:list 表示一组同类型、可变的序列;tuple 通常表示一条不可变的结构化记录。
point = (3, 4)
point[0] = 5 # TypeError: 'tuple' object does not support item assignment
numbers = [1, 2, 3]
numbers.append(4) # [1, 2, 3, 4]注意:tuple 的「不可变」是浅层的,如果 tuple 里存放的是 list,这个 list 本身仍然可以被修改。
什么是 GIL?它对多线程有什么影响?
难度:中等 | 标签:GIL, 并发, 多线程
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器中的一把互斥锁。同一时刻只允许一个线程执行 Python 字节码,因此多线程无法真正利用多核 CPU 并行执行 Python 代码。
影响:
- CPU 密集型任务:多线程受 GIL 限制,无法提速,应使用多进程(
multiprocessing); - I/O 密集型任务:线程在等待 I/O 时会释放 GIL,多线程仍然有效,也可以使用
asyncio; - 扩展模块:NumPy 等 C 扩展在执行计算时会释放 GIL,因此能获得一定并行能力。
需要强调:GIL 是 CPython 的实现细节,不是 Python 语言规范。Jython、IronPython 没有 GIL;Python 3.13 开始也提供了实验性的 free-threaded 构建(PEP 703)。
Python 装饰器的原理是什么?
难度:中等 | 标签:装饰器, 闭包, 高阶函数
装饰器本质上是一个接收函数并返回新函数的高阶函数,常用于在不修改原函数代码的前提下增加日志、缓存、权限校验等能力。它依赖闭包保存原函数。
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timer
def slow_task(n):
time.sleep(n)
# 等价于 slow_task = timer(slow_task)要点:
@functools.wraps(func)会保留原函数的__name__、__doc__等元信息,排查问题更方便;- 带参数的装饰器需要再包一层:
def repeat(times): def decorator(func): ... return decorator; - 多个装饰器从下往上依次包装,执行时从上往下进入。
生成器和迭代器有什么区别?
难度:中等 | 标签:生成器, 迭代器, 惰性求值
- 迭代器:实现了
__iter__()和__next__()协议的对象,能被for循环遍历;__next__()每次返回一个元素,耗尽后抛出StopIteration。 - 生成器:一种用
yield快速创建迭代器的语法糖(也可以是生成器表达式),自动实现了迭代器协议,并且是惰性求值的。
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip()
# 不会一次性把整个文件读进内存
for line in read_lines("big.log"):
print(line)区别在于:迭代器是协议,生成器是实现方式之一;生成器函数调用后不会立即执行,而是在每次 next() 时运行到下一个 yield,适合处理大文件、数据流等内存敏感场景。
Python 的深拷贝和浅拷贝有什么区别?
难度:简单 | 标签:拷贝, copy, 引用
- 赋值:只是给同一个对象增加一个引用,不产生新对象;
- 浅拷贝(
copy.copy、切片、list()):创建新的外层容器,但内部元素仍然是原对象中元素的引用; - 深拷贝(
copy.deepcopy):递归复制所有层级,得到的对象与原对象完全独立。
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
b[0][0] = 99
print(a) # [[99, 2], [3, 4]],浅拷贝共享内层列表
c[1][1] = 100
print(a) # [[99, 2], [3, 4]],深拷贝互不影响deepcopy 会维护一个 memo 字典处理循环引用,因此性能开销更大;对于不可变对象(如 int、str、tuple 中的不可变元素),deepcopy 通常会直接返回原对象。
asyncio 的协程为什么适合 I/O 密集型任务?
难度:中等 | 标签:asyncio, 协程, 异步编程
asyncio 基于事件循环,在单线程内通过协作式调度实现并发。协程遇到 await 的 I/O 操作时会主动让出控制权,事件循环转而执行其他就绪任务,等 I/O 完成后再恢复执行,从而避免线程阻塞和上下文切换开销。
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
pages = await asyncio.gather(
fetch(session, "https://example.com/a"),
fetch(session, "https://example.com/b"),
)
return pages
asyncio.run(main())适用场景:网络请求、数据库访问、文件 I/O 等等待时间远大于计算时间的任务。不适合 CPU 密集型任务,因为协程无法绕过 GIL,计算仍然会阻塞事件循环;这类任务应交给多进程或线程池。