☰
几行代码让 Python 函数执行快 30 倍
2026/10/17 9:39:51 网站建设 项目流程

前言


先把话说在前面:不存在"几行代码让所有 Python 函数都稳定快 30 倍"这回事。"30 倍"是一个具体数字,而实际提速取决于函数在做什么、瓶颈在哪、跑在哪个 解释器上,绝不可能有一个放之四海而皆准的倍数。本文不会给出任何"提速 N 倍"的 承诺,而是讲清楚哪些做法通常更快、为什么更快,并给出一段你可以自己运行、 亲眼测出结果的基准代码。


另一个要破除的误解是"优化就是到处加小技巧"。真正的顺序应该是:先测量、找到 瓶颈、再优化。如果函数的时间花在网络请求或磁盘 I/O 上,把循环里的属性访问 抠得再细也几乎没用;反过来,一个纯计算的递归函数如果重复算同一批子问题,一个 缓存装饰器就能改变数量级。


最后提醒:本文提到的很多"更快"都属于CPython 的实现细节(比如局部变量比 全局变量查找快),换解释器、换版本甚至换成 3.13 起实验性的自由线程构建,结论 都可能不同。任何优化,都要在你自己的环境里量一遍。


一、为什么不能承诺"30 倍"


优化的收益来自"消除了多少无效工作",而不是来自"加了几行魔法"。可以分成几类:



  • 减少重复计算:把已经算过的结果缓存下来,重复部分越多,收益越大。

  • 降低查找开销:变量、属性、方法的查找有成本,把高频查找提到循环外。

  • 选对算法 / 数据结构:这一步往往才是数量级差别,但和"几行代码"无关。

  • 减少内存拷贝:避免不必要地物化大列表。


其中"几行代码"能碰到的,主要是前两类。后面两类更多是设计问题。


二、缓存重复计算:functools.lru_cache 与 cache


对纯函数(相同输入必得相同输出、无副作用)来说,把结果缓存起来是最直接的 加速手段。标准库提供了两个装饰器:



  • functools.lru_cache(maxsize=128, typed=False):带容量上限的最近最少使用


缓存,超出上限时淘汰最久未用的条目。



  • functools.cache(Python 3.9 起):等价于lru_cache(maxsize=None),


即不设上限。


# 适用于 Python 3.9+

from functools import cache

@cache
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)

print(fib(30)) # 832040,且大大减少了重复递归

lru_cache在 Python 3.8 及更早版本也能用,只是没有functools.cache这个 简写,写成@lru_cache(maxsize=None)即可。


三、把全局查找变成局部查找


在 CPython 里,函数内访问局部变量用的是快速路径(字节码LOAD_FAST), 访问全局变量要走字典查找(LOAD_GLOBAL)。所以把高频使用的全局名字在 函数开头绑定成局部名字,通常能省下一点查找开销。


# 适用于 Python 3.8+

from math import sqrt

def hypot_many(pairs):
local_sqrt = sqrt # 绑定成局部名字
return [local_sqrt(x * x + y * y) for x, y in pairs]

同理,把"每次都算一遍"的表达式提到循环外:


# 适用于 Python 3.8+

def scale(values, factor, offset):
# 把不变的表达式放到循环外
shift = factor * offset
return [v * factor + shift for v in values]

再次强调:这类改动是"通常略快",不是"一定快 30 倍",实际差别取决于循环 体有多重——循环体越轻,查找开销占比越大。


四、避免在热循环里重复做属性查找


每次obj.attr都要查一次属性。在很热的循环里,把方法或属性提到循环外能少做 很多次查找:


# 适用于 Python 3.8+

def double_all(items):
result = []
append = result.append # 绑定一次,循环里反复用
for x in items:
append(x * 2)
return result

但要清醒:如果循环体里有真正的重活(比如字符串处理、网络 I/O),这点查找 开销可以忽略。先测量,再决定值不值得牺牲可读性。


五、选对写法:能少算就少算


有些"提速"其实是沟通"哪些工作可以不做":



  • 用生成器表达式代替大列表,避免一次性物化,降低内存峰值(但注意生成器


只能遍历一次)。



  • 用str.join(...)拼接大量字符串,而不是在循环里反复s += piece。

  • 用字典 / 集合做成员判断,而不是在列表里线性扫描。

  • 用内置函数(sum、min、max、any、all)让它走 C 实现,而不是手写


Python 循环。


# 适用于 Python 3.8+

words = ["alpha", "beta", "gamma"]

# 推荐:join 一次拼好
line = ",".join(words)

# 推荐:用集合做成员判断
allowed = {"yes", "no", "maybe"}
print("yes" in allowed) # True,通常比在列表里扫更快

六、自己动手量:timeit


timeit.timeit(stmt='pass', setup='pass', timer=..., number=1000000, globals=None)和timeit.repeat(stmt='pass', setup='pass', timer=..., repeat=5, number=1000000, globals=None)是做微基准的标准工具(timer参数不传就用平台默认的最佳计时器)。下面这段 可以直接复制运行:


# 适用于 Python 3.8+

import timeit

setup_plain = """
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
"""

setup_cached = """
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
"""

plain = min(timeit.repeat("fib(25)", setup=setup_plain, repeat=5, number=1))
cached = min(timeit.repeat("fib(25)", setup=setup_cached, repeat=5, number=1))

print(f"普通递归: {plain:.6f} 秒")
print(f"加缓存 : {cached:.6f} 秒")

说明:repeat返回每轮的耗时列表,取min是为了避开偶尔被系统调度干扰的 那几次。注意缓存版本在第一次跑完之后缓存就热了,所以后续几轮量的是"命中 缓存"的速度,这正是我们想展示的差异。


命令行里也能量:


python -m timeit -s "from math import sqrt" "sqrt(2)"

七、常见优化手段对照




手段收益来源适用前提



functools.cache/lru_cache去掉重复计算函数是纯函数、参数可哈希

局部绑定全局名少走一次字典查找名字在热循环里高频使用

循环外提表达式不重复计算不变量表达式确实与循环变量无关

局部绑定方法少做属性查找循环很热、循环体很轻

用内置函数走 C 实现有对应的内置函数

选对数据结构改变复杂度常出现在成员判断、查找环节



常见坑点



  1. 给不可哈希的参数加缓存


``python from functools import lru_cache # ❌ 列表不可哈希,调用时抛 TypeError @lru_cache(maxsize=None) def f(xs): return sum(xs) # ✅ 传不可变类型 @lru_cache(maxsize=None) def g(xs): return sum(xs) g((1, 2, 3))``



  1. 给有副作用的函数加缓存


``python from functools import lru_cache # ❌ 第二次调用不会真的写文件,副作用被跳过 @lru_cache(maxsize=None) def save(path, data): ... # ✅ 缓存只用在纯函数上``



  1. 指望微优化解决 I/O 瓶颈


``python # ❌ 时间花在网络等待上,改循环写法没用 for url in urls: data = fetch(url) # 真正的瓶颈在这里 # ✅ 先定位瓶颈,再决定优化手段``



  1. 过早优化


``python # ❌ 还没测就先把代码改得又长又难读 # ✅ 先测量,只优化热点``



  1. timeit 用法错误导致结果不可信


``python # ❌ 把 setup 里的准备工作算进了计时 # ✅ 准备工作放 setup,被计时的只放 stmt`另外,单次运行波动大,要用repeat多跑几轮再取min`。



  1. 误用 lru_cache 的默认容量


``python from functools import lru_cache # ❌ 参数取值很多时,默认 maxsize=128 会频繁淘汰 @lru_cache def f(n): ... # ✅ 要么显式设容量,要么明确要无上限 @lru_cache(maxsize=None) def g(n): ...``



  1. 把"通常更快"当成"必然更快"


``python # ❌ 不测就断言某写法一定快 # ✅ 在自己的数据、自己的版本上量一遍,再下结论``


总结




思路代表做法



少算functools.cache/lru_cache

少查局部绑定、循环外提表达式

少拷生成器、join、内置函数

选对字典 / 集合代替线性扫描

先量timeit测,再决定改不改



"几行代码快 30 倍"更像是一句吸引眼球的宣传语,而不是可以复现的承诺。真正 可靠的做法是:用timeit找到瓶颈,针对瓶颈选对手段,然后重新测一遍确认 有效。缓存和局部绑定能帮上忙,但它们降的是常数与重复开销,改变不了算法本身 的复杂度——那才是数量级差别真正来源的地方。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询