有什么理由让我使用吗

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

而不是

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

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

So is there any reason why for such simple cases I should actually consider switching to purrr::map? 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, i.e. map(<list-like-object>, function(x) <do stuff>). Is there any advantage that purrr::map has in terms of performance, exception handling etc.? The comments below suggest that it does not, but maybe someone could elaborate a little bit more?


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

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

我不是在问一个人对语法的好恶, 由purrr等提供的其他功能,但严格地讲 假设使用标准,purrr::map与lapply的比较 评价

如果不考虑语法或purrr的其他功能,则没有使用map的特殊理由。我自己也使用purrr,我对Hadley的回答很满意,但具有讽刺意味的是,它忽略了OP前面所说的他没有问的事情。


比较purrr和lapply可以归结为方便和速度。


1. Purrr::map在语法上比lapply更方便

提取列表的第二个元素

map(list, 2)  

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

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

与拉普兰人

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的一个原因。

2. 类型特定的映射函数只需多行代码

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

这些特定于类型的map函数都返回一个向量,而不是map()和lapply()返回的列表。如果处理的是向量的嵌套列表,可以使用这些特定于类型的map函数直接提取向量,并将向量直接强制转换为int、dbl、chr向量。基本R版本看起来像as.numeric(sapply(…)),as.character(sapply(…)),等等。

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

3.除了方便之外,lapply比map(稍微)快一些

使用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


这个优秀的purrr教程强调了在使用purrr时不必显式地写出匿名函数的便利性,以及特定类型映射函数的好处。


如果您从purrr中使用的惟一函数是map(),那么不 优势并不明显。正如里奇·保罗指出的,主要 map()的优点是它可以帮助你编写compact 常见特殊情况代码:

~。+ 1等价于函数(x) x + 1(以及R-4.1及更新版本中的\(x) x + 1) 列表(“x”,1)相当于x (x)函数[[x]][[1]]。这些 helper比[[-参见pluck来了解细节。 对数据 矩形, .default参数特别有用。

但大多数情况下,您不会使用单个*apply()/map() 函数,你会用到很多,而purrr的优点是 函数之间有更大的一致性。例如:

The first argument to lapply() is the data; the first argument to mapply() is the function. The first argument to all map functions is always the data. With vapply(), sapply(), and mapply() you can choose to suppress names on the output with USE.NAMES = FALSE; but lapply() doesn't have that argument. There's no consistent way to pass consistent arguments on to the mapper function. Most functions use ... but mapply() uses MoreArgs (which you'd expect to be called MORE.ARGS), and Map(), Filter() and Reduce() expect you to create a new anonymous function. In map functions, constant argument always come after the function name. Almost every purrr function is type stable: you can predict the output type exclusively from the function name. This is not true for sapply() or mapply(). Yes, there is vapply(); but there's no equivalent for mapply().

你可能认为所有这些微小的区别都不重要 (就像有些人认为做兼职没有好处一样 base R正则表达式),但根据我的经验,它们会导致不必要的 编程时的摩擦(不同的参数顺序总是习惯的 绊倒我),它们使得函数式编程技术更难实现 学习,因为除了大的想法,你还必须学习一堆东西 偶然的细节。

Purrr还填充了一些方便的地图变体,这些变体在基本R中是没有的:

modify() preserves the type of the data using [[<- to modify "in place". In conjunction with the _if variant this allows for (IMO beautiful) code like modify_if(df, is.factor, as.character) map2() allows you to map simultaneously over x and y. This makes it easier to express ideas like map2(models, datasets, predict) imap() allows you to map simultaneously over x and its indices (either names or positions). This is makes it easy to (e.g) load all csv files in a directory, adding a filename column to each. dir("\\.csv$") %>% set_names() %>% map(read.csv) %>% imap(~ transform(.x, filename = .y)) walk() returns its input invisibly; and is useful when you're calling a function for its side-effects (i.e. writing files to disk).

更不用说其他的帮助程序,比如safely()和partial()。

就我个人而言,我发现当我使用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;

我并不是在询问人们对语法或purrr提供的其他功能的喜欢或不喜欢。

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

方便

2021年10月更新 因为公认的答案和投票第二多的帖子都提到了语法的便利性:

R版本4.1.1及更高版本现在支持速记匿名函数\(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

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

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

考虑到一个人在其职业生涯中可能写了数万或数十万次这样的调用,这种语法长度差异可能相当于写了1或2本小说(例如,小说80000封信)。进一步考虑您的代码输入速度(每分钟约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

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

然而,有一种反对意见:如果你想要速度,就去使用较低级别的语言。


I think people hit most of the points here, but I want to mention that the speedup from the user's perspective in using lapply() becomes much more significant, particularly if you're not using R on Windows, when you upgrade to mclapply() (from the parallel package, which to my knowledge doesn't work on Windows and literally never will). The mclapply() syntax is identical to lapply(), and so if you write your code using lapply() from the beginning, you won't need to change anything about your code aside from typing an "mc" at the beginning of the function call and providing it with a number of cores to use. This may be important if you're using the lapply() to break a job up into parallelizable chunks; speedup factor compared with lapply() will be approximately the number of processor cores being used. If you're using your code on the right server or cluster, that can easily turn hours into seconds.