96_Python性能优化技巧
Python性能优化技巧
文章目录
- Python性能优化技巧
- 前言
- 一、数据结构选择:列表 vs 元组
- 二、生成器:延迟计算的艺术
- 三、使用缓存加速重复计算
- 四、本地变量优化
- 五、字符串拼接优化
- 六、选择合适的数据遍历方式
- 七、使用 C 扩展和 Cython
- 八、性能分析工具
- 总结
- 亮点总结
- 适用场景
- 扩展方向
前言
Python 以开发效率高著称,但在某些场景下其运行效率可能成为瓶颈。性能优化不是盲目追求速度——在1%的代码上花80%的时间优化是典型的时间浪费——而是用最小代价换取最大收益。优化的二八法则:80%的执行时间往往集中在20%的代码中,找到这段代码并用对方法优化,比全局微调有效得多。
面试常见考点:列表 vs 元组的内存差异、生成器与列表推导式的适用场景、lru_cache的原理和使用、join()比+快的原因(因为join预先计算出总长度一次性分配内存)、本地变量查找比全局变量快的原因(LEGB规则)。面试官喜欢问"你做过哪些性能优化?",回答时一定要带具体数字(优化前X秒,优化后Y秒)。本文将介绍从数据结构选择、代码编写习惯、标准库利用到C 扩展集成的一系列实用优化技巧。每个技巧都配有可运行的代码示例和性能对比,帮助你在实际项目中做出正确的选择。
一、数据结构选择:列表 vs 元组
元组的访问速度略快于列表,且内存占用更小。当数据不可变时,优先使用元组:
importtimeitimportsys list_data=list(range(1000))tuple_data=tuple(range(1000))# 内存对比print(f"列表内存:{sys.getsizeof(list_data)}字节")print(f"元组内存:{sys.getsizeof(tuple_data)}字节")# 访问速度t1=timeit.timeit(lambda:list_data[500],number=10_000_000)t2=timeit.timeit(lambda:tuple_data[500],number=10_000_000)print(f"列表索引{t1:.4f}s,元组索引{t2:.4f}s")集合 vs 列表的查找性能是更显著的差异:
importtimeit n=100000my_list=list(range(n))my_set=set(range(n))# 成员检查list_time=timeit.timeit(lambda:99999inmy_list,number=1000)set_time=timeit.timeit(lambda:99999inmy_set,number=1000)print(f"列表查找{list_time:.6f}s")# O(n) - 需遍历print(f"集合查找{set_time:.6f}s")# O(1) - 哈希查找实用建议:需要频繁成员检查时,使用set或dict;需要有序存储时用list;不可变数据用tuple。
二、生成器:延迟计算的艺术
当处理大量数据时,生成器可以避免一次性将所有数据加载到内存中。这是Python性能优化的第一原则——不是让代码算得更快,而是让代码算得更少。如果只需要前10个结果,为什么要把100万元素全部计算出来?生成器实现的延迟计算(lazy evaluation)正是这个思想的最佳体现。注意区分生成器函数(使用yield)和生成器表达式(使用()圆括号)——两者都是生成器,但表达式的语法更紧凑,适合简单到中等复杂度的场景。
importsysimporttimedefread_list(filename):"""一次性加载所有行"""withopen(filename,"r",encoding="utf-8")asf:returnf.readlines()defread_generator(filename):"""逐行读取"""withopen(filename,"r",encoding="utf-8")asf:forlineinf:yieldline.strip()# 生成器表达式 vs 列表推导式squares_list=[x*xforxinrange(1000000)]# 立即分配大列表squares_gen=(x*xforxinrange(1000000))# 不分配,按需生成print(f"列表内存:{sys.getsizeof(squares_list)}字节")print(f"生成器内存:{sys.getsizeof(squares_gen)}字节")处理管道式数据流时,生成器尤为强大:
defread_numbers(filename):withopen(filename)asf:forlineinf:yieldint(line.strip())deffilter_even(numbers):forninnumbers:ifn%2==0:yieldndefsquare(numbers):forninnumbers:yieldn*n# 管道串联,每次只处理一个元素pipeline=square(filter_even(read_numbers("data.txt")))forresultinpipeline:print(result)三、使用缓存加速重复计算
functools.lru_cache是一个简单高效的缓存装饰器。LRU的全称是Least Recently Used(最近最少使用),当缓存满时,它会驱逐最久未被访问的条目。maxsize参数控制缓存大小,对于无限制缓存可以设为None(但要注意内存增长)。缓存的适用条件:被缓存函数的输入参数必须可哈希(作为字典key)、函数输出应该对相同输入保持不变(即纯函数)、函数的计算成本远大于字典查找成本。
fromfunctoolsimportlru_cacheimporttime@lru_cache(maxsize=128)deffibonacci(n):"""计算第n个斐波那契数"""ifn<2:returnnreturnfibonacci(n-1)+fibonacci(n-2)# 对比有无缓存deffib_no_cache(n):ifn<2:returnnreturnfib_no_cache(n-1)+fib_no_cache(n-2)start=time.time()fib_no_cache(35)print(f"无缓存:{time.time()-start:.2f}s")start=time.time()fibonacci(35)print(f"有缓存:{time.time()-start:.6f}s")# 查看缓存信息print(f"缓存命中:{fibonacci.cache_info().hits}")字典缓存模式:
classComputedCache:"""计算属性的自定义缓存"""def__init__(self):self._cache={}defget_or_compute(self,key,compute_func):ifkeynotinself._cache:self._cache[key]=compute_func(key)returnself._cache[key]cache=ComputedCache()result=cache.get_or_compute("report_2026",lambdak:expensive_db_query(k))四、本地变量优化
Python 变量查找遵循 LEGB 规则(Local → Enclosing → Global → Builtin)。每一层查找都有微小的开销——Python需要在命名空间的字典中逐层搜索。当这发生在百万次循环的内部时,累积的查找开销就不可忽视了。将频繁引用的外部变量或方法绑定为本地变量,本质上是将多次字典查找"缓存"为一次。对于math.sqrt这样的全局函数,这个优化能带来10%-30%的提升。
importtimeitdefuse_global(n):total=0foriinrange(n):total+=i# 每次循环查找 global totalreturntotaldefuse_local(n):total=0foriinrange(n):total+=i# total 是本地变量returntotal# 更典型的例子:频繁调用方法importmathdefsqrt_global(data):return[math.sqrt(x)forxindata]# 每次查找 global mathdefsqrt_local(data):my_sqrt=math.sqrt# 绑定为本地变量return[my_sqrt(x)forxindata]data=list(range(100000))print("导入 math.sqrt 到本地:",timeit.timeit(lambda:sqrt_local(data),number=100))print("使用 math.sqrt:",timeit.timeit(lambda:sqrt_global(data),number=100))五、字符串拼接优化
在循环中频繁拼接字符串是常见的性能陷阱。为什么+慢?因为Python字符串是不可变的,每次s += str(i)都会创建一个全新的字符串对象,复制所有已有内容,再追加新内容——这是O(n²)的复杂度。而"".join(parts)只做一次内存分配(预先计算出所有部分的总长度),然后一次性将各部分复制进去,这是O(n)的复杂度。在大规模拼接时,两者的性能差距可达数百倍。
# 不推荐:循环中使用 + 拼接(每次都创建新字符串)defslow_join(n):s=""foriinrange(n):s+=str(i)# O(n²)returns# 推荐:使用 joindeffast_join(n):parts=[]foriinrange(n):parts.append(str(i))return"".join(parts)# O(n)# 更简洁:使用列表推导deffastest_join(n):return"".join(str(i)foriinrange(n))六、选择合适的数据遍历方式
data=list(range(100000))# 直接迭代最快defdirect_loop():total=0forxindata:total+=x# 索引访问较慢defindex_loop():total=0foriinrange(len(data)):total+=data[i]# 使用内置函数最快(C 语言实现)defbuiltin_sum():returnsum(data)print("直接循环:",timeit.timeit(direct_loop,number=100))print("索引循环:",timeit.timeit(index_loop,number=100))print("内置函数:",timeit.timeit(builtin_sum,number=100))原则:能用内置函数(如sum、map、filter、min、max)就不要自己写循环,它们底层用 C 实现,速度远超 Python 循环。
七、使用 C 扩展和 Cython
当 Python 代码优化到极限仍不能满足性能要求时,可以考虑 C 扩展:
# 使用 ctypes 调用 C 库importctypes libc=ctypes.CDLL("msvcrt.dll")if"win"inplatformelsectypes.CDLL("libc.so.6")libc.memset.argtypes=[ctypes.c_void_p,ctypes.c_int,ctypes.c_size_t]buf=ctypes.create_string_buffer(1024)libc.memset(buf,0,1024)更实用的方式是使用Cython将 Python 代码编译为 C 扩展:
# example_cy.pyx def fibonacci_cy(int n): cdef int i cdef double a = 0.0, b = 1.0 for i in range(n): a, b = a + b, a return a编译后,计算斐波那契数的速度可提升数十倍。
八、性能分析工具
在优化之前,先要定位瓶颈:
# 使用 cProfile 进行性能分析importcProfileimportpstatsdefmain():result=sum(i*iforiinrange(1000000))returnresult profiler=cProfile.Profile()profiler.enable()main()profiler.disable()stats=pstats.Stats(profiler).sort_stats("cumulative")stats.print_stats(10)line_profiler可以逐行分析函数性能:
# pip install line_profiler# 在需要分析的函数前加 @profile 装饰器# 然后:kernprof -l -v script.py总结
Python 性能优化的核心理念是先测量、再优化、后验证。优先从数据结构选择和算法层面入手(收益最大),其次利用内置函数和缓存避免重复计算,再次通过本地变量和生成器减少不必要的开销,最后才考虑 C 扩展。记住 Donald Knuth 的名言:“过早优化是万恶之源”——在代码正确、可读的前提下,有针对性地优化真正的热点路径,才是务实的工程实践。
亮点总结
- 数据结构选择对比:列表 vs 元组的内存与速度对比、集合 vs 列表的 O(1) vs O(n) 查找差异,用 timeit 量化呈现,直观有力。
- 生成器延迟计算:生成器表达式与列表推导式的内存占用天壤之别,管道式的数据处理流(read → filter → transform)优雅又高效。
- 缓存加速技术:functools.lru_cache 装饰器实现无侵入的函数级缓存,配合缓存命中率统计,适合重复计算密集型场景。
- 编码习惯优化全景:本地变量绑定(LEGB 查找规则)、字符串拼接(join vs +)、数据遍历方式(直接迭代 vs 索引 vs 内置函数 sum)等多维度技巧。
- 性能分析工具使用:cProfile 用于宏观热点定位,line_profiler 用于逐行分析,避免盲目优化。
适用场景
- API 接口响应时间优化:对 Web 应用中的慢查询接口,通过缓存数据库查询结果、优化数据结构和减少不必要循环来降低响应延迟。
- 大数据处理脚本提速:数据清洗、ETL 管道等批量处理脚本,使用生成器避免内存溢出,结合
collections模块选用合适的数据结构。 - Python 应用性能基测与调优:在项目上线前,用 cProfile 进行全量性能分析,定位 CPU 热点,按优先级逐项优化。
扩展方向
- Python 多进程编程详解(本系列第 94 篇):当单进程优化触及天花板时,使用多进程将 CPU 密集型任务分发到多核,实现线性加速。
- Python 内存管理与垃圾回收(本系列第 95 篇):性能优化的另一面是内存优化,理解引用计数和 GC 机制有助于避免内存抖动和泄漏。
- Cython 与 C 扩展深入学习:当 Python 层面优化到极限时,将热点函数用 Cython 重写或使用 pybind11 编写 C++ 扩展,可获得显著加速。
