函数式编程
前面学了函数、lambda 和装饰器,这篇来把它们串起来,聊聊 Python 中的函数式编程思想。简单来说,函数式编程就是把函数本身也当成一种「数据」来用:既能传进别的函数,也能从函数里返回,还能用来组装新函数。
1. 什么是函数式编程
我们平时写函数,更多是把代码打包复用;而函数式编程(Functional Programming)更进一步,它把「计算」抽象成函数之间的组合。一个理想化的函数式函数有两个特点:
- 同样的输入一定得到同样的输出,不产生副作用。
- 函数本身可以像变量一样被传递、被返回、被组合。
Python 不是纯函数式语言,它允许变量和副作用,所以只提供「部分」函数式支持。但这并不妨碍我们借鉴它的思想,让代码更灵活、更简洁。
# 函数的参数和返回值可以是数字、字符串、列表……
# 在函数式编程里,它们也可以是函数本身
def hello():
return 'hello'
# hello 是函数名,hello() 是函数调用
print(hello) # <function hello at 0x...>
print(hello()) # hello函数名和函数调用是两回事:
hello表示函数本身,hello()表示执行函数拿到结果。
2. 高阶函数
如果一个函数满足以下两个条件之一,就可以称为高阶函数(Higher-order function):
- 接收另一个函数作为参数。
- 把函数作为返回值。
Python 中很多内置函数都是高阶函数,比如 map()、filter()、sorted()、reduce() 等。其中 map()、filter()、reduce() 已经在《lambda》中配合匿名函数展开过了,这里重点看看高阶函数背后的通用思想,以及 sorted() 的 key 用法。
2.1 函数可以赋值给变量
函数本身也是一种对象,可以被变量引用,也能通过变量来调用。
# 把内置函数 abs 赋值给变量 my_abs
my_abs = abs
# 输出: 10
print(my_abs(-10))
# 输出: <built-in function abs>
print(my_abs)函数名其实就是指向函数对象的变量。虽然平时不建议覆盖内置函数名(比如 abs = 10),但理解这一点对后面写高阶函数很重要。
# 不推荐,但可以帮助理解函数名即变量
# abs = 10
# abs(-10) # 会报错:TypeError: 'int' object is not callable2.2 函数作为参数
既然函数能赋值给变量,那它也能作为参数传给另一个函数。
def add_with_func(x, y, f):
"""把 x 和 y 先经过 f 处理,再相加"""
return f(x) + f(y)
# 传入 abs 函数,结果是 abs(-5) + abs(6) = 11
result = add_with_func(-5, 6, abs)
# 输出: 11
print(result)这种写法的好处是:把「处理逻辑」和「组合逻辑」拆开。add_with_func 负责组合,f 负责具体处理,想换什么处理就换什么函数。
# 传入自定义函数,求平方和
def square(n):
return n * n
# 输出: 25 + 36 = 61
print(add_with_func(5, 6, square))2.3 函数作为返回值
函数不仅可以返回数字、字符串、列表,也能返回另一个函数。这个会留到下一节「返回函数与闭包」详细讲,这里先知道高阶函数有两类:
- 接收函数做参数。
- 返回函数做结果。
2.4 sorted 的 key 参数
sorted() 本身是高阶函数,它接收一个可选的 key 参数,要求传入一个函数,用来告诉 Python「按什么排序」。
names = ['Tom', 'Jerry', 'Spike', 'Tyke']
# 按字符串长度排序
result = sorted(names, key=len)
# 输出: ['Tom', 'Tyke', 'Jerry', 'Spike']
print(result)len 就是一个函数,传给 key 后,sorted() 会先用 len 把每个元素「映射」成一个排序依据,再对这些依据排序。
students = [
{'name': 'Alice', 'score': 88},
{'name': 'Bob', 'score': 75},
{'name': 'Cara', 'score': 92},
]
# 按 score 从小到大排序
result = sorted(students, key=lambda s: s['score'])
# 输出: [{'name': 'Bob', 'score': 75}, {'name': 'Alice', 'score': 88}, {'name': 'Cara', 'score': 92}]
print(result)
key只决定排序依据,不会修改原始元素。如果想倒序,再加reverse=True即可。
# 按分数从高到低排序
result = sorted(students, key=lambda s: s['score'], reverse=True)
# 输出: [{'name': 'Cara', 'score': 92}, {'name': 'Alice', 'score': 88}, {'name': 'Bob', 'score': 75}]
print(result)3. 返回函数与闭包
高阶函数的另一个方向是「返回函数」。也就是说,调用一个函数时,得到的不是最终结果,而是一个新的函数,之后需要时再调用这个新函数。
3.1 函数里定义函数
Python 允许在一个函数内部再定义函数,这个内部函数可以引用外部函数的参数和局部变量。
def lazy_sum(*args):
"""不立刻求和,而是返回一个求和函数"""
def calc():
total = 0
for n in args:
total += n
return total
return calc
f = lazy_sum(1, 3, 5, 7, 9)
# 输出: <function lazy_sum.<locals>.calc at 0x...>
print(f)
# 输出: 25
print(f())这里 lazy_sum 返回的是 calc 函数本身,而不是求和结果。只有调用 f() 时,才真正执行计算。这种「先构造、后执行」的模式叫延迟求值。
3.2 什么是闭包
上面的例子中,calc 函数引用了外部函数 lazy_sum 的参数 args。即使 lazy_sum 已经执行完毕,args 也依然被返回的 calc 函数保存着。这种「内部函数引用外部函数的变量,并且外部函数已经返回」的结构,就叫闭包(Closure)。
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
# 创建两个闭包函数
double = make_multiplier(2)
triple = make_multiplier(3)
# 输出: 10
print(double(5))
# 输出: 15
print(triple(5))double 和 triple 虽然都来自同一个 make_multiplier,但各自保存了不同的 n 值,互不干扰。
3.3 每次返回都是新函数
即使传入相同的参数,每次调用返回函数时也会得到一个新的函数对象。
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
# 输出: False
print(f1 == f2)
# 输出: 25
print(f1())
# 输出: 25
print(f2())3.4 闭包里的常见陷阱:循环变量
闭包最容易踩的坑,就是返回的函数引用了会变化的变量,比如循环变量。
def make_functions():
fs = []
for i in range(1, 4):
def f():
return i * i
fs.append(f)
return fs
f1, f2, f3 = make_functions()
# 你以为结果依次是 1, 4, 9?
# 输出: 9
print(f1())
# 输出: 9
print(f2())
# 输出: 9
print(f3())三个函数都引用的是同一个变量 i。等到它们真正被调用时,循环已经结束,i 的值变成了 3,所以结果全是 9。
返回闭包时,尽量不要让它引用循环变量,或者后续会变化的变量。
如果确实需要引用循环变量,可以用一个「中间函数」把当前值固定下来。
def make_functions_fixed():
def make_f(j):
def f():
return j * j
return f
fs = []
for i in range(1, 4):
fs.append(make_f(i))
return fs
f1, f2, f3 = make_functions_fixed()
# 输出: 1
print(f1())
# 输出: 4
print(f2())
# 输出: 9
print(f3())make_f(i) 立刻执行,把 i 的当前值传给了 j,闭包内部引用的是不会再变的 j。
3.5 修改外层变量:nonlocal
闭包内如果只是读取外层变量,Python 会自动去找外层作用域。但如果想给外层变量赋值,解释器会认为这个变量是当前函数的局部变量,从而报错。
def make_counter():
count = 0
def counter():
# 这里直接写 count += 1 会报错
count = count + 1
return count
return counter
# UnboundLocalError: local variable 'count' referenced before assignment
# c = make_counter()
# c()要告诉 Python「这个变量不是局部变量,而是外层函数的变量」,需要用 nonlocal 声明。
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
# 输出: 1
print(c())
# 输出: 2
print(c())
# 输出: 3
print(c())
nonlocal用于声明变量来自外层函数,但不是全局变量;如果是全局变量,则用global。
4. 偏函数
有些函数参数很多,但大部分时候我们只会用其中一种固定组合。比如 int() 函数默认按十进制转换字符串,但如果我们经常要转二进制,每次都写 int(x, base=2) 就很麻烦。
Python 的 functools.partial 可以帮我们快速生成一个「简化版」的新函数:把原函数的一部分参数固定住,调用时只需要传剩下的参数。
4.1 用 partial 固定关键字参数
import functools
# 创建 int2,默认 base=2
int2 = functools.partial(int, base=2)
# 输出: 64
print(int2('1000000'))
# 输出: 85
print(int2('1010101'))int2 本质上还是 int,只是默认把 base 参数固定成了 2。
# 如果临时想按十进制转换,也可以覆盖
# 输出: 1000000
print(int2('1000000', base=10))4.2 用 partial 固定位置参数
partial 也支持固定位置参数,但要注意它会把固定值放到参数列表的左边。
import functools
# 固定 max 的第一个参数为 10
max_with_10 = functools.partial(max, 10)
# 等价于 max(10, 5, 6, 7)
# 输出: 10
print(max_with_10(5, 6, 7))所以 max_with_10(5, 6, 7) 实际上相当于 max(10, 5, 6, 7),结果自然是 10。
4.3 手动实现 vs 偏函数
如果不使用 partial,我们可以手动写一个包装函数:
def int2_manual(x):
return int(x, base=2)
# 输出: 64
print(int2_manual('1000000'))这种方式没问题,但 partial 更通用:它不需要你写新的函数体,只需要「冻结」原函数的部分参数。
# 固定 print 的 sep 参数
print_star = functools.partial(print, sep=' * ')
# 输出: a * b * c
print_star('a', 'b', 'c')偏函数不是新语法,它只是
functools模块提供的一个小工具。适合用来把通用函数包装成专用函数。
5. lambda 与装饰器:函数式编程的延伸
前面三篇把函数式编程的几个核心概念拆开了:
lambda提供了一种轻量级的函数写法,适合作为高阶函数的参数。- 装饰器本质上也是高阶函数:接收一个函数,返回一个包装后的新函数。
- 返回函数和闭包则让函数可以「记住」外部状态,按需执行。
这几个概念相互配合,是 Python 函数式编程里非常实用的部分。至于 map()、filter()、reduce() 这些内置高阶函数,已经在《lambda》中详细展开,这里就不再重复了。
相关信息
- 函数式编程的核心思想是把函数也当成数据来传递和组合。
- 高阶函数分两种:接收函数作为参数,或返回函数作为结果。
sorted()的key参数是高阶函数的典型用法,可以灵活定义排序规则。- 返回函数时形成闭包,内部函数会记住外部函数的变量。
- 返回闭包要避免引用循环变量或后续会变化的变量,必要时用中间函数固定值。
- 修改外层变量需要用
nonlocal声明。 functools.partial可以把函数的部分参数固定住,生成一个更简洁的调用版本。