9.3 遍历列表

现在,你已经注意到向量化在R中无处不在。事实上,你会很自然地选择编写向量化代码。因为它使代码看上去更精简,且与循环相比它的性能更好。不过,在某些情况下,保持矢量化意味着控制代码的方式不太自然。此时,apply系列的函数能更自然地让你进行“伪矢量化”2。

2 由于向量化发生在R语言的级别,而非通过调用内部的C代码实现,所以它不能帮你得到更好的性能,仅使代码更可读。

最简单且常用的成员函数是lapply,它是“list apply”的缩写。lapply的输入参数是某个函数,此函数将依次作用于列表中的每个元素上,并将结果返回到另一个列表中。回忆一下第5章介绍的质因数分解列表:

  1. prime_factors <- list(
  2. two = 2,
  3. three = 3,
  4. four = c(2, 2),
  5. five = 5,
  6. six = c(2, 3),
  7. seven = 7,
  8. eight = c(2, 2, 2),
  9. nine = c(3, 3),
  10. ten = c(2, 5)
  11. )
  12. head(prime_factors)
  13. ## $two
  14. ## [1] 2
  15. ##
  16. ## $three
  17. ## [1] 3
  18. ##
  19. ## $four
  20. ## [1] 2 2
  21. ##
  22. ## $five
  23. ## [1] 5
  24. ##
  25. ## $six
  26. ## [1] 2 3
  27. ##
  28. ## $seven
  29. ## [1] 7

以向量化的方式在每个列表元素中搜索唯一值是很难做到的。我们可以写一个for循环来逐个地检查元素,但这种方法有点笨拙:

  1. unique_primes <- vector("list", length(prime_factors))
  2. for(i in seq_along(prime_factors))
  3. {
  4. unique_primes[[i]] <- unique(prime_factors[[i]])
  5. }
  6. names(unique_primes) <- names(prime_factors)
  7. unique_primes
  8. ## $two
  9. ## [1] 2
  10. ##
  11. ## $three
  12. ## [1] 3
  13. ##
  14. ## $four
  15. ## [1] 2
  16. ##
  17. ## $five
  18. ## [1] 5
  19. ##
  20. ## $six
  21. ## [1] 2 3
  22. ##
  23. ## $seven
  24. ## [1] 7
  25. ##
  26. ## $eight
  27. ## [1] 2
  28. ##
  29. ## $nine
  30. ## [1] 3
  31. ##
  32. ## $ten
  33. ## [1] 2 5

lapply大大简化了这种操作,你无需再用那些陈腔滥调的代码来进行长度和名称检查:

  1. lapply(prime_factors, unique)
  2. ## $two
  3. ## [1] 2
  4. ##
  5. ## $three
  6. ## [1] 3
  7. ##
  8. ## $four
  9. ## [1] 2
  10. ##
  11. ## $five
  12. ## [1] 5
  13. ##
  14. ## $six
  15. ## [1] 2 3
  16. ##
  17. ## $seven
  18. ## [1] 7
  19. ##
  20. ## $eight
  21. ## [1] 2
  22. ##
  23. ## $nine
  24. ## [1] 3
  25. ##
  26. ## $ten
  27. ## [1] 2 5

如果函数的每次返回值大小相同,且你知其大小为多少,那么你可以使用lapply的变种vapplyvapply的含义是:应用于(apply)列表而返回向量(vector)。和前面一样,它的输入参数是一个列表和函数,但vapply还需要第三个参数,即返回值的模板。它不直接返回列表,而是把结果简化为向量或数组:

  1. vapply(prime_factors, length, numeric(1))
  2. ## two three four five six seven eight nine ten
  3. ## 1 1 2 1 2 1 3 2 2

如果输出不能匹配模板,那么vapply将抛出一个错误——vapply不如lapply灵活,因为它输出的每个元素必须大小相同且必须事先就知道。

还有一种介于lapplyvapply之间的函数sapply,其含义为:简化(simplfy)列表应用。与其他两个函数类似,sapply的输入参数也是一个列表和函数。它不需要模板,但它会尽可能地把结果简化到一个合适的向量和数组中:

  1. sapply(prime_factors, unique) #返回一个列表
  2. ## $two
  3. ## [1] 2
  4. ##
  5. ## $three
  6. ## [1] 3
  7. ##
  8. ## $four
  9. ## [1] 2
  10. ##
  11. ## $five
  12. ## [1] 5
  13. ##
  14. ## $six
  15. ## [1] 2 3
  16. ##
  17. ## $seven
  18. ## [1] 7
  19. ##
  20. ## $eight
  21. ## [1] 2
  22. ##
  23. ## $nine
  24. ## [1] 3
  25. ##
  26. ## $ten
  27. ## [1] 2 5
  28. sapply(prime_factors, length) #返回一个向量
  29. ## two three four five six seven eight nine ten
  30. ## 1 1 2 1 2 1 3 2 2
  31. sapply(prime_factors, summary) #返回一个数组
  32. ## two three four five six seven eight nine ten
  33. ## Min. 2 3 2 5 2.00 7 2 3 2.00
  34. ## 1st Qu. 2 3 2 5 2.25 7 2 3 2.75
  35. ## Median 2 3 2 5 2.50 7 2 3 3.50
  36. ## Mean 2 3 2 5 2.50 7 2 3 3.50
  37. ## 3rd Qu. 2 3 2 5 2.75 7 2 3 4.25
  38. ## Max. 2 3 2 5 3.00 7 2 3 5.00

这非常适合于交互式的应用,因为你通常能自动地得到想要的形式。不过,如果你不太确定输入的是什么,那就要谨慎使用此函数。因为它的结果有时是一个列表,有时一个向量,会使你不知不觉地出错。之前的length的例子返回一个向量。现在,给它传入一个空列表时,看看会发生什么:

  1. sapply(list(), length)
  2. ## list()

如果输入列表中的长度为零,无论函数传入了什么参数,sapply总会返回一个列表。因此,如果你的数据是空的且你已知其返回值,使用vapply会更安全:

  1. vapply(list(), length, numeric(1))
  2. ## numeric(0)

虽然这些函数主要和列表一起使用,但它们也可以接受向量为输入参数。在这种情况下,函数被依次应用到向量中的每个元素上。source函数用于读取和访问R文件的内容(即可以用它来运行R脚本)。不幸地,它不是向量化的。因此,如果想运行某个目录下的所有R脚本,我们需要先把目录中的内容都转换到列表中再传给lapply

在下例中,dir函数返回在指定目录中的文件名,默认为当前工作目录(回忆一下,你可以用getwd找到它)。参数pattern="\.R$"的含义为:只返回以.R为后缀的文件名:

  1. r_files <- dir(pattern = "\\.R$")
  2. lapply(r_files, source)

你可能已经注意到,在所有的例子中,传到lapplyvapplysapply的函数都只有一个参数。这些函数限制你只能传入一个向量化的参数(稍后会谈到如何规避此限制),但你可为其传入其他的标量参数。为此,只需把命名参数传递给lapply(或sapplyvapply),它们就会被传递到内部函数。例如,如果rep.int需要两个参数,而times参数只允许单个的(标量)数值,你可以输入:

  1. complemented <- c(2, 3, 6, 18) #参见http://oeis.org/A000614
  2. lapply(complemented, rep.int, times = 4)
  3. ## [[1]]
  4. ## [1] 2 2 2 2
  5. ##
  6. ## [[2]]
  7. ## [1] 3 3 3 3
  8. ##
  9. ## [[3]]
  10. ## [1] 6 6 6 6
  11. ##
  12. ## [[4]]
  13. ## [1] 18 18 18 18

如果向量参数不是第一个,那会如何?在这种情况下,我们需要自定义一个函数来封装那个真正想调用的函数。为此,你可以另起一行。但更常见的做法是把函数的定义包括在lapply的调用中:

  1. rep4x <- function(x) rep.int(4, times = x)
  2. lapply(complemented, rep4x)
  3. ## [[1]]
  4. ## [1] 4 4
  5. ##
  6. ## [[2]]
  7. ## [1] 4 4 4
  8. ##
  9. ## [[3]]
  10. ## [1] 4 4 4 4 4 4
  11. ##
  12. ## [[4]]
  13. ## [1] 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4

通过把匿名函数传给lapply,我们可以继续简化以上的代码。这是第五章所谈到的技巧:我们无需另起一行就能完成赋值操作,只要把函数传递给lapply即可,连名字都不需要:

  1. lapply(complemented, function(x) rep.int(4, times = x))
  2. ## [[1]]
  3. ## [1] 4 4
  4. ##
  5. ## [[2]]
  6. ## [1] 4 4 4
  7. ##
  8. ## [[3]]
  9. ## [1] 4 4 4 4 4 4
  10. ##
  11. ## [[4]]
  12. ## [1] 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4

在极个别的情况下,你可能需要循环遍历环境(而非列表)中每个变量。对此,你可以使用专门的函数eapply。当然,在最新版本的R中,你也可以使用lapply

  1. env <- new.env()
  2. env$molien <- c(1, 0, 1, 0, 1, 1, 2, 1, 3) #参见http://oeis.org/A008584
  3. env$larry <- c("Really", "leery", "rarely", "Larry")
  4. eapply(env, length)
  5. ## $molien
  6. ## [1] 9
  7. ##
  8. ## $larry
  9. ## [1] 4
  10. lapply(env, length) # 一样的
  11. ## $molien
  12. ## [1] 9
  13. ##
  14. ## $larry
  15. ## [1] 4

rapplylapply函数的递归版本,它允许你循环遍历嵌套列表。这是个特殊的要求,且如果事先使用unlist将数据扁平化就会使代码变得更简单。