如何正确使用列表?

345
简要背景:许多(大多数?)广泛使用的现代编程语言至少有一些共同的ADT [抽象数据类型],特别是:
- 字符串(由字符组成的序列) - 列表(值的有序集合),以及 - 基于映射的类型(将键映射到值的无序数组)
在R编程语言中,前两者分别实现为charactervector
当我开始学习R时,从一开始就有两件事是显而易见的:列表是R中最重要的数据类型(因为它是R data.frame的父类),其次,我就是无法理解它们的工作原理,至少不足以在我的代码中正确使用它们。
首先,对我来说,R的list数据类型似乎是map ADT(在Python中是dictionary,在Objective C中是NSMutableDictionary,在Perl和Ruby中是hash,在Javascript中是object literal等)的一个直接实现。
例如,您可以像创建Python字典一样通过向构造函数传递键值对来创建它们(在Python中是dict而不是list):
x = list("ev1"=10, "ev2"=15, "rv"="Group 1")

您可以像访问Python字典一样访问R List中的项,例如x['ev1']。同样,您可以通过以下方式仅检索'keys'或仅检索'values'

names(x)    # fetch just the 'keys' of an R list
# [1] "ev1" "ev2" "rv"

unlist(x)   # fetch just the 'values' of an R list
#   ev1       ev2        rv 
#  "10"      "15" "Group 1" 

x = list("a"=6, "b"=9, "c"=3)  

sum(unlist(x))
# [1] 18

但是,R中的list也与其他映射类型ADT(至少在我学过的语言中)不同。我的猜测是这是基于S的初始规范的结果,即意图从头开始设计一个数据/统计DSL [特定领域语言]。

R中的list和其他广泛使用的语言中的映射类型有三个重要差异(例如Python、Perl、JavaScript):

首先,R中的list是一种有序集合,就像向量一样,即使值是键控的(即,键可以是任何可哈希值,而不仅仅是连续整数)。几乎总是,在其他语言中,映射数据类型是无序的

其次list可以从函数返回,即使在调用函数时你没有传递list,而且即使返回list的函数不包含(显式的)list构造函数(当然,你可以通过将返回结果包装在对unlist的调用中来处理这个问题):

x = strsplit(LETTERS[1:10], "")     # passing in an object of type 'character'

class(x)                            # returns 'list', not a vector of length 2
# [1] list

R的list的第三个特点:它似乎不能成为另一个ADT的成员,如果你尝试这样做,那么主容器将被强制转换为list。例如:

x = c(0.5, 0.8, 0.23, list(0.5, 0.2, 0.9), recursive=TRUE)

class(x)
# [1] list

我在这里的意图不是批评语言或者它的文档,同样的,我也不认为list数据结构或者其行为有任何问题。我只是想纠正对它们如何工作的理解,以便我可以正确地在我的代码中使用它们。

以下是我想更好地了解的内容:

  • 什么规则决定了函数调用何时返回一个 list (例如,上面提到的 strsplit 表达式)?

  • 如果我没有显式地为 list 分配名称(例如,list(10,20,30,40)),默认名称是否只是从 1 开始的顺序整数?(我假设答案是肯定的,否则我们就无法通过调用 unlist 将此类型的 list 强制转换为向量。)

  • 为什么这两个不同的运算符 [][[]] 返回 相同 的结果?

    x = list(1, 2, 3, 4)

    这两个表达式都返回 "1":

    x[1]

    x[[1]]

  • 为什么这两个表达式返回相同的结果?

    x = list(1, 2, 3, 4)

    x2 = list(1:4)

请不要引导我去阅读R文档(?listR-intro),我已经仔细阅读过它,但它并不能帮助我回答上述问题的类型。
(最后,我最近了解并开始使用一个R包(可在CRAN上获得),名为hash,它通过S4类实现了传统的映射类型行为;我当然可以推荐这个包。)

5
对于 x = list(1, 2, 3, 4),使用 x[1]x[[1]] 并不会得到相同的结果。前者返回一个列表,后者返回一个数值向量。向下滚动看来,我认为 Dirk 是唯一一个正确回答这个问题的人。 - IRTFM
3
我没注意到有人扩展你列举的R语言中list与哈希表不同的方法,我还有一个认为值得注意的。在R中,list可以有两个具有相同引用名称的成员。考虑到 obj <- c(list(a = 1),list(a = 2))是有效的,并返回一个具有两个名为'a'的命名值的列表。在这种情况下,对于obj["a"]的调用将仅返回第一个匹配的列表元素。您可以使用R中的环境来获得类似于哈希表的行为(可能是相同的),每个引用名称只使用一个条目,例如 x <- new.env(); x[["a"]] <- 1; x[["a"]] <- 2; x[["a"]] - russellpierce
2
我在过去的六个月中已经三次重新阅读了这篇带有答案的帖子,并每次都发现更多的启示。非常好的问题和一些很棒的答案。谢谢。 - Rich Lysakowski PhD
13个回答

159

只回答你问题的最后一部分,因为这真正指出了在R中listvector之间的区别:

为什么这两个表达式没有返回相同的结果?

x = list(1, 2, 3, 4); x2 = list(1:4)

列表可以包含任何其他类作为每个元素。因此,您可以拥有一个列表,其中第一个元素是字符向量,第二个元素是数据框等。在这种情况下,您创建了两个不同的列表。x有四个长度为1的向量。x2有一个长度为4的向量:

> length(x[[1]])
[1] 1
> length(x2[[1]])
[1] 4

所以这些是完全不同的列表。

R列表非常类似于哈希映射数据结构,每个索引值都可以与任何对象相关联。以下是一个包含3个不同类(包括函数)的简单列表示例:

> complicated.list <- list("a"=1:4, "b"=1:3, "c"=matrix(1:4, nrow=2), "d"=search)
> lapply(complicated.list, class)
$a
[1] "integer"
$b
[1] "integer"
$c
[1] "matrix"
$d
[1] "function"

假设最后一个元素是搜索函数,我可以这样调用它:

> complicated.list[["d"]]()
[1] ".GlobalEnv" ...

作为最后的评论:需要注意的是,data.frame实际上是一个列表(来自data.frame文档):

数据框是具有唯一行名称的相同行数变量的列表,给定类别“"data.frame"”

这就是为什么data.frame中的列可以具有不同的数据类型,而矩阵中的列则不能。例如,在这里我尝试创建一个包含数字和字符的矩阵:
> a <- 1:4
> class(a)
[1] "integer"
> b <- c("a","b","c","d")
> d <- cbind(a, b)
> d
 a   b  
[1,] "1" "a"
[2,] "2" "b"
[3,] "3" "c"
[4,] "4" "d"
> class(d[,1])
[1] "character"

请注意,由于第二列中存在字符,因此我无法将第一列的数据类型更改为数字:

> d[,1] <- as.numeric(d[,1])
> class(d[,1])
[1] "character"

5
明白了,谢谢。顺便说一下,你提到的关于“复杂列表”的例子,正是在C++、Java等没有“switch”语句的语言中,复制该语句的标准方式;当我需要时,在R中使用这种方法可能是不错的选择。+1 - doug
8
жІЎй—®йўҳпјҢиҷҪ然еңЁRдёӯжңүдёҖдёӘжңүз”Ёзҡ„switchеҮҪж•°еҸҜд»Ҙз”ЁдәҺжӯӨзӣ®зҡ„пјҲиҜ·еҸӮйҳ…help(switch)пјүгҖӮ - Shane

67

关于你的问题,让我按顺序回答并举些例子:

1) 只有在return语句中添加了列表时才会返回列表。考虑以下示例:

 R> retList <- function() return(list(1,2,3,4)); class(retList())
 [1] "list"
 R> notList <- function() return(c(1,2,3,4)); class(notList())
 [1] "numeric"
 R> 

2) 名称并未被设置:

R> retList <- function() return(list(1,2,3,4)); names(retList())
NULL
R> 

3) 它们并不返回相同的东西。你的例子给出了

R> x <- list(1,2,3,4)
R> x[1]
[[1]]
[1] 1
R> x[[1]]
[1] 1

x[1]返回x的第一个元素,这与x相同。每个标量都是长度为一的向量。另一方面,x[[1]]返回列表的第一个元素。

4) 最后,请注意它们之间的区别:分别创建一个包含四个标量的列表和一个包含单个元素(恰好是四个元素的向量)的列表。


1
非常有帮助,谢谢。 (关于你回答中的第一条 - 我同意,但我想说的是像 'strsplit' 这样的内置函数,而不是用户创建的函数。) 无论如何,我给你加1。 - doug
3
关于项目#1,我认为唯一的方法是查看特定函数的帮助文档中的“Value”部分。就像在?strsplit中所描述的那样:“与x长度相同的列表”。但是您应该考虑到,有些函数可能会根据参数返回不同的值(例如,sapply可以返回列表或向量)。 - Marek

37

仅针对您的问题的一个子集:

这篇文章介绍了索引的问题,其中解答了[][[]]之间的区别。

简而言之,[[]]从列表中选择单个项,而[]返回所选项的列表。 在您的示例中,x = list(1, 2, 3, 4)'项1是一个整数,但x[[1]]返回单个1,而x[1]返回仅具有一个值的列表。

> x = list(1, 2, 3, 4)
> x[1]
[[1]]
[1] 1

> x[[1]]
[1] 1

顺便说一下,A = array( 11:16, c(2,3) ); A[5]flat 数组中是15?! - denis

14

列表之所以像它们实现的那样(有序),是为了解决需要一个有序容器,可以在任何节点包含任何类型的需求。向量无法做到这一点。在 R 中,列表还可用于多种目的,包括构成 data.frame 的基础,它是任意类型向量(但长度相同)的列表。

为什么这两个表达式的结果不同?

x = list(1, 2, 3, 4); x2 = list(1:4)

在@Shane的回答之上,如果你想要达到相同的结果,请尝试:

x3 = as.list(1:4)

这将把向量1:4强制转换为列表。


12

只是为了补充一个观点:

R语言确实有一种类似于Python字典的数据结构,位于hash包中。你可以在这个链接中了解更多信息。此外,这篇来自Open Data Group的博客文章也提供了简单的示例:

> library(hash)
> h <- hash( keys=c('foo','bar','baz'), values=1:3 )
> h[c('foo','bar')]
<hash> containing 2 key-value pairs.
  bar : 2
  foo : 1

hash类在可用性方面与列表非常相似,但对于大型数据集,性能更好。


1
我知道哈希包——它在我的原始问题中被提到作为传统哈希类型的合适代理。 - doug
还要注意,相对于哈希环境,使用hash::hash的效用是有问题的。https://rpubs.com/rpierce/hashBenchmarks。 - russellpierce

10

你说:

另外,即使在调用函数时没有传入List,也可以从函数返回列表,即使该函数不包含List构造函数,例如:

x = strsplit(LETTERS[1:10], "") # passing in an object of type 'character'
class(x)
# => 'list'

我猜你认为这是个问题(?)。我在这里告诉你为什么这不是问题 :-)。你的例子有点简单,在进行字符串拆分时,你会得到一个元素长度为1的列表,因此你知道x[[1]]unlist(x)[1]是一样的。但是,如果strsplit返回了不同长度的结果,仅返回向量(而不是列表)根本行不通。

例如:

stuff <- c("You, me, and dupree",  "You me, and dupree",
           "He ran away, but not very far, and not very fast")
x <- strsplit(stuff, ",")
xx <- unlist(strsplit(stuff, ","))
在第一种情况下(x:返回一个列表),你可以通过 x[[3]][2] 来查看第三个字符串的第二个“部分”。现在使用 xx 时,由于结果已经被“展开”(通过 unlist),你该如何做到同样的效果?

8

这是一个很老的问题,但我认为新的答案可能会增加一些价值,因为我认为没有人直接解决了一些OP中的问题。

尽管被接受的答案表明,R中的list对象不是哈希映射。如果你想与python进行比较,list更像,你猜对了,python的lists(或者实际上是tuples)。

最好描述一下大多数R对象的内部存储方式(R对象的C类型为SEXP)。它们基本上由三部分组成:

  • 一个头文件,声明对象的R类型、长度和一些其他元数据;
  • 数据部分,是一个标准的C堆分配数组(连续的内存块);
  • 属性,是指向其他R对象的命名链接列表(如果对象没有属性,则为NULL)。

从内部的角度来看,listnumeric向量之间几乎没有区别。它们所存储的值只是不同而已。让我们将两个对象分解为我们之前描述的范式:

x <- runif(10)
y <- list(runif(10), runif(3))

对于x
  • 头部信息将显示类型为numeric(C端中的REALSXP),长度为10,以及其他一些信息。
  • 数据部分将是一个包含10个double值的数组。
  • 属性为NULL,因为对象没有任何属性。
对于y
  • 头部信息将显示类型为list(C端中的VECSXP),长度为2,以及其他一些信息。
  • 数据部分将是一个包含两个指向两种SEXP类型的指针的数组,分别指向runif(10)runif(3)得到的值。
  • 属性与x一样为NULL
因此numeric向量和list之间唯一的区别在于numeric数据部分由double值组成,而对于list,数据部分是指向其他R对象的指针数组。
那么名称怎么处理呢?嗯,名称只是您可以分配给对象的某些属性之一。让我们看看下面的对象:
z <- list(a=1:3, b=LETTERS)
  • 标头将显示类型为list(在C端为VECSXP),长度为2和其他内容。
  • 数据部分将是一个包含两个指向两种SEXP类型的指针的数组,这些指针分别指向1:3LETTERS获得的值。
  • 属性现在存在,是一个names组件,是一个字符型的R对象,值为c("a","b")

从R级别,您可以使用attributes函数检索对象的属性。

R中哈希映射的键值对只是一种幻象。当你说:

z[["a"]]

这是发生的过程:
  • 调用[[子集函数;
  • 函数的参数("a")属于character类型,因此该方法被指示从对象znames属性(如果存在)中搜索该值;
  • 如果不存在names属性,则返回NULL
  • 如果存在,则在其中搜索"a"值。 如果"a"不是对象的名称,则返回NULL
  • 如果存在,则确定第一个出现的位置(在示例中为1)。 因此,将返回列表的第一个元素,即等价于z[[1]]
键值搜索相当间接,并且始终具有位置性。 同样要记住的是:
  • in hash maps the only limit a key must have is that it must be hashable. names in R must be strings (character vectors);

  • in hash maps you cannot have two identical keys. In R, you can assign names to an object with repeated values. For instance:

      names(y) <- c("same", "same")
    

在R中,y[["same"]]是完全有效的。这将检索第一个值。此时,您应该知道为什么要这样做。

总之,在对象上赋予任意属性的能力可以使外部观察者看到与众不同的东西。但是,R中的list不以任何方式作为哈希映射。


1
在R中,您可以为具有重复值的对象分配“名称”。除了加载包之外,它们是R最接近哈希映射的东西。 - J. Mini
实际上,没有必要分配 names(),因为即使您直接运行 list(a = 1, a = 2),R也不会抱怨。 - Marcelo Ventura

6
x = list(1, 2, 3, 4)
x2 = list(1:4)
all.equal(x,x2)

它们不相同,因为1:4与c(1,2,3,4)相同。 如果你想让它们相同,则:

x = list(c(1,2,3,4))
x2 = list(1:4)
all.equal(x,x2)

5
尽管这是一个很老的问题,但我必须说它恰好触及了我在R语言初学时所缺乏的知识 - 即如何将手头的数据表达为R语言中的对象或如何从现有对象中进行选择。对于一个R语言新手来说,从一开始就“用R的思维方式”并不容易。
因此,我自己开始使用下面的支架,这些支架帮助我很多,让我知道要使用哪个对象来处理什么类型的数据,并基本上能够想象出实际应用场景。
虽然我没有给出确切的答案,但下面的简短文本可能会对刚开始学习R语言并提出类似问题的读者有所帮助。
- 原子向量...我称之为“序列”,没有方向,只是相同类型的序列。[子集。 - 向量...具有从2D到1D的方向,[子集。 - 矩阵...一堆长度相同的向量形成的行或列,[按行和列、或按序列进行子集。 - 数组...形成3D的分层矩阵 - 数据框...像Excel中的2D表格,可以对其进行排序、添加或删除行或列,或进行算术运算。只有在一段时间后,我才真正意识到数据框是一个聪明的list实现,其中我可以使用[按行和列进行子集,甚至可以使用[[。 - 列表...为了帮助自己,我把列表看作是树形结构,其中[i]选择并返回整个分支,[[i]]返回分支中的项目。由于它是树形结构,因此您甚至可以使用索引序列来访问非常复杂的list上的每个单独的叶子,使用其[[index_vector]]。列表可以是简单的,也可以是非常复杂的,并且可以将各种类型的对象混合在一起。 - 因此,对于lists,您可能会根据情况以更多方式选择一个leaf,例如以下示例。
l <- list("aaa",5,list(1:3),LETTERS[1:4],matrix(1:9,3,3))
l[[c(5,4)]] # selects 4 from matrix using [[index_vector]] in list
l[[5]][4] # selects 4 from matrix using sequential index in matrix
l[[5]][1,2] # selects 4 from matrix using row and column in matrix

这种思维方式对我很有帮助。

3
关于向量和来自其他语言的哈希/数组概念:
  1. Vectors are the atoms of R. Eg, rpois(1e4,5) (5 random numbers), numeric(55) (length-55 zero vector over doubles), and character(12) (12 empty strings), are all "basic".

  2. Either lists or vectors can have names.

    > n = numeric(10)
    > n
     [1] 0 0 0 0 0 0 0 0 0 0
    > names(n)
    NULL
    > names(n) = LETTERS[1:10]
    > n
    A B C D E F G H I J 
    0 0 0 0 0 0 0 0 0 0
    
  3. Vectors require everything to be the same data type. Watch this:

    > i = integer(5)
    > v = c(n,i)
    > v
    A B C D E F G H I J           
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
    > class(v)
    [1] "numeric"
    > i = complex(5)
    > v = c(n,i)
    > class(v)
    [1] "complex"
    > v
       A    B    C    D    E    F    G    H    I    J                          
    0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i
    
  4. Lists can contain varying data types, as seen in other answers and the OP's question itself.

我曾看过一些语言(如Ruby、JavaScript),其中“数组”可以包含不同的数据类型,但在C++中,“数组”必须是相同的数据类型。我相信这是出于速度/效率的考虑:如果你有一个numeric(1e6),你就知道它的大小和每个元素的位置,而如果这个东西可能在某个未知的切片中包含“Flying Purple People Eaters”,那么你就必须解析它来了解关于它的基本信息。
某些标准的R操作在数据类型得到保证时也更有意义。例如,cumsum(1:9)是有意义的,而cumsum(list(1,2,3,4,5,'a',6,7,8,9))则没有,除非类型被保证为double。
关于你的第二个问题:
尽管在调用函数时没有传入List,但是List可以从函数中返回。
函数经常返回与输入不同的数据类型。即使没有传入绘图,plot也会返回一个绘图。Arg接受复数,但返回一个numeric。等等。
(至于strsplit:源代码在这里。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接