9.3 遍历列表
现在,你已经注意到向量化在R中无处不在。事实上,你会很自然地选择编写向量化代码。因为它使代码看上去更精简,且与循环相比它的性能更好。不过,在某些情况下,保持矢量化意味着控制代码的方式不太自然。此时,apply
系列的函数能更自然地让你进行“伪矢量化”2。
2 由于向量化发生在R语言的级别,而非通过调用内部的C代码实现,所以它不能帮你得到更好的性能,仅使代码更可读。
最简单且常用的成员函数是lapply
,它是“list apply”的缩写。lapply
的输入参数是某个函数,此函数将依次作用于列表中的每个元素上,并将结果返回到另一个列表中。回忆一下第5章介绍的质因数分解列表:
prime_factors <- list(
two = 2,
three = 3,
four = c(2, 2),
five = 5,
six = c(2, 3),
seven = 7,
eight = c(2, 2, 2),
nine = c(3, 3),
ten = c(2, 5)
)
head(prime_factors)
## $two
## [1] 2
##
## $three
## [1] 3
##
## $four
## [1] 2 2
##
## $five
## [1] 5
##
## $six
## [1] 2 3
##
## $seven
## [1] 7
以向量化的方式在每个列表元素中搜索唯一值是很难做到的。我们可以写一个for
循环来逐个地检查元素,但这种方法有点笨拙:
unique_primes <- vector("list", length(prime_factors))
for(i in seq_along(prime_factors))
{
unique_primes[[i]] <- unique(prime_factors[[i]])
}
names(unique_primes) <- names(prime_factors)
unique_primes
## $two
## [1] 2
##
## $three
## [1] 3
##
## $four
## [1] 2
##
## $five
## [1] 5
##
## $six
## [1] 2 3
##
## $seven
## [1] 7
##
## $eight
## [1] 2
##
## $nine
## [1] 3
##
## $ten
## [1] 2 5
lapply
大大简化了这种操作,你无需再用那些陈腔滥调的代码来进行长度和名称检查:
lapply(prime_factors, unique)
## $two
## [1] 2
##
## $three
## [1] 3
##
## $four
## [1] 2
##
## $five
## [1] 5
##
## $six
## [1] 2 3
##
## $seven
## [1] 7
##
## $eight
## [1] 2
##
## $nine
## [1] 3
##
## $ten
## [1] 2 5
如果函数的每次返回值大小相同,且你知其大小为多少,那么你可以使用lapply
的变种vapply
。vapply
的含义是:应用于(apply)列表而返回向量(vector)。和前面一样,它的输入参数是一个列表和函数,但vapply
还需要第三个参数,即返回值的模板。它不直接返回列表,而是把结果简化为向量或数组:
vapply(prime_factors, length, numeric(1))
## two three four five six seven eight nine ten
## 1 1 2 1 2 1 3 2 2
如果输出不能匹配模板,那么vapply
将抛出一个错误——vapply
不如lapply
灵活,因为它输出的每个元素必须大小相同且必须事先就知道。
还有一种介于lapply
和vapply
之间的函数sapply
,其含义为:简化(simplfy)列表应用。与其他两个函数类似,sapply
的输入参数也是一个列表和函数。它不需要模板,但它会尽可能地把结果简化到一个合适的向量和数组中:
sapply(prime_factors, unique) #返回一个列表
## $two
## [1] 2
##
## $three
## [1] 3
##
## $four
## [1] 2
##
## $five
## [1] 5
##
## $six
## [1] 2 3
##
## $seven
## [1] 7
##
## $eight
## [1] 2
##
## $nine
## [1] 3
##
## $ten
## [1] 2 5
sapply(prime_factors, length) #返回一个向量
## two three four five six seven eight nine ten
## 1 1 2 1 2 1 3 2 2
sapply(prime_factors, summary) #返回一个数组
## two three four five six seven eight nine ten
## Min. 2 3 2 5 2.00 7 2 3 2.00
## 1st Qu. 2 3 2 5 2.25 7 2 3 2.75
## Median 2 3 2 5 2.50 7 2 3 3.50
## Mean 2 3 2 5 2.50 7 2 3 3.50
## 3rd Qu. 2 3 2 5 2.75 7 2 3 4.25
## Max. 2 3 2 5 3.00 7 2 3 5.00
这非常适合于交互式的应用,因为你通常能自动地得到想要的形式。不过,如果你不太确定输入的是什么,那就要谨慎使用此函数。因为它的结果有时是一个列表,有时一个向量,会使你不知不觉地出错。之前的length
的例子返回一个向量。现在,给它传入一个空列表时,看看会发生什么:
sapply(list(), length)
## list()
如果输入列表中的长度为零,无论函数传入了什么参数,sapply
总会返回一个列表。因此,如果你的数据是空的且你已知其返回值,使用vapply
会更安全:
vapply(list(), length, numeric(1))
## numeric(0)
虽然这些函数主要和列表一起使用,但它们也可以接受向量为输入参数。在这种情况下,函数被依次应用到向量中的每个元素上。source
函数用于读取和访问R文件的内容(即可以用它来运行R脚本)。不幸地,它不是向量化的。因此,如果想运行某个目录下的所有R脚本,我们需要先把目录中的内容都转换到列表中再传给lapply
。
在下例中,dir
函数返回在指定目录中的文件名,默认为当前工作目录(回忆一下,你可以用getwd
找到它)。参数pattern="\.R$"
的含义为:只返回以.R为后缀的文件名:
r_files <- dir(pattern = "\\.R$")
lapply(r_files, source)
你可能已经注意到,在所有的例子中,传到lapply
、vapply
和sapply
的函数都只有一个参数。这些函数限制你只能传入一个向量化的参数(稍后会谈到如何规避此限制),但你可为其传入其他的标量参数。为此,只需把命名参数传递给lapply
(或sapply
、vapply
),它们就会被传递到内部函数。例如,如果rep.int
需要两个参数,而times
参数只允许单个的(标量)数值,你可以输入:
complemented <- c(2, 3, 6, 18) #参见http://oeis.org/A000614
lapply(complemented, rep.int, times = 4)
## [[1]]
## [1] 2 2 2 2
##
## [[2]]
## [1] 3 3 3 3
##
## [[3]]
## [1] 6 6 6 6
##
## [[4]]
## [1] 18 18 18 18
如果向量参数不是第一个,那会如何?在这种情况下,我们需要自定义一个函数来封装那个真正想调用的函数。为此,你可以另起一行。但更常见的做法是把函数的定义包括在lapply
的调用中:
rep4x <- function(x) rep.int(4, times = x)
lapply(complemented, rep4x)
## [[1]]
## [1] 4 4
##
## [[2]]
## [1] 4 4 4
##
## [[3]]
## [1] 4 4 4 4 4 4
##
## [[4]]
## [1] 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
通过把匿名函数传给lapply
,我们可以继续简化以上的代码。这是第五章所谈到的技巧:我们无需另起一行就能完成赋值操作,只要把函数传递给lapply
即可,连名字都不需要:
lapply(complemented, function(x) rep.int(4, times = x))
## [[1]]
## [1] 4 4
##
## [[2]]
## [1] 4 4 4
##
## [[3]]
## [1] 4 4 4 4 4 4
##
## [[4]]
## [1] 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
在极个别的情况下,你可能需要循环遍历环境(而非列表)中每个变量。对此,你可以使用专门的函数eapply
。当然,在最新版本的R中,你也可以使用lapply
:
env <- new.env()
env$molien <- c(1, 0, 1, 0, 1, 1, 2, 1, 3) #参见http://oeis.org/A008584
env$larry <- c("Really", "leery", "rarely", "Larry")
eapply(env, length)
## $molien
## [1] 9
##
## $larry
## [1] 4
lapply(env, length) # 一样的
## $molien
## [1] 9
##
## $larry
## [1] 4
rapply
是lapply
函数的递归版本,它允许你循环遍历嵌套列表。这是个特殊的要求,且如果事先使用unlist
将数据扁平化就会使代码变得更简单。