复制—修改机制

前一节中我们展示了惰性求值的工作机制,通过避免不必要计算,节省时间和内存。
本节要介绍 R 的一个重要特性,以便更安全地处理数据。我们先创建一个简单的数值向
量 x1:
x1 <- c(1, 2, 3)
然后将 x1 的值赋给 x2:
x2 <- x1
现在 x1 和 x2 的值完全相同。如果我们修改其中一个向量的一个元素,两个向量都会
改变吗?
x1[1] <- 0
x1
## [1] 0 2 3
x2
## [1] 1 2 3
结果显示改变 x1 的值并不影响 x2。你可能以为赋值操作会自动复制值,并使新变量
指向数据的副本,而不是原始数据。我们用 tracemem( ) 函数来追踪内存中数据的足迹。
先重置向量并追踪 x1 和 x2 的内存地址:
x1 <- c(1, 2, 3)
x2 <- x1
当我们将 tracemem( ) 作用在两个向量上时,结果给出了当前数据的内存地址。如
果被追踪的内存地址改变,将会有一段语句显示原始地址和新地址,说明数据被复制了:
tracemem(x1)
## [1] "<0000000013597028>"
tracemem(x2)
## [1] "<0000000013597028>"
现在这两个向量的值相同,并且共享内存地址,说明它们指向相同的数据,而赋值操
作并没有自动复制数据。那数据是什么时候被复制的呢?
现在将 x1 的第 1 个元素修改为 0:
x1[1] <- 0
## tracemem[0x0000000013597028 -> 0x00000000170c7968]
内存追踪的结果显示 x1 的地址发生了改变。具体来说,x1 和 x2 指向的原始向量被
复制到了一个新的地址。现在我们在两个不同的地址有两份相同的数据。然后副本的第 1
个元素被修改了,x1 指向被修改的副本。
x1 和 x2 现在含有不同的值:x1 指向被修改的向量,而 x2 仍指向原向量。
换句话说,如果多个变量指向同一个对象,那么修改一个变量会生成该对象的一个副
本,这就是复制—修改机制。
修改函数参数是另一种会引发复制—修改机制的情况。例如我们创建如下函数:
modify_first <- function(x) {
x[1] <- 0
x
}
当函数被执行时,它会修改参数 x 的第 1 个元素。我们用向量和列表做两个试验,看
看函数 modify_first( ) 能否修改它们。
首先看数值向量 v1:
v1 <- c(1, 2, 3)
modify_ _first(v1)
## [1] 0 2 3
v1
## [1] 1 2 3
再看列表 v2:
v2 <- list(x = 1, y = 2)
modify_ _first(v2)
## $x
## [1] 0
##
## $y
## [1] 2
v2
## $x
## [1] 1
##
## $y
## [1] 2
以上两个例子中,函数都只返回了原始对象修改后的数据,但是并没有改变原始对象。
但是,直接在函数外部修改向量就会改变原始对象:
v1[1] <- 0
v1
## [1] 0 2 3
v2[1] <- 0
v2
## $x
## [1] 0
##
## $y
## [1] 2
若要使用修改后的数据,就需要将其赋值给原始变量:
v3 = 1:5
v3 <- modify_ _first(v3)
v3
## [1] 0 2 3 4 5
这几个例子表明,修改函数的参数也会生成一个副本以确保函数外部的对象不受影响。
此外,修改对象属性也会触发复制—修改机制。下面这个函数移除数据框的行名称,
并将其列名称替换为大写字母:
change_names <- function(x) {
if (is.data.frame(x)) {
rownames(x) <- NULL
if (ncol(x) <= length(LETTERS)) {
colnames(x) <- LETTERS[1:ncol(x)]
} else {
stop("Too many columns to rename")
}
} else {
stop("x must be a data frame")
}
x
}
为了测试这个函数,我们先创建一个由随机数构成的简单数据框:
small_df <- data.frame(
id = 1:3,
width = runif(3, 5, 10),
height = runif(3, 5, 10))
small_df
## id width height
## 1 1 7.605076 9.991836
## 2 2 8.763025 7.360011
## 3 3 9.689882 8.550459
然后调用函数来观察修改后的数据框:
change_ _names(small_df)
## A B C
## 1 1 7.605076 9.991836
## 2 2 8.763025 7.360011
## 3 3 9.689882 8.550459
根据复制—修改机制,small_df 在移除行名称的时候就被复制了,之后的改动都是
作用在副本上而不是原始数据上。我们可以通过查看 small_df 来验证:
small_df
## id width height
## 1 1 7.605076 9.991836
## 2 2 8.763025 7.360011
## 3 3 9.689882 8.550459
此时,原始数据并没有发生改变。
修改函数外部的对象
尽管存在复制—修改机制,我们仍可以用运算符<<- 修改函数外部的向量。例如我们
有一个变量 x,然后创建一个函数 modify_x 将一个新值赋给变量 x:
x <- 0
modify_x <- function(value) {
x <<- value
}
当调用了这个函数时,x 的值便被替换了:
modify_ _x(3)
x
## [1] 3
若想将一个向量映射到一个新的列表上,同时做一些计算,这个方法就很有用的。以
下代码创建了一个由元素数目递增的向量构成的列表,在 lapply( ) 的每一次迭代中,
我们用 count( ) 函数计算生成向量的元素数目的总和:
count <- 0
lapply(1:3, function(x) {
result <- 1:x
count <<- count + length(result)
result
})
## [[1]]
## [1] 1
##
## [[2]]
## [1] 1 2
##
## [[3]]
## [1] 1 2 3
count
## [1] 6
运算符<<- 的另一个用法是“拉平”一个嵌套列表。假设我们有如下嵌套列表:
nested_list <- list(
a = c(1, 2, 3),
b = list(
x = c("a", "b", "c"),
y = list(
z = c(TRUE, FALSE),
w = c(2, 3, 4))
)
)
str(nested_list)
## List of 2
## $ a: num [1:3] 1 2 3
## $ b:List of 2
## ..$ x: chr [1:3] "a" "b" "c"
## ..$ y:List of 2
## .. ..$ z: logi [1:2] TRUE FALSE
## .. ..$ w: num [1:3] 2 3 4
我们想要把这个列表“拉平”,即将所有嵌套的部分放在最外层。以下的代码运
用 rapply( ) 和<<- 实现这个结果。
首先,我们要知道 rapply( ) 是 lapply( ) 的递归版本。每次迭代中,函数都作用在
列表特定层的原子向量上,直到遍历所有层的所有原子向量。rapply(nested_list, f)
的主要运行方式是这样的:
f(c(1, 2, 3))
f(c("a", "b", "c"))
f(c(TRUE, FALSE))
f(c(2, 3, 4))
要记住,我们需要解决的问题是如何“拉平”nested_list。而将要讨论的解法的灵
感来源于 Stackoverflow 上的一个答案(http://stackoverflow.com/a/8139959/2906900),它巧妙地
利用了 rapply( )函数。首先,我们创建一个用于存放嵌套向量的空列表和一个计数器:
flat_list <- list()
i <- 1
然后,利用 rapply( ) 将一个函数递归地应用在 nested_list 上。每一次迭代,
函数都通过 x 获得一个 nested_list 中的原子向量,然后将 flat_list 的第 i 个元素
设为 x ,并将 i 加 1:
res <- rapply(nested_list, function(x) {
flat_list[[i]] <<- x
i <<- i + 1
})
迭代完成后,所有的原子向量都被存储在 flat_list 的最外层。rapply( ) 的返回
结果如下:
res
## a b.x b.y.z b.y.w
## 2 3 4 5
res 中迭代次数的结果,即 i<<-i+1 表达式最终 i 的值,并不那么重要。而 res 中
的列名却表明了 flat_list 中每个元素的原始层级和名称。因此我们将 res 中的元素名
赋给 flat_list,以标示每个元素的原始层级:
names(flat_list) <- names(res)
str(flat_list)
## List of 4
## $ a : num [1:3] 1 2 3
## $ b.x : chr [1:3] "a" "b" "c"
## $ b.y.z: logi [1:2] TRUE FALSE
## $ b.y.w: num [1:3] 2 3 4
至此,nested_list 中的所有元素都以扁平结构存储在了 flat_list 中。

posted @ 2019-02-11 10:10  NAVYSUMMER  阅读(151)  评论(0编辑  收藏  举报
交流群 编程书籍