为什么使用 purrr::map 而不是 lapply?

Why use purrr::map instead of lapply?

有什么理由我应该使用

map(<list-like-object>, function(x) <do stuff>)

而不是

lapply(<list-like-object>, function(x) <do stuff>)

输出应该是相同的,我做的基准测试似乎表明 lapply 稍微快一点(应该是 map 需要评估所有非标准评估输入) .

那么对于这种简单的情况,我真的应该考虑切换到 purrr::map 吗?我不是在这里问一个人对语法、purrr 等提供的其他功能的喜欢或不喜欢,而是严格地关于 purrr::maplapply 的比较,假设使用标准评估,即 map(<list-like-object>, function(x) <do stuff>) . purrr::map 在性能、异常处理等方面有什么优势吗?下面的评论表明它没有,但也许有人可以详细说明一下?

如果我们不考虑品味方面(否则这个问题应该关闭)或语法一致性、风格等,答案是否定的,没有特殊理由使用 map 而不是 lapply 或 apply 系列的其他变体,例如更严格的 vapply.

PS:对于那些无缘无故投反对票的人,请记住 OP 写道:

I am not asking here about one's likes or dislikes about the syntax, other functionalities provided by purrr etc., but strictly about comparison of purrr::map with lapply assuming using the standard evaluation

如果您不考虑 purrr 的语法或其他功能,则没有特殊理由使用 map。我自己使用 purrr,我对 Hadley 的回答很满意,但具有讽刺意味的是,它超越了 OP 事先声明的他没有问的事情。

比较 purrrlapply 归结为 方便 速度


1。 purrr::map 在语法上比 lapply

更方便

提取列表的第二个元素

map(list, 2)  

作为@F。 Privé指出,等同于:

map(list, function(x) x[[2]])

lapply

lapply(list, 2) # doesn't work

我们需要传递一个匿名函数...

lapply(list, function(x) x[[2]])  # now it works

...或者正如@RichScriven 指出的那样,我们将 [[ 作为参数传递给 lapply

lapply(list, `[[`, 2)  # a bit more simple syntantically

因此,如果发现自己使用 lapply 将函数应用于许多列表,并且厌倦了定义自定义函数或编写匿名函数,那么方便是支持 purrr.[=45 的原因之一=]

2。特定于类型的映射功能只需多行代码

  • map_chr()
  • map_lgl()
  • map_int()
  • map_dbl()
  • map_df()

这些类型特定的映射函数中的每一个 return 都是一个向量,而不是 map()lapply() 编辑的列表 return。如果您正在处理向量的嵌套列表,则可以使用这些特定于类型的映射函数直接提取向量,并将向量直接强制转换为 int、dbl、chr 向量。基础 R 版本看起来像 as.numeric(sapply(...))as.character(sapply(...))

map_<type> 函数还有一个有用的特性,即如果它们不能 return 指定类型的原子向量,它们就会失败。这在定义严格的控制流时很有用,如果它[以某种方式]生成错误的对象类型,您希望函数失败。

3。除了方便,lapplymap

[稍微] 快

使用purrr的便捷功能,如@F。 Privé 指出会稍微减慢处理速度。让我们对上面介绍的 4 个案例中的每一个进行比赛。

# devtools::install_github("jennybc/repurrrsive")
library(repurrrsive)
library(purrr)
library(microbenchmark)
library(ggplot2)

mbm <- microbenchmark(
  lapply       = lapply(got_chars[1:4], function(x) x[[2]]),
  lapply_2     = lapply(got_chars[1:4], `[[`, 2),
  map_shortcut = map(got_chars[1:4], 2),
  map          = map(got_chars[1:4], function(x) x[[2]]),
  times        = 100
)
autoplot(mbm)

获胜者是......

lapply(list, `[[`, 2)

总而言之,如果原始速度是您所追求的:base::lapply(尽管速度并没有那么快)

对于简单的语法和可表达性:purrr::map


This excellent purrr tutorial 强调了 使用 purrr 时不必显式写出匿名函数的便利 ,以及特定类型 [=36] 的好处=] 函数。

如果您从 purrr 使用的唯一函数是 map(),那么不, 优势并不明显。正如 Rich Pauloo 指出的那样,主要 map() 的优点是可以让你写紧凑的助手 常见特殊情况代码:

  • ~ . + 1 等同于 function(x) x + 1(R-4.1 及更新版本中的 \(x) x + 1

  • list("x", 1)等同于function(x) x[["x"]][[1]]。这些 helpers 比 [[ 更通用一些 - 有关详细信息,请参阅 ?pluck。 对于 数据 矩形.default 论点特别有用。

但大多数时候您并没有使用单个 *apply()/map() 功能,你正在使用其中的一堆,而 purrr 的优点是 功能之间的一致性更高。例如:

  • lapply()的第一个参数是数据;第一个参数 mapply() 是函数。所有地图函数的第一个参数 永远是数据。

  • 使用 vapply()sapply()mapply() 您可以选择 使用 USE.NAMES = FALSE 抑制输出中的名称;但 lapply() 没有那个参数。

  • 没有一致的方法将一致的参数传递给 映射函数。大多数函数使用 ...mapply() 使用 MoreArgs(您希望将其称为 MORE.ARGS),以及 Map()Filter()Reduce() 希望您创建一个新的 匿名函数。在 map 函数中,常量参数总是出现 在函数名称之后。

  • 几乎每个 purrr 函数都是类型稳定的:你可以预测 输出类型仅来自函数名称。这不是真的 sapply()mapply()。是的,有vapply();但没有 相当于 mapply().

您可能认为所有这些细微差别都不重要 (就像有些人认为 stringr 没有优势一样 base R 正则表达式),但根据我的经验,它们会导致不必要的 编程时的摩擦(不同的参数顺序总是用于 trip me up),它们使函数式编程技术更难 学习,因为除了伟大的想法,你还必须学习很多东西 附带细节。

Purrr 还填充了一些方便的地图变体,这些变体在基础 R 中不存在:

  • modify()保留数据的类型,使用[[<-修改“in place”。结合 _if 变体,这允许(IMO 漂亮)像 modify_if(df, is.factor, as.character)

    这样的代码
  • map2() 允许您同时映射 xy。这个 更容易表达想法,例如 map2(models, datasets, predict)

  • imap() 允许您同时映射 x 及其索引 (姓名或职位)。这使得(例如)加载所有 csv 个目录中的文件,为每个文件添加一个 filename 列。

    dir("\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
    
  • walk() returns其输入无形;当你 为它的副作用调用一个函数(即将文件写入 磁盘)。

更不用说 safely()partial() 等其他帮手了。

就个人而言,我发现当我使用 purrr 时,我可以编写功能代码 摩擦更少,更轻松;它减少了之间的差距 想出一个主意并付诸实施。但是您的里程可能会有所不同; 没有必要使用 purrr 除非它真的能帮助你。

微基准测试

是的,map()lapply() 稍慢。但是使用成本 map()lapply() 由您映射的内容驱动,而不是开销 执行循环。下面的微基准表明成本 map()lapply() 相比每个元素大约 40 ns,这 似乎不太可能对大多数 R 代码产生实质性影响。

library(purrr)
n <- 1e4
x <- 1:n
f <- function(x) NULL

mb <- microbenchmark::microbenchmark(
  lapply = lapply(x, f),
  map = map(x, f)
)
summary(mb, unit = "ns")$median / n
#> [1] 490.343 546.880

tl;博士

I am not asking about one's likes or dislikes about syntax or other functionalities provided by purrr.

选择与您的用例相匹配的工具,并最大限度地提高您的生产力。对于优先考虑速度的生产代码,使用 *apply,对于需要小内存占用的代码,使用 map。根据人体工程学,map 可能 适合大多数用户和大多数一次性任务。

方便

2021 年 10 月更新 由于接受的答案和第二大投票 post 都提到了语法 convenience:

R 版本 4.1.1 及更高版本现在支持 shorthand 匿名函数 \(x) 和管道 |> 语法。要检查您的 R 版本,请使用 version[['version.string']].

library(purrr)
library(repurrrsive)
lapply(got_chars[1:2], `[[`, 2) |>
  lapply(\(.) . + 1)
#> [[1]]
#> [1] 1023
#> 
#> [[2]]
#> [1] 1053
map(got_chars[1:2], 2) %>%
  map(~ . + 1)
#> [[1]]
#> [1] 1023
#> 
#> [[2]]
#> [1] 1053

如果您的任务涉及 2 次以上的类列表对象操作,purrr 方法的语法通常更短。

nchar(
"lapply(x, fun, y) |>
      lapply(\(.) . + 1)")
#> [1] 45
nchar(
"library(purrr)
map(x, fun) %>%
  map(~ . + 1)")
#> [1] 45

考虑到一个人可能在其职业生涯中写过数万或数十万个这样的电话,这种语法长度差异相当于写1或2本小说(av. 小说 80 000 个字母),假设输入了代码。进一步考虑你的代码输入速度(~65字每分钟?),你的输入准确率(你是否发现你经常打错某些语法(\"< ?), 你的 函数参数的回忆,然后你可以公平地比较 你的 使用一种风格或组合的生产力两人

另一个考虑因素可能是您的目标受众。就我个人而言,我发现解释 purrr::map 如何比 lapply 更难工作正是因为它的语法简洁。

1 |>
  lapply(\(.z) .z + 1)
#> [[1]]
#> [1] 2

1 %>%
  map(~ .z+ 1)
#> Error in .f(.x[[i]], ...) : object '.z' not found

but,
1 %>%
  map(~ .+ 1)
#> [[1]]
#> [1] 2

速度

通常在处理类似列表的对象时,会执行多个操作。关于 purrr 的开销在大多数代码中微不足道的讨论的细微差别 - 处理大型列表和用例。

got_large <- rep(got_chars, 1e4) # 300 000 elements, 1.3 GB in memory
bench::mark(
  base = {
    lapply(got_large, `[[`, 2) |>
      lapply(\(.) . * 1e5) |>
      lapply(\(.) . / 1e5) |>
      lapply(\(.) as.character(.))
  },
  purrr = {
    map(got_large, 2) %>%
      map(~ . * 1e5) %>%
      map(~ . / 1e5) %>%
      map(~ as.character(.))
  }, iterations = 100,
)[c(1, 3, 4, 5, 7, 8, 9)]

# A tibble: 2 x 7
  expression   median `itr/sec` mem_alloc n_itr  n_gc total_time
  <bch:expr> <bch:tm>     <dbl> <bch:byt> <int> <dbl>   <bch:tm>
1 base          1.19s     0.807    9.17MB   100   301      2.06m
2 purrr         2.67s     0.363    9.15MB   100   919      4.59m

执行的操作越多,这种差异越大。如果您正在编写一些用户经常使用的代码或包依赖于它,那么速度可能是您在 base R 和 purr 之间进行选择时要考虑的重要因素。注意 purrr 的内存占用略低。

但是有一个反对意见:如果你想要速度,请使用较低级别的语言。