别的语言里函数是"命令",Python 里函数是"公民"——可以存进变量、塞进列表、当参数传来传去。这种把函数当数据用的风格叫函数式编程,它能让循环代码缩成一行。本文从 lambda 讲起,实战 map/filter/reduce,最后用 functools 的偏函数和缓存解决真实问题。

1. 函数是一等公民

"一等公民"的意思是:函数能像整数、字符串一样被赋值、传参、返回。先感受一下这种"函数当参数"的威力:

def shout(text):
    return text.upper()

def whisper(text):
    return text.lower()

def speak(func, text):
    return func(text)

print(speak(shout, "hello"))    # HELLO
print(speak(whisper, "HELLO"))  # hello

speak 自己不做任何处理,它接收一个函数和一个文本,把处理工作"外包"给传入的函数。这就是高阶函数:接收函数作为参数、或返回函数的函数。调用时注意别写错:speak(shout, ...) 传的是函数对象本身,写 speak(shout(), ...) 就变成传它的返回值了。

函数既然能当参数,自然也能当返回值——返回函数的函数叫工厂函数,它是闭包的雏形。比如 make_multiplier(n) 返回一个"把输入乘以 n"的函数,每次调用工厂都能得到一个新函数,各自记住自己的 n。这种"函数是数据"的观念一旦建立,你会开始用组合代替复制:与其写十个几乎一样的函数,不如写一个工厂批量生产。这也是后面 partial 的思想源头。

2. lambda:一行函数

上面的 shout/whisper 为了一次使用专门写两个函数,有点浪费。lambda 表达式允许用一行定义匿名函数,专治这种场景:

double = lambda x: x * 2
print(double(21))          # 42

add = lambda a, b: a + b
print(add(3, 5))           # 8

# 等价于:
def double_def(x):
    return x * 2
print(double_def(21))

lambda 的语法是 lambda 参数: 表达式,表达式的结果就是返回值,不需要也不能写 return。它和 def 完全等价,只是只能写一个表达式——如果需要多行逻辑、循环或赋值,老老实实用 def。lambda 的最大价值是配合高阶函数,在调用点就地定义逻辑,不用跳来跳去找函数定义。

lambda 有一个著名陷阱:闭包捕获的是变量本身,不是捕获时刻的值。经典翻车现场是在循环里批量生成 lambda:

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])   # [2, 2, 2],不是 [0, 1, 2]!

三个函数打印的都是 2,因为循环结束时 i 停在 2,而 lambda 执行时才去读 i。解决办法是用默认参数把值"钉"住:lambda i=i: i。这个坑遇到一次就记住了——lambda 虽小,行为却很刁钻。

3. map、filter 与 reduce

这是函数式编程的"三件套",分别对应"批量变换""按条件筛选""累积归约"。注意 reduce 不在内置函数里,要从 functools 导入:

from functools import reduce

nums = [1, 2, 3, 4, 5]

# map:把函数作用到每个元素上
doubled = list(map(lambda x: x * 2, nums))
print(doubled)                    # [2, 4, 6, 8, 10]

# filter:保留使函数返回真的元素
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)                      # [2, 4]

# reduce:从左到右累积,把序列归约成一个值
total = reduce(lambda a, b: a + b, nums, 0)
print(total)                      # 15

三个坑要记住:第一,map 和 filter 返回的是迭代器,不是列表,想打印或索引必须用 list() 包一层;第二,map 的回调函数返回什么,结果就是什么(x * 2 返回数字);第三,reduce 的第三个参数 0 是初始值——求和不给初始值也能跑,但给了更稳,空列表时不会报错。

map 还有一个隐藏技能:接收多个序列,回调函数按位置从每个序列各取一个元素。比如 map(lambda a, b: a + b, [1, 2], [10, 20]) 得到 [11, 22],相当于并行遍历两个列表。这其实是 zip 的 map 版,但日常更推荐直接写 [a + b for a, b in zip(xs, ys)],意图更直白。另外注意 map 是惰性的:不迭代就不会执行回调,所以 map(f, huge_list) 本身不耗内存,只有 list() 那一刻才真正计算——处理大数据流时这是个优点。

4. 更 Pythonic 的替代:推导式与 sorted

说实话,map + lambda 在 Python 里不是最优解,社区更推荐列表推导式——同样的逻辑,可读性高一个档次。但函数式思想在 sorted 的 key 参数上依然大放异彩:

nums = [3, 1, 4, 1, 5, 9, 2, 6]

# 推导式替代 map
print([x * 2 for x in nums])

# 推导式替代 filter
print([x for x in nums if x % 2 == 0])

# sorted 的 key 接收函数:按长度、按末字母排序
words = ["banana", "apple", "cherry", "date"]
print(sorted(words, key=len))            # 按长度升序
print(sorted(words, key=lambda w: w[-1]))  # 按最后一个字母排序

推导式写法 [表达式 for x in 序列 if 条件],把"变换 + 筛选 + 建列表"一步到位。而 sorted(key=...) 是函数式思想的最佳实践:排序逻辑和数据结构解耦,想按什么排就传什么函数。经验法则:处理列表优先写推导式,处理"排序/最大最小/分组"这类任务才用 key 函数。

key 函数的实战价值在"按对象属性排序"时彻底释放:比如一份学生成绩字典 [{"name": "小明", "score": 92}, ...],按分数排序只需 sorted(students, key=lambda s: s["score"], reverse=True)。配合 operator 模块还能更优雅:from operator import itemgetter 后写 sorted(students, key=itemgetter("score"))——用现成的取值函数代替手写 lambda,既快又不易错。max、min 同样支持 key,找"分数最高的人"这类需求一行搞定。

5. functools.partial:固定参数

写代码时常遇到这种情况:同一个函数反复调用,每次都要重复传几个相同的参数。partial 可以把这些参数"预填"进去,生成一个新函数:

from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube = partial(power, exp=3)

print(square(5))    # 25
print(cube(5))      # 125
print(power(5, 2))  # 原函数不受影响

partial(power, exp=2) 返回一个"exp 已经被钉死为 2"的新函数,调用时只需传 base。实战场景很多:比如给日志函数预填前缀、给请求函数预填 timeout=5。注意用关键字参数固定更安全——如果按位置固定 exp,万一以后 power 的参数顺序变了,partial 出来的函数就悄悄错了。

GUI 编程里 partial 尤其好用:按钮的 command 回调需要传参,直接绑定会立刻执行,partial 正好把"参数"和"延迟执行"打包在一起。另一个典型场景是批量注册处理器:把十几个接口的处理函数各自 partial 上不同的路由参数,再统一注册,代码结构立刻清爽。记住 partial 的本质:它不改原函数,只是造了一个"预填了部分参数"的新壳,原函数照常可用——这也是它比改全局变量优雅得多的地方。

6. functools.lru_cache:自动记忆化

最后这个大招解决的是性能问题。经典的斐波那契递归,直接写是指数级复杂度,fib(40) 就要跑几秒。lru_cache 装饰器会自动缓存函数的调用结果,同样的参数第二次调用直接返回缓存:

from functools import lru_cache

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

print(fib(50))   # 12586269025,瞬间出结果
print(fib.cache_info())  # CacheInfo(hits=48, misses=51, ...)

不加 @lru_cache 的 fib(50) 要跑到天荒地老,加了之后毫秒级返回——因为每个 fib(k) 只真正计算一次,其余都是缓存命中。maxsize=None 表示缓存不设上限(纯函数、参数可哈希时安全);生产环境给个上限,比如 maxsize=128,防止内存无限增长。铁律:只有"相同输入必然相同输出"的纯函数才能用缓存,依赖全局状态或时间的函数用了会得到过期结果。

什么时候该想到 lru_cache?凡是"递归里有大量重复子问题"的算法——斐波那契、爬楼梯、背包问题的朴素递归版——加上它立刻从指数级复杂度降为多项式级,代码一行都不用改。工程上它还能当"轻量级请求缓存"用:同一个耗时的查询函数,短时间内重复调用直接命中缓存。想清空缓存(比如数据更新了),调用 fib.cache_clear();用 cache_info() 看命中率,如果命中率极低,说明缓存设计有问题,不如不加。

7. 总结与练习

这一路下来,你手里多了四件工具:lambda 写匿名函数、map/filter/reduce 做批量处理(但列表推导式往往更优)、partial 固定参数生成新函数、lru_cache 白捡性能。核心思想只有一个——函数也是数据,可以传来传去、组合叠加。

💡 函数式风格追求"声明你要什么,而不是怎么循环"。但别为了函数式而函数式:一行推导式能说清的事,就别叠三层 lambda——可读性永远是第一位的。