一、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。如果你确实需要并行计算,有几种方案:

  1. 多进程:每个进程有自己的GIL,真正并行。用multiprocessing模块。
  2. 使用C扩展:在C代码里手动释放GIL。比如numpy就是这么干的。
  3. 异步编程:用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都出在这里。

不可变对象

创建后就不能修改的对象。包括:intfloatstrtuplefrozenset

a = 10
b = a
a = 20
print(b)  # 输出10,b没变

为什么会这样?因为a = 20不是修改了10这个对象,而是让a指向了新的对象20。b还指着原来的10。

可变对象

创建后可以修改。包括:listdictset、自定义类的实例。

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的数据结构,特别是列表和字典的底层实现。