11使用 apply 家族与 foreach 进行迭代

理解 apply、lapply、vapply、tapply 与 foreach 的输入对象、返回结构、结果组合和适用场景。

2026-03-01
RIterationapplyforeachFunctional Programming
本章目录 · 18

R 中的 apply 家族把常见循环模式封装成函数:指定一组数据和一个操作,由迭代函数逐行、逐列、逐元素或分组执行。

这组函数的差异不只在于“处理什么”,还在于返回什么结构。选择时应同时确认输入对象、迭代方向和预期输出。

快速选择

函数 主要输入 迭代方式 典型返回值
apply() matrix、array 按维度 向量、矩阵或 array
lapply() list、vector、data frame 逐元素 list
sapply() list、vector 逐元素并尝试简化 向量、矩阵或 list
vapply() list、vector 逐元素并校验类型 指定类型的向量或 array
tapply() atomic vector + 分组因子 分组聚合 array 或 list

可以先用一句话判断:

  • 矩阵按行或列:apply()
  • 希望稳定得到 list:lapply()
  • 探索时希望自动简化:sapply()
  • 正式代码要求固定输出类型:vapply()
  • 一个向量按组汇总:tapply()

apply():沿矩阵维度计算

apply() 用于 matrix 或 array。MARGIN 指定保留哪些维度:二维矩阵中,1 表示逐行,2 表示逐列。

m <- matrix(1:6, nrow = 2)
m
apply(m, MARGIN = 1, FUN = sum)
apply(m, MARGIN = 2, FUN = mean)

也可以使用简写:

apply(m, 1, sum)
apply(m, 2, mean)

对高维 array,MARGIN 可以包含多个维度:

x <- array(1:24, dim = c(2, 3, 4))

# 保留前两个维度,对第三维求和
apply(x, c(1, 2), sum)

不要随意把 data frame 交给 apply()

apply() 会先把 data frame 转成 matrix。matrix 只能存一种原子类型;只要其中一列是 character,其他列也可能一起被转换成 character。

df <- data.frame(
  id = c("A", "B", "C"),
  score = c(80, 90, 85)
)

as.matrix(df)

混合类型的 data frame 通常应按列使用 lapply(),或者先明确选择数值列。

apply(df["score"], 2, mean)

lapply():逐元素返回 list

lapply() 对向量或 list 的每个元素执行函数,并始终返回 list。

lapply(1:3, function(x) x^2)

稳定返回 list 是它最重要的特征。无论每次结果是标量、向量还是更复杂的对象,外层结构都不会自动改变。

result <- lapply(
  1:3,
  function(x) seq_len(x)
)

str(result)

data frame 本质上是等长列组成的 list,因此 lapply() 会逐列处理:

lapply(iris, class)
lapply(iris[1:4], mean)

需要对一批模型、文件或不等长结果迭代时,list 通常也是最安全的容器。

sapply():尝试简化结果

sapply() 先执行与 lapply() 相同的逐元素计算,再尝试把结果简化成更紧凑的结构。

sapply(1:5, sqrt)
sapply(iris[1:4], mean)

当每次返回一个同类型标量时,结果通常是 atomic vector:

sapply(1:4, function(x) x^2)

当每次返回等长向量时,结果可能被简化成 matrix:

sapply(1:3, function(x) c(value = x, square = x^2))

如果各次结果长度不同,则可能仍然返回 list:

sapply(1:3, seq_len)

这种自动简化适合交互探索,但也意味着输出形状取决于实际数据。输入稍有变化,后续代码收到的对象类型就可能不同。

vapply():预先声明返回模板

vapply()sapply() 的用途相近,但要求通过 FUN.VALUE 声明每次调用应返回的类型和长度。

vapply(
  1:3,
  function(x) x^2,
  FUN.VALUE = numeric(1)
)

模板 numeric(1) 表示每次必须返回一个数值。如果某次返回了错误的长度或不兼容类型,vapply() 会立即报错。

vapply(
  iris[1:4],
  mean,
  FUN.VALUE = numeric(1)
)

返回字符标量时使用 character(1)

vapply(
  iris,
  function(x) class(x)[1],
  FUN.VALUE = character(1)
)

返回固定长度向量时,模板也写成相同长度:

vapply(
  iris[1:4],
  function(x) c(min = min(x), max = max(x)),
  FUN.VALUE = c(min = 0, max = 0)
)

正式脚本、函数和包代码更适合使用 vapply(),因为输出契约直接写在调用中。

tapply():按组汇总向量

tapply() 把一个原子向量按 factor 分组,然后在每组内执行函数。

tapply(
  iris$Sepal.Length,
  iris$Species,
  mean
)

它表达的是常见的 split–apply–combine 模式:

  1. 根据分组变量拆分数据;
  2. 在每组中执行函数;
  3. 把结果组合起来。

多个分组变量可以放进 list:

df <- transform(
  iris,
  WidthGroup = Sepal.Width > median(Sepal.Width)
)

tapply(
  df$Sepal.Length,
  list(df$Species, df$WidthGroup),
  mean
)

缺少某些分组组合时,可以设置默认值:

tapply(
  df$Sepal.Length,
  list(df$Species, df$WidthGroup),
  mean,
  default = NA_real_
)

tapply() 很适合快速分组统计;需要同时汇总多列、产生多个统计量或继续管道处理时,data frame 分组工具通常更方便。

额外参数怎样传给函数

apply 家族中写在 FUN 后面的参数会继续传给目标函数。

x <- list(
  c(1, 2, NA),
  c(3, NA, 5)
)

lapply(x, mean, na.rm = TRUE)
vapply(
  x,
  mean,
  FUN.VALUE = numeric(1),
  na.rm = TRUE
)

如果额外参数会随每次迭代变化,就不再是单一固定参数,应使用多输入迭代、显式循环或其他合适结构。

foreach:显式组合每次结果

foreach 是另一套迭代接口。它的特点是通过 .combine 明确说明每次返回值怎样合并,并可以把顺序执行的 %do% 替换为已注册 backend 上的 %dopar%

library(foreach)

result <- foreach(i = 1:4, .combine = c) %do% {
  sqrt(i)
}

result

组合 data frame 行:

result <- foreach(i = 1:3, .combine = rbind) %do% {
  data.frame(value = i, square = i^2)
}

.combine 必须与每次迭代的返回结构相符。对于复杂或较大的对象,反复 rbind 可能效率较低;先保留 list、最后一次性合并通常更容易验证。

错误处理

result <- foreach(
  i = 1:4,
  .errorhandling = "pass"
) %do% {
  if (i == 2) stop("test error")
  i
}
设置 行为
"stop" 遇到错误时停止
"pass" 将 error object 放进结果
"remove" 从组合结果中移除失败项

"pass" 不是“跳过错误”;它保留错误以便检查。"remove" 虽然让流程继续,但也可能悄悄改变结果数量和样本对应关系。批处理时应同时保存任务 ID、成功状态和错误消息。

切换到并行 backend

library(doParallel)

cluster <- parallel::makeCluster(2)
registerDoParallel(cluster)

result <- foreach(i = 1:4, .combine = c) %dopar% {
  sqrt(i)
}

parallel::stopCluster(cluster)

并行 worker 是独立进程。需要的包、函数和数据必须能够在 worker 中获得;随机数、错误处理和输出顺序也要显式设计。任务很小时,启动与传输成本可能比计算本身更大。

foreach 适合已有 .combine 逻辑、需要切换 backend 的批处理。普通顺序迭代若只需稳定的 vector 或 list,lapply()vapply() 或 purrr 通常更直接;使用 future 的并行策略则参见 R 系列的后续章节。

常见选择错误

apply() 处理混合类型 data frame

先发生 matrix 强制转换,随后数值函数可能收到 character。应改用 lapply() 按列处理,或先选择纯数值列。

假设 sapply() 永远返回 vector

它也可能返回 matrix 或 list。后续代码依赖固定类型时,应换成 vapply()

为了简短而忽略输出结构

迭代代码是否可靠,往往取决于输出约束,而不是函数名有多短。先确定需要 list、atomic vector、matrix 还是分组 array,再选择函数。

把 apply 家族理解成并行计算

这些函数默认仍在当前 R 进程中顺序执行。它们简化的是循环表达,不会自动使用多个 CPU 核心。

for 循环的关系

apply 家族不是比 for 更“高级”的语法,而是对常见迭代模式的封装。

out <- vector("list", length(iris))

for (i in seq_along(iris)) {
  out[[i]] <- class(iris[[i]])
}

等价的 lapply() 写法是:

out <- lapply(iris, class)

当每次迭代需要更新多个对象、提前退出、跳过步骤或维护复杂状态时,for 往往更直接。模式简单且输出清楚时,apply 家族能减少样板代码。

实用选择

  • 探索性代码:lapply()sapply()
  • 需要保留复杂结果:lapply()
  • 正式代码要求稳定类型:vapply()
  • matrix/array 按维度计算:apply()
  • 单一数值向量做快速分组汇总:tapply()

真正需要记住的不是五个函数的语法,而是三件事:输入是什么、沿哪里迭代、输出必须是什么结构。