先测量,再优化:timeit 与 cProfile、GIL 真正阻止的是什么、生成器的节俭,以及字符串和切片的内存陷阱。
在 Kodokon 中打开本课永远不要猜测,要测量。timeit 能隔离出一个可靠的微基准:它会关闭垃圾回收器,并重复执行以抹平噪声。cProfile 则勾勒出整个程序的调用全貌 - 按 cumtime(累计时间,包含子调用)排序,就能找出真正的罪魁祸首。任何性能工作的黄金法则是:那行慢代码几乎从来都不是你怀疑的那一行。
python -m timeit "sum(x * x for x in range(1000))"
python -m timeit -s "s = 'abc' * 100" "s.upper()"
python -m cProfile -s cumtime -m timeit "min(range(50))"GIL(全局解释器锁)保证在一个进程内,同一时刻只有一个线程执行 Python 字节码。结果就是:线程无法加速纯计算。不过它们对 I/O 依然完美,因为在网络或磁盘读取期间 GIL 会被释放,NumPy 这类许多 C 语言库也会释放它。要用计算把多个核心跑满,就得走 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。Python 3.13 提供了一个实验性的、不带 GIL 的"自由线程"(free-threaded)变体,但它在生产环境中仍然罕见。
import sys
squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(squares_list), "bytes")
print(sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))无论序列有多长,一个生成器都只有几百字节重:它只保存自己的执行状态,并按需产出每一个值。把它们串成流水线 - sum(x * x for x in data) - 就能处理数据流,而无需实体化任何中间列表。有一个需要知道的取舍:生成器是一次性的。一旦耗尽,它就不再产出任何东西,而且悄无声息 - 这是一类经典的 bug 来源,第二次迭代看上去就像是"空的"。
import timeit
def concat(n: int) -> str:
out = ""
for i in range(n):
out += str(i)
return out
def join(n: int) -> str:
return "".join(str(i) for i in range(n))
t1 = timeit.timeit(lambda: concat(20_000), number=10)
t2 = timeit.timeit(lambda: join(20_000), number=10)
print(f"concat: {t1:.3f} s / join: {t2:.3f} s")sys.getsizeof 与等价的列表相比小得可怜?''.join(parts) 胜过在循环里反复执行 out += part?