跳至主要内容

Python reduce():完整指南

了解何时以及如何使用 Python 的 reduce()。包含实用示例与最佳实践。
· 11分钟 阅读

使用 AI 探索

Python 的 reduce() 函数源自函数式编程世界。 函数式编程(FP)是一种编程范式,其中程序通过对不可变数据应用函数来构建结果。 

这种风格中的一个常见模式是“折叠(fold)”,即将序列归约为单一结果。例如,将数字列表 [2, 4, 5, 3] 在加法下逐步折叠为 14: [2, 4, 5, 3] → [6, 5, 3] → [11, 3] → 14。

reduce() 将这一思想进行泛化。它在可迭代对象上应用一个二元操作,直到只剩下结果。

在本文中,我将探讨 Python reduce() 的关键要素,并给出一些实用示例。若您需要回顾 Python 基础,建议查看以下资源:

历史

Python 中还包含其他函数式编程函数,例如 map() 和 filter()。 

诸如 map() 和 filter() 等函数式构造自 Python 1.0 起便已存在。Guido van Rossum 并不喜欢它们,他指出 reduce() 难以理解,而 for 循环几乎总是更易读。按照 PEP 3100,在 Python 3.0 中,开发者将 reduce() 从内置函数中移除,并移至 functools 模块。这一调整实质上将它降级为小众工具。

为何使用 reduce()?

大多数情况下,我认为使用内置函数或循环通常是更好的选择。不过,在某些用例中,reduce() 仍然是一个不错的选择。

  • 函数管道。以简洁方式串联(可能是动态的)一系列转换。
  • 代数式折叠。用于具有自然幺元的操作,例如与空集的集合并或与 0 的位掩码操作。
  • 无内置的自定义折叠。当不存在内置函数时,以领域特定的方式定义合并逻辑。
  • 结构化累加器。在一个自定义累加函数中同时跟踪多段状态。

Python reduce() 的工作原理

让我们来看看 reduce() 的机制。

reduce() 的基本函数签名为 

functools.reduce(function, iterable, [initializer])

reduce() 接受两个必需参数和一个可选的第三个参数。

  • function:一个二元函数,指定如何组合两个元素,
  • iterable:要归约的序列或可迭代对象,如列表或元组, initializer(可选):用于为函数提供种子的初始值。

逐步示例:

  •  [1, 3, 2, 7] → [4, 2, 7]。
  • [4, 2, 7] → [6, 7]。
  • [6, 7] → [13]。

最终结果是 13。

简单的 reduce() 示例

以下玩具示例演示了如何使用 reduce() 的机制。 

from functools import reduce

numbers = [2, 4, 6]
product = reduce(lambda x, y: x * y, numbers)  # ((2 x 4) x 6) = 8 x 6 = 48
min_value = reduce(lambda x, y: x if x < y else y, numbers) # 2

words = ['dog', 'cat', 'tree', 'pony']
str_concat = reduce(lambda x, y: x + y, words)  # "dogcattreepony"

初始化器(Initializer)

如果不提供初始化器,reduce() 会将可迭代对象的第一个元素作为起始值。若可迭代对象为空,reduce() 会抛出 TypeError。为使代码健壮,请提供初始化器以定义空输入时的行为。

from functools import reduce

words = []
# Error: reduce() of empty iterable with no initial value
str_concat = reduce(lambda x, y: x + y, words)

# Correct: use empty string initializer
str_concat = reduce(lambda x, y: x + y, words, "")

初始化器还可用于以空容器为结果“播种”。例如,您可以将单词连接为一个扁平的字符列表。 

from functools import reduce

words = ['reduce', 'is', 'fun']
chars_list = reduce(lambda acc, word: acc + list(word), words, [])
print(chars_list)  # ['r', 'e', 'd', 'u', 'c', 'e', 'i', 's', 'f', 'u', 'n']

若要进一步探索 Python 与数据处理,以下是我推荐的一些选项。

定义归约器(reducer)

到目前为止,我们使用的是 lambda 函数来定义二元运算符。 

您也可以使用 operator 模块中的运算符。operator 模块 提供常见运算符和方法调用的函数版本。例如,可以用 operator.add(x, y) 代替 x + y。这样您就可以将预定义(且高效)的运算符传入 reduce(),无需编写 lambda。

from functools import reduce
import operator as op

numbers = [2, 4, 6]
total = reduce(op.add, numbers, 0)  # instead of reduce(lambda x,y: x + y, numbers)

第三种选择是编写自定义函数。当没有预定义运算符,或函数复杂到不适合用 lambda 时,这是一个好办法。 

例如,假设您想从列表中移除重复项,但保留首次出现的顺序。您可以定义一个归约器,仅在某项此前未出现过时才将其追加到列表中。  

from functools import reduce


items = ['the', 'wild', 'wild', 'world', 'is', 'the', 'wide', 'world', 'is', 'the', 'world']


def dedup(acc, x):
    if x not in acc:  # O(n) membership test
        acc.append(x)
    return acc




unique = reduce(dedup, items, [])  # ['the', 'wild', 'world', 'is', 'wide']

如需更多去重思路,请参考本教程。

Python reduce() 的性能

reduce() 涉及性能方面的问题。

函数调用开销

Python 的 reduce() 会为每个元素调用一次我们的函数。对于拥有一百万项的列表,这意味着一百万次函数调用,每次都要创建栈帧、处理参数、更新引用计数。这会带来显著的开销。

相比之下,像 sum 这样的内置函数只需调用一次 C 函数,并在 C 循环内完成这一百万次加法。这种差异会使内置函数的速度快出几个数量级。

缓存局部性与 CPU 效率问题

Reduce 还会在缓存局部性和 CPU 效率方面受限。  

现代 CPU 每秒可执行数十亿条指令,但访问内存(RAM)的速度要慢几个数量级。为弥补这一差距,现代 CPU 具有缓存(L1、L2、L3),用来快速存储和访问数据。

这些缓存利用两种模式。 

  • 时间局部性:最近使用过的数据很可能会再次被使用。 
  • 空间局部性:靠近最近使用地址的数据很可能即将被使用。

相比之下,reduce() 的每一步都需要通过指针遍历来寻找下一个元素,并进行 Python 函数调用,这会破坏局部性并让 CPU 停顿。内置函数与向量化函数通过在紧凑的 C 循环中运行来避免这个问题。

若需回顾如何编写地道且高效的 Python 代码,请查看:

Python reduce() 的替代方案

内置函数:

  • 优化的 C 代码。内置函数在优化的 C 代码中运行循环,而非 Python,从而避免了 reduce() 带来的开销。随着输入规模增大,这种速度优势会进一步放大。 
  • 可读性。内置函数名称具有描述性(sum、min),意图显而易见。调用 reduce() 则需要您解析被折叠的函数。

循环:

  • 性能。循环通常比内置函数慢,但比 reduce() 更快。
  • 可读性。与内置函数类似,循环通常比 reduce() 更易读。调用 reduce() 会迫使您解析一个函数式表达式,而循环更具 Python 风格。

itertools.accumulate()

Python 的 itertools 库提供了一组高性能迭代器,例如 count()、product() 和 combinations()。其中一个有用的 itertools 函数是 itertools.accumulate()。与 reduce() 类似,它将函数折叠到可迭代对象上。然而,accumulate() 会存储计算的中间值,而不仅仅是最终结果。 

例如,

import itertools, operator
from functools import reduce

list(itertools.accumulate([1, 2, 3, 4], initial=0)) # [0, 1, 3, 6, 10]
reduce(operator.add, [1, 2, 3, 4], 0) # 10

当您需要运行中的总和或最小值/最大值时,accumulate 非常有用。例如,您可能想知道各月的最高气温。

使用 reduce() 的常见陷阱

当您使用 reduce() 时,请牢记以下陷阱。

  • 优先考虑更简单的替代方案,如内置函数或循环。仅在确有必要时再使用 reduce()。
  • 处理空可迭代对象。始终使用合适的初始化器以避免空输入时报错。
  • 注意内存问题。不要将 reduce() 生搬硬套到应当使用生成器或流式处理更高效的场景中。
  • 避免晦涩的 lambda。尽可能使用 operator 模块中的函数。Lambda 尤其在非结合性操作中会损害清晰度。
  • 重视清晰胜于技巧性。

Python reduce() 的最佳实践与指南 

与任何工具一样,使用 reduce() 也有最佳实践。如果您决定 reduce() 是合适的工具,请遵循以下指南。

先设计归约器 

  • 用一句话定义契约。“按键合并字典,并对每个键的计数求和。”
  • 尽量保持可结合性(associative)。这有助于并行化与测试。
  • 识别您的幺元,并将其用作初始化器。例如,对于求和,幺元是 0;对于 min,是 math.inf;对于 集合并集,是 set()。这样可以保持代码健壮,避免 TypeErrors。

保持简洁与可读

  • 聚焦单一清晰操作。无副作用。
  • 为归约器起一个描述性名称。

文档化

  • 在文档字符串中记录幺元与空输入行为、结合性假设、错误策略。

测试

  • 针对边界情况的单元测试:空可迭代、混合类型、极端值。
  • 测试结合性。

监控性能

  • 对小输入与大输入进行基准测试。与内置函数和循环进行比较。
  • 若速度重要,考虑预处理、分批处理,并将繁重的数学运算交给 NumPy 或 pandas。

Python reduce() 的高级应用与真实用例

鉴于其劣势,reduce() 似乎毫无价值。事实并非如此,reduce() 仍有许多有用的用例。 

  • 处理嵌套结构
  • 类数据库操作
  • 数据处理管道
  • MapReduce 应用

处理嵌套结构

Reduce 提供了一种简洁的方法来遍历嵌套数据结构(如 JSON 对象),通过将一系列键折叠为连续查找。

import json
from functools import reduce
import operator

data = json.loads('''
{
  "user": {
    "id": "ABC123",
    "name": "Alice",
    "email": "[email protected]",
    "profile": {
      "address": {
        "city": "San Francisco",
        "zip": "94103"
      },
      "age": 34,
      "skills": ["Python", "Data Science", "Machine Learning"]
    }
  }
}
''')

# Example lookups with reduce + operator.getitem
city = reduce(operator.getitem, ["user", "profile", "address", "city"], data)
print(city)  # "San Francisco"

user_id = reduce(operator.getitem, ["user", "id"], data)
print(user_id)  # "ABC123"

age = reduce(operator.getitem, ["user", "profile", "age"], data)
print(age)  # 34

在这里使用 reduce() 很合理。JSON 嵌套很深:user → profile → address → city。与其手动串联查找,不如将路径表示为键的列表。然后使用 reduce(operator.getitem, path, data) 来遍历。这样代码更通用、可读、可复用。

数据处理管道

Reduce 可以通过将数据传递给一系列转换步骤来驱动数据处理管道。每个函数处理一个步骤,按顺序应用便形成管道。以下是一个在将文本输入 NLP 模型之前进行预处理的玩具管道。 

from functools import reduce
import re

# Define preprocessing steps
def strip_punctuation(s):
    return re.sub(r"[^\w\s]", "", s)

def to_lower(s):
    return s.lower()

def remove_stopwords(s):
    stops = {"the", "is", "a", "of"}
    return " ".join(word for word in s.split() if word not in stops)

def stem_words(s):
    # trivial "stemmer": cut off 'ing'
    return " ".join(word[:-3] if word.endswith("ing") else word for word in s.split())

pipeline = [
    strip_punctuation,
    to_lower,
    remove_stopwords,
    stem_words,
]

# Input data
text = "The quick brown fox is Jumping over a log."

# Apply pipeline with reduce
processed = reduce(lambda acc, f: f(acc), pipeline, text)
print(processed)  # quick brown fox jump over log

复杂应用中的错误处理

回到嵌套 JSON 示例。目前,直接调用 reduce(operator.getitem, …) 会在缺少键或遇到非字典时抛出 KeyError 或 TypeError。为提高安全性,可以定义一个辅助函数,在 try/except 中包装 operator.getitem,当出错时返回默认值。

以下是一个可能的辅助函数。

def deep_get(data, keys, default=None):
    """Traverse nested dicts/lists safely with reduce."""
    try:
        return reduce(operator.getitem, keys, data)
    except (KeyError, IndexError, TypeError):
        return default

现在,将示例查找改为使用我们的新函数,而不是未包装的 reduce()

# Example lookups
city = deep_get(data, ["user", "profile", "address", "city"], default="Unknown City")
print(city)  # "San Francisco"

user_id = deep_get(data, ["user", "id"], default="N/A")
print(user_id)  # "ABC123"

age = deep_get(data, ["user", "profile", "age"], default="N/A")
print(age)  # 34

# Example with missing key
phone = deep_get(data, ["user", "profile", "phone"], default="No phone")
print(phone)  # "No phone"

结合 map() 和 filter() 的多步数据转换

您可以将 reduce() 与其他函数式工具(如 map() 和 filter())结合使用,以构建多步数据转换。以下是我们先前 NLP 预处理管道的函数式写法。

from functools import reduce
import re

# Define preprocessing steps
def strip_punctuation(s):
    return re.sub(r"[^\w\s]", "", s)

def to_lower(s):
    return " ".join(map(str.lower, s.split()))

def remove_stopwords(s):
    stops = {"the", "is", "a", "of"}
    return " ".join(filter(lambda w: w not in stops, s.split()))

def stem_words(s):
    # trivial "stemmer": cut off 'ing'
    return " ".join(map(lambda w: w[:-3] if w.endswith("ing") else w, s.split()))

# Pipeline of transformations
pipeline = [
    strip_punctuation,
    to_lower,
    remove_stopwords,
    stem_words,
]

# Input data
text = "The quick brown fox is Jumping over a log."

# Apply pipeline with reduce
processed = reduce(lambda acc, f: f(acc), pipeline, text)
print(processed)  # quick brown fox jump over log

转换步骤包括:

  • 去除标点符号。“The quick brown fox is Jumping over a log.” → “The quick brown fox is Jumping over a log”
  • 转为小写

“The quick brown fox is Jumping over a log” → “the quick brown fox is jumping over a log”

  • 移除停用词。“the quick brown fox is jumping over a log” → “quick brown fox jumping over log”
  • 词干提取

 “quick brown fox jumping log”  → “quick brown fox jump log”

若要进一步探索函数式编程与向量化理念,推荐阅读以下 DataCamp 文章。

与现代 Python 生态的集成

要用好 reduce(),了解它在现代 Python 生态中的定位很有帮助。下面看看它如何与 NumPy 和 pandas 配合、如何支撑并行与分布式系统,以及如何与静态分析器等现代工具协同。

numpy 与 pandas

NumPy 与 pandas 基于优化的 C 代码运行,因此不要用 reduce() 去重复它们的功能。不过,reduce() 适用于具有动态步骤的管道。例如,您可以在数组上组合多个 NumPy 变换。

from functools import reduce
import numpy as np

def standardize(x):
    return (x - x.mean()) / (x.std() + 1e-9)

def clip(x):
    return np.clip(x, 0, 1)

def log(x):
    return np.log1p(x)

x = np.array([1, 500, 40.5, 100, 250.45])
funcs = [standardize, clip, log]
y = reduce(lambda a, f: f(a), funcs, x)  # x is ndarray

并行与分布式计算框架

Reduce 是并行与分布式计算框架的核心。这些系统通过将数据集分区、并行处理各分区、然后将部分结果合并为一个答案来工作。“合并”这一步就是归约。

要让归约在集群中正确工作,请确保满足以下条件。

  • 结合性。无论如何分组,操作都应得到相同结果。这样才能在网络中以任意顺序合并部分结果。
  • 幺元。操作必须有一个不会影响最终结果的初始化器。这样当分区为空或大小不均时,仍能保证正确性。 

如果这些性质不成立,归约要么变慢(因为无法安全并行),要么不正确(因为结果依赖于求值顺序)。

该过程需要可结合的操作与明确的幺元。否则,您会得到缓慢的代码或错误的结果。

静态分析工具

静态分析器(如 mypy、Pyright、ruff 或 bandit)是在不运行代码的情况下检查代码的工具。这些工具可以捕获缺陷、强制执行风格规则,并检查类型正确性。

静态分析器在处理 reduce() 时会遇到困难。在折叠过程中,累加器类型可能与元素类型不同,类型推断会变得混乱。 

请看以下代码。

from functools import reduce

def add_chars(acc, word):
    acc.extend(word)   # extend adds each character of the string
    return acc

chars = reduce(add_chars, ["hi", "ok"], [])
print(chars)  # ['h', 'i', 'o', 'k']

尽管这段代码运行良好,静态分析器仍可能提出以下疑问。

  • 初始化器 [] 的元素类型不明确。 
  • 分析器可能假设累加器与元素类型相同。

为让人和分析器更易理解,请添加类型注解。

from functools import reduce
from typing import List

def add_chars(acc: List[str], word: str) -> List[str]:
    acc.extend(word)   # extend adds each character of the string
    return acc

chars: List[str] = reduce(add_chars, ["hi", "ok"], [])
print(chars)  # ['h', 'i', 'o', 'k']

现在,归约器明确展示了

  • 累加器是 List[str]。
  • 每个元素是 str。
  • 返回类型是 list[str]。

借助这些注解,静态分析器可以更严格地检查代码。 

结语

Reduce 源自函数式编程,将集合折叠为单个结果是其核心思想。尽管它在 Python 中不再是一等工具,但仍有用武之地。当您需要灵活的管道、自定义折叠,或无法恰当地映射到现有函数的操作时,reduce() 是一把强大的工具。谨慎使用时,它能与更广泛的 Python 生态良好集成,并在合适的场景中保持实用性。

以下是一些您可能会觉得有用的 Python 相关链接。

Python reduce() 常见问答

reduce() 的作用是什么?

它将一个接受两个参数的函数反复应用于可迭代对象,将其归约为单个结果。

reduce() 定义在哪里?

在 3 之前的版本中,它是内置函数。此后,它位于 functools 模块中。

我应该向 reduce() 传入什么样的函数?

简单、可结合、且文档完善的函数。避免副作用与非结合性逻辑。

它与 itertools.accumulate() 有何不同?

reduce() 只返回最终结果,accumulate() 会产生所有中间结果。

何时应使用初始化器?

当可迭代对象可能为空时请使用初始化器。

主题