一、Python解释器类型:不只是CPython
面试时我经常问候选人:「Python解释器有哪几种?」很多人只会说CPython。其实这背后藏着不少门道。
Python解释器主要有这么几种:
- CPython:官方默认实现,用C语言写的。我们平时用的就是它。
- PyPy:用Python写的Python解释器。它有个JIT编译器,跑某些代码能快好几倍。
- Jython:运行在JVM上的Python解释器。可以和Java无缝集成。
- IronPython:运行在.NET平台上的Python解释器。
我个人习惯用CPython,毕竟最稳定、生态最全。但如果你做高性能计算,PyPy值得一试。我在项目中遇到过用PyPy跑数据处理脚本,速度提升了3倍多——当然,前提是你的代码没有太多C扩展依赖。
面试高频题:「CPython和PyPy有什么区别?」
核心区别在于:CPython是解释执行,PyPy有JIT编译。但PyPy对C扩展支持不好,很多第三方库用不了。
二、GIL全局解释器锁:Python的「紧箍咒」
GIL,全称Global Interpreter Lock。说白了就是一把大锁,保证同一时刻只有一个线程在执行Python字节码。
为什么会这样?CPython的内存管理不是线程安全的。没有GIL的话,多个线程同时操作对象引用计数,程序直接崩给你看。
GIL带来的影响
- CPU密集型任务:多线程反而更慢。因为线程切换有开销,但同一时刻只能跑一个线程。
- IO密集型任务:多线程有效。因为线程在等待IO时会释放GIL,其他线程可以趁机执行。
避坑指南:我曾经在爬虫项目里用多线程做CPU密集的数据解析,结果发现8核CPU只跑满了一核。后来改成多进程,速度直接翻了6倍。
如何绕过GIL?
嗯,这里要注意,不是所有场景都需要绕过GIL。如果你确实需要并行计算,有几种方案:
- 多进程:每个进程有自己的GIL,真正并行。用
multiprocessing模块。 - 使用C扩展:在C代码里手动释放GIL。比如
numpy就是这么干的。 - 异步编程:用
asyncio做协程,单线程内实现高并发。
# 多进程示例
from multiprocessing import Pool
def cpu_intensive_task(n):
return sum(i * i for i in range(n))
if __name__ == '__main__':
with Pool(4) as p:
results = p.map(cpu_intensive_task, [1000000] * 4)
print(results)
注意:多进程有通信开销。进程间传递大数据对象时,序列化反序列化很耗时。你想想看,如果每个任务只算1毫秒,通信却要10毫秒,那还不如单线程。
三、可变与不可变对象:Python的「变」与「不变」
这是Python基础里的核心概念。很多bug都出在这里。
不可变对象
创建后就不能修改的对象。包括:int、float、str、tuple、frozenset。
a = 10
b = a
a = 20
print(b) # 输出10,b没变
为什么会这样?因为a = 20不是修改了10这个对象,而是让a指向了新的对象20。b还指着原来的10。
可变对象
创建后可以修改。包括:list、dict、set、自定义类的实例。
list_a = [1, 2, 3]
list_b = list_a
list_a.append(4)
print(list_b) # 输出[1, 2, 3, 4],list_b也跟着变了
面试高频题:「函数的默认参数为什么不能用可变对象?」
因为默认参数在函数定义时只创建一次。如果你用def func(lst=[]),多次调用会共享同一个列表对象。
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — 不是[2]!
我建议默认参数永远用None,然后在函数内部创建新对象。
四、深拷贝与浅拷贝:复制也有门道
面试时我经常让候选人手写一个深拷贝。很多人栽在这里。
浅拷贝
只复制最外层对象,内部元素还是引用。用copy.copy()或切片[:]。
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] — 内部变了!
深拷贝
递归复制所有层级的对象。用copy.deepcopy()。
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original) # [[1, 2], [3, 4]] — 没变!
避坑指南:我曾经在项目里用浅拷贝复制了一个嵌套字典,结果改了一个值,原始数据也跟着变了。排查了半天才发现是拷贝的问题。从那以后,只要涉及嵌套结构,我直接用深拷贝。
什么时候用浅拷贝?
其实浅拷贝也有用武之地:
- 对象只有一层,没有嵌套结构
- 你明确知道内部元素不会被修改
- 性能敏感场景,深拷贝递归遍历很慢
自定义对象的深拷贝
如果你的类里有文件句柄、数据库连接等资源,深拷贝会出问题。需要实现__deepcopy__方法。
class MyClass:
def __init__(self, data):
self.data = data
self.file = open('test.txt', 'w')
def __deepcopy__(self, memo):
# 不复制文件句柄,只复制数据
return MyClass(self.data)
小技巧:面试时如果被问到深拷贝,可以提一下memo参数的作用——它用来记录已经拷贝过的对象,防止循环引用导致无限递归。
好了,这一章的内容就到这里。Python基础看似简单,但面试官往往从这些细节判断你的功底深浅。下一章我们聊聊Python的数据结构,特别是列表和字典的底层实现。