为什么使用 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::map
与 lapply
的比较,假设使用标准评估,即 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 事先声明的他没有问的事情。
比较 purrr
和 lapply
归结为 方便 和 速度 。
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。除了方便,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
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()
允许您同时映射 x
和 y
。这个
更容易表达想法,例如
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
的内存占用略低。
但是有一个反对意见:如果你想要速度,请使用较低级别的语言。
有什么理由我应该使用
map(<list-like-object>, function(x) <do stuff>)
而不是
lapply(<list-like-object>, function(x) <do stuff>)
输出应该是相同的,我做的基准测试似乎表明 lapply
稍微快一点(应该是 map
需要评估所有非标准评估输入) .
那么对于这种简单的情况,我真的应该考虑切换到 purrr::map
吗?我不是在这里问一个人对语法、purrr 等提供的其他功能的喜欢或不喜欢,而是严格地关于 purrr::map
与 lapply
的比较,假设使用标准评估,即 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 事先声明的他没有问的事情。
比较 purrr
和 lapply
归结为 方便 和 速度 。
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。除了方便,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
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()
允许您同时映射x
和y
。这个 更容易表达想法,例如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
的内存占用略低。
但是有一个反对意见:如果你想要速度,请使用较低级别的语言。