RSS
菜单
全部文章快讯开发科技深度热点

推导式与函数式工具(Python 从精通到入门 · 21)

内容摘要

一行写完一个循环的爽感,和"一行写完导致没人看懂"的惨剧,中间只隔一条可读性。这篇教你把推导式用在对的地方。

一行写完一个循环的爽感,和"一行写完导致没人看懂"的惨剧,中间只隔一条可读性。这篇教你把推导式用在对的地方。

你将学到

  • 列表、字典、集合推导式的写法,以及条件过滤与嵌套
  • 生成器表达式省内存的原理,什么时候该用它
  • lambda、map/filter/reduce,以及它们和推导式的取舍
  • sorted(key=...)、any/all/zip/enumerate 的惯用法
  • functools.partial、itertools 的常用函数,并会做命令式 ↔ 推导式改写

前置知识

上一篇《异常处理》讲的是"出错怎么办"。这一篇讲怎么把一批数据变得又短又快地处理掉。假定你熟悉 for 循环和基本容器。

列表推导式

从一个可迭代对象里,按规则生成新列表。

# 命令式写法
squares = []
for x in range(5):
    squares.append(x * x)
print(squares)                 # 输出: [0, 1, 4, 9, 16]

# 推导式写法:一句话
squares = [x * x for x in range(5)]
print(squares)                 # 输出: [0, 1, 4, 9, 16]

结构是 [表达式 for 变量 in 可迭代对象]。for 里的变量只在推导式内部有效,不会泄漏到外面。

条件过滤与嵌套

在末尾加 if 就是筛选,满足才保留。

evens = [x for x in range(10) if x % 2 == 0]
print(evens)                   # 输出: [0, 2, 4, 6, 8]

if 放在前面则变成三元表达式(每个元素都要有值):

labels = ["偶" if x % 2 == 0 else "奇" for x in range(4)]
print(labels)                  # 输出: ['偶', '奇', '偶', '奇']

多重 for 可以展平嵌套结构,顺序和写普通循环一致:

matrix = [[1, 2], [3, 4]]
flat = [n for row in matrix for n in row]
print(flat)                    # 输出: [1, 2, 3, 4]

记忆窍门:for 从左到右就是外层到内层,if 只作用于紧挨它左边的那层。

字典与集合推导式

把方括号换成花括号,写法对称。

# 字典推导式:{键: 值 for ...}
sq = {x: x * x for x in range(4)}
print(sq)                      # 输出: {0: 0, 1: 1, 2: 4, 3: 9}

# 常用场景:反转一个字典的键值
prices = {"apple": 3, "banana": 2}
rev = {v: k for k, v in prices.items()}
print(rev)                     # 输出: {3: 'apple', 2: 'banana'}

# 集合推导式:自动去重
lens = {len(w) for w in ["a", "bb", "cc", "ddd"]}
print(lens)                    # 输出: {1, 2, 3}

生成器表达式与内存

把方括号换成圆括号,得到的是生成器表达式:它不会立刻算出所有结果,而是你要一个才算一个。

gen = (x * x for x in range(1_000_000))   # 瞬间返回,不占大内存
print(next(gen))               # 输出: 0
print(next(gen))               # 输出: 1

# 作为函数唯一参数时,可省掉外层圆括号
total = sum(x * x for x in range(1_000_000))
print(total)                   # 输出: 333332833333500000

对比:[x * x for x in range(1_000_000)] 会立刻造出含一百万个元素的列表,占几十 MB;生成器只占常数级内存。所以:

  • 要反复遍历、要下标、要长度 → 用列表。
  • 只遍历一次、数据量大、可以流式处理 → 用生成器表达式。

什么时候别用推导式

推导式是工具,不是信仰。以下情况请老老实实写循环:

# ❌ 三层嵌套加多个条件,别人得用纸笔才能读懂
result = [x for x in data if x > 0 for y in range(x) if y < 5 if x * y % 3 == 0]
# ✅ 拆开,清晰得多
result = []
for x in data:
    if x <= 0:
        continue
    for y in range(x):
        if y < 5 and (x * y) % 3 == 0:
            result.append(x)

还有两种明确不该用推导式的情况:

  • 带副作用:循环里要 print、写文件、改外部变量,就老老实实写 for。
  • 逻辑跨多行难放:表达式塞不进一行,或者需要 try/except,用循环更合适。

一句话原则:推导式适合"把一堆数据映射/筛选成另一堆数据",不适合"做一串动作"。

map / filter / reduce 与 lambda

lambda 是匿名小函数,只能写一个表达式。

double = lambda x: x * 2
print(double(3))               # 输出: 6

map / filter 是老牌函数式工具,但在 Python 里通常推导式更清晰。

nums = [1, 2, 3, 4]

# map:对每个元素做变换
print(list(map(lambda x: x * 2, nums)))       # 输出: [2, 4, 6, 8]
print([x * 2 for x in nums])                  # ✅ 更 Pythonic,等价

# filter:筛选
print(list(filter(lambda x: x % 2 == 0, nums)))   # 输出: [2, 4]
print([x for x in nums if x % 2 == 0])            # ✅ 等价

# reduce:把序列折叠成一个值(需要导入)
from functools import reduce
print(reduce(lambda a, b: a + b, nums))       # 输出: 10

那 map/filter 什么时候用?当已有现成函数、又不值得写推导式时:

names = [" alice ", "bob "]
print(list(map(str.strip, names)))            # 输出: ['alice', 'bob']

reduce 不建议滥用,很多场景 sum/math.prod/max 更直白:

import math
print(sum([1, 2, 3, 4]))                       # 输出: 10
print(math.prod([1, 2, 3, 4]))                 # 输出: 24

sorted(key=...)、any / all / zip / enumerate

这几个是日常最常打交道的"内置函数式工具"。

words = ["banana", "kiwi", "apple"]
print(sorted(words, key=len))                  # 输出: ['kiwi', 'apple', 'banana']

people = [("小明", 30), ("小红", 25)]
print(sorted(people, key=lambda p: p[1]))      # 输出: [('小红', 25), ('小明', 30)]
# any:只要有一个为真就 True;all:全都为真才 True
print(any(x > 3 for x in [1, 2, 5]))           # 输出: True
print(all(x > 0 for x in [1, 2, 5]))           # 输出: True
print(all(x > 0 for x in [1, -2, 5]))          # 输出: False

# enumerate:同时拿到下标和值(下标从 0 开始,可指定 start)
for i, w in enumerate(["a", "b"], start=1):
    print(i, w)
# 输出: 1 a
# 输出: 2 b

# zip:并行遍历多个序列,按最短的停
names, ages = ["小明", "小红"], [30, 25]
print(list(zip(names, ages)))                  # 输出: [('小明', 30), ('小红', 25)]
print(dict(zip(names, ages)))                  # 输出: {'小明': 30, '小红': 25}

functools.partial 与 itertools

functools.partial 用来"冻结"函数的部分参数,造出一个新函数。

from functools import partial

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

square = partial(power, exp=2)     # 固定指数为 2
print(square(5), square(9))        # 输出: 25 81

itertools 是迭代器工厂,处理组合、重复、切片特别顺手。

import itertools

# 无限计数器 + islice 截取
print(list(itertools.islice(itertools.count(10), 3)))     # 输出: [10, 11, 12]

# chain:把多个可迭代对象首尾拼接
print(list(itertools.chain([1, 2], [3, 4])))              # 输出: [1, 2, 3, 4]

# product:笛卡尔积
print(list(itertools.product([1, 2], "ab")))
# 输出: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]

命令式 vs 推导式改写

同一批逻辑,两种写法对照。注意什么时候不改。

# 1) 求平方
# 命令式
out = []
for x in range(5):
    out.append(x * x)
# 推导式
out = [x * x for x in range(5)]                       # ✅ 改写

# 2) 挑出偶数
# 命令式
out = []
for x in range(10):
    if x % 2 == 0:
        out.append(x)
# 推导式
out = [x for x in range(10) if x % 2 == 0]            # ✅ 改写

# 3) 单词转大写(同样是"映射",适合推导式)
out = [w.upper() for w in ["a", "b"]]                 # ✅ 改写

# 4) 带副作用:读写文件
# ❌ 别硬改成推导式
for line in lines:
    with open("out.txt", "a", encoding="utf-8") as f:
        f.write(line)
# 这种有 I/O 副作用的,保持循环

改写的判断很简单:本质是"生成一份新数据"就改写,"执行一串操作"就保留循环。

常见坑

❌ 把大列表推导式当流式处理,白白吃内存。

total = sum([x * x for x in range(10_000_000)])       # ❌ 先造出一个巨大列表

✅ 用生成器表达式。

total = sum(x * x for x in range(10_000_000))         # ✅ 常量级内存

❌ 推导式里塞副作用,结果没人看得懂。

[print(x) for x in range(3)]                          # ❌ 就为了打印,用普通 for

✅ 用循环。

for x in range(3):
    print(x)

❌ lambda 里写多语句,语法都不允许;✅ 需要多行就用 def。

f = lambda x: y = x + 1            # ❌ SyntaxError:lambda 只能是一个表达式
def f(x):                          # ✅
    y = x + 1
    return y

小结

  • 列表/字典/集合推导式是"生成新容器"的利器,写法 [表达式 for 变量 in 可迭代对象 if 条件]。
  • 多重 for 顺序与普通循环一致,if 只作用于左边那层。
  • 生成器表达式用圆括号,惰性求值、省内存,适合大数据只遍历一次的场景。
  • 逻辑复杂、带副作用、难塞进一行时,果断用循环,可读性优先。
  • map/filter 多数场景可由推导式替代;reduce 常常有更直白的 sum/max。
  • sorted(key=...)、any/all、zip、enumerate 是高频惯用法,值得熟练。
  • functools.partial 冻结参数,itertools 提供组合/切片/拼接等迭代器工具。

延伸阅读

  • 大师篇《迭代器、生成器与协程》会把"惰性求值"讲到底层,读完你就明白生成器表达式为什么省内存。
  • 入门篇《列表元组字典集合》是这些容器的地基,觉得自己对切片、dict 方法还不熟的话,可以回头补一补。

上一篇:异常处理 · 下一篇:变量与数据类型

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《推导式与函数式工具(Python 从精通到入门 · 21)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。