前言
这个标题把好几组概念并排列在一起,其实它们处在不同层次上,需要先厘清:「函数式编程」是一种编程风格,「字符串和元组」是两种具体的数据类型,「函数分类」和「高阶函数」则是围绕函数这个对象展开的话题。把它们塞进一句标题,是关键词堆出来的,读起来像并列关系,实际不是。本文按真实的概念层次来组织:先讲函数在 Python 里是什么、怎么分类,再讲高阶函数,最后讲为什么字符串和元组天然适合函数式风格。
先说最关键的一点:在 Python 里,函数是「一等对象」(first-class object),和整数、字符串、列表没有本质区别。函数可以被赋值给变量、作为参数传给别的函数、作为返回值返回、放进列表和字典。这不是什么高级技巧,而是理解 Python 一切函数式写法(map、filter、sorted(key=...)、装饰器)的地基。
常见的误解是把「函数式编程」等同于「用map和filter」。其实函数式风格的核心约束是避免副作用、不做原地修改、用纯函数把输入映射成输出。Python 不是纯函数式语言,它允许list.append、允许赋值,所以这里的「函数式」更多是一种倾向:能用不可变对象就不用可变的,能用表达式表达就不写循环。
一、函数是什么:一等对象的四个证据
# 适用于 Python 3.8+
def double(x):
return x * 2
f = double # 1. 赋值给变量
print(f(3)) # 6
funcs = {"double": double, "upper": str.upper} # 2. 放进容器
print(funcs["double"](3)) # 6
print(funcs["upper"]("ab")) # AB
def apply_twice(func, value): # 3. 作为参数
return func(func(value))
def make_adder(n): # 4. 作为返回值(闭包)
def adder(x):
return x + n
return adder
print(apply_twice(double, 3)) # 12
add5 = make_adder(5)
print(add5(10)) # 15make_adder返回的adder记住了外层的n,这就是闭包(closure)——函数式风格的常见构件。
二、函数的分类
按「怎么产生」和「用在哪」,Python 的函数可以分成几类:
| 类别 | 例子 | 说明 |
|---|
| 内置函数(built-in function) | len、print、abs | 由解释器提供,C 实现 |
| 自定义函数 | def double(...) | 用def定义 |
| 匿名函数(lambda) | lambda x: x * 2 | 只能是单个表达式 |
| 方法(method) | "a".upper、lst.append | 绑定到对象,带self |
| 高阶函数(higher-order function) | sorted、map、filter | 接收函数作为参数或返回函数 |
| 可调用对象(callable) | 实现了__call__的实例 | 像函数一样obj() |
lambda与def的关键差别:lambda 的函数体只能是一个表达式,不能写if/else语句、不能写多行、不能有文档字符串。它适合「就地传一个小函数」,不适合承载复杂逻辑。
# 适用于 Python 3.8+
print((lambda a, b: a + b)(2, 3)) # 5,立即调用
words = ["bb", "a", "ccc"]
print(sorted(words, key=lambda w: len(w))) # ['a', 'bb', 'ccc']三、高阶函数:把函数当参数
高阶函数的定义是「接收函数作为参数,或返回一个函数」。日常最常用的其实不是map,而是sorted的key:
# 适用于 Python 3.8+
records = [("alice", 30), ("bob", 25), ("carol", 28)]
print(sorted(records, key=lambda r: r[1])) # 按年龄升序
print(sorted(records, key=lambda r: r[1], reverse=True)) # 降序# 适用于 Python 3.8+
nums = [1, 2, 3, 4, 5]
squares = map(lambda x: x * x, nums) # 返回迭代器,惰性
evens = filter(lambda x: x % 2 == 0, nums) # 同样返回迭代器
print(list(squares)) # [1, 4, 9, 16, 25]
print(list(evens)) # [2, 4]重要版本差异:在 Python 2 里map和filter返回列表,在 Python 3 里它们返回迭代器。这意味着两件事:结果只遍历一次就空了;想拿到列表必须显式list(...)。这也是 Python 2 代码迁移时常见的隐性 bug。
functools.reduce把二元函数累积地作用到序列上:
# 适用于 Python 3.8+
from functools import reduce
print(reduce(lambda a, b: a * b, [1, 2, 3, 4])) # 24
print(reduce(lambda a, b: a + b, [1, 2, 3], 100)) # 106,100 是初始值注意reduce在 Python 3 里不再是内置函数,必须从functools导入(Python 2 时代它是内置的)。
functools.partial用来「预先固定一部分参数」,生成一个新的可调用对象:
# 适用于 Python 3.8+
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
print(square(5)) # 25四、字符串和元组:为什么它们契合函数式风格
函数式风格偏好不可变(immutable)的数据,因为不可变对象不会被「别人偷偷改掉」,写出来的函数没有副作用。Python 的两个内建不可变序列正好是字符串(str)和元组(tuple)。
# 适用于 Python 3.8+
s = "hello"
t = (1, 2, 3)
# 所有「修改」操作其实都返回新对象,原对象不变
s2 = s.upper()
t2 = t + (4,)
print(s, "|", s2) # hello | HELLO
print(t, "|", t2) # (1, 2, 3) | (1, 2, 3, 4)
# s[0] = "H" # TypeError: 'str' object does not support item assignment
# t[0] = 9 # TypeError: 'tuple' object does not support item assignment不可变带来两个直接好处。第一,可以安全地当字典的键(前提是元组里的每个元素本身可哈希),str和tuple都能做键,而list不能:
# 适用于 Python 3.8+
grid = {("a", 1): "x"}
print(grid[("a", 1)]) # x第二,适合做函数式变换的载体:把元组当成「不可变的记录」,用map生成一串新元组,而不用担心数据被下游改掉。
元组还支持解包(unpacking),这让它在小规模数据的函数式处理里非常好用:
# 适用于 Python 3.8+
point = (3, 4)
x, y = point
print(x + y) # 7
pairs = [("a", 1), ("b", 2)]
print([f"{k}={v}" for k, v in pairs]) # ['a=1', 'b=2']需要「带名字的元组」时用collections.namedtuple,它既有元组的不可变和低开销,又能按字段名访问:
# 适用于 Python 3.8+
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p._replace(y=10)) # Point(x=3, y=10),返回新对象五、实战:用函数式风格做数据流水线
把上面的零件组合起来:给一批记录打分、排序、取名,全程不修改原数据。
# 适用于 Python 3.8+
from collections import namedtuple
Student = namedtuple("Student", ["name", "score"])
raw = [("alice", 88), ("bob", 72), ("carol", 95), ("dave", 72)]
# 1. 转成不可变记录
students = [Student(*item) for item in raw]
# 2. 过滤 + 排序(都返回新对象,不改 students)
passed = list(filter(lambda s: s.score >= 80, students))
ranked = sorted(passed, key=lambda s: s.score, reverse=True)
# 3. 生成展示名(str 操作同样返回新字符串)
labels = list(map(lambda s: f"{s.name.title()}({s.score})", ranked))
print(labels) # ['Carol(95)', 'Alice(88)']
print(students[0]) # Student(name='alice', score=88) —— 原数据未变整段代码没有一个for循环型的原地修改,每个环节都输入旧数据、输出新数据——这就是函数式风格在 Python 里的实用形态。
常见坑点
- 以为 map / filter 返回列表
❌n = map(int, ["1", "2"]); print(len(n))→TypeError: object of type 'map' has no len()
✅ Python 3 里它们是惰性迭代器:需要列表用list(map(...));且只能遍历一次
- 把 reduce 当内置函数
❌reduce(lambda a, b: a + b, [1, 2])→NameError: name 'reduce' is not defined
✅ Python 3 里要从functools导入:from functools import reduce
- 循环里用 lambda 造成延迟绑定
❌funcs = [lambda: i for i in range(3)],调用时三个都返回2
✅ 用默认参数固定当前值:[lambda i=i: i for i in range(3)]
- 用 sort() 的返回值
❌result = lst.sort()得到None,因为list.sort()原地排序并返回None
✅ 要新列表用sorted(lst)(返回新列表);要原地排用lst.sort()且不要接返回值
- 误以为字符串 / 元组的方法会改自己
❌s.strip()之后直接用s,发现没变;t.append(1)直接AttributeError
✅ 接住返回值:s = s.strip();元组本就没有 append,要扩展得写t = t + (1,)
- 用可变默认参数配函数式风格
❌def collect(x, acc=[]): acc.append(x); return acc,多次调用之间互相污染
✅ 默认值用不可变:def collect(x, acc=None): acc = [] if acc is None else acc; ...
- 误用 list 当字典键
❌d[[1, 2]] = "x"→TypeError: unhashable type: 'list'
✅ 改用元组d[(1, 2)] = "x"(元素必须都可哈希)
- 对迭代器重复遍历
❌it = map(str, nums)先用list(it)打印,再sum(it)得到 0
✅ 迭代器一次性消费,需要复用就转成列表存下来
总结
| 概念 | 要点 |
|---|
| 一等对象 | 函数可赋值、传参、返回、入容器 |
| lambda | 只能写单个表达式,适合就地传小函数 |
| 高阶函数 | 接收函数或返回函数,如sorted(key=...)、map、filter |
| map / filter | Python 3 返回惰性迭代器,不是列表 |
| reduce | 在functools里,不是内置 |
| 字符串 | 不可变序列,所有方法返回新串 |
| 元组 | 不可变序列,可哈希、可解包、适合做记录 |
| 函数式风格 | 无副作用、不原地改、纯函数映射 |
把这篇的线索收成一句:函数在 Python 里是普通对象,所以能当参数和返回值;而不可变的字符串和元组,正好提供了函数式变换最省心的载体。map、filter只是工具,真正要建立的习惯是「先想这一步会不会改到现有数据」,习惯用返回新对象的方式表达变换,代码的自然副作用就会少很多。