参考:https://www.math.pku.edu.cn/teachers/lidf/docs/Rbook/html/_Rbook/prog-type-ws.html

简介

在R studio 的分布栏目中,有专门的一栏environment,专门显示读取的相关变量。

这是因为R把在命令行定义的变量都保存到工作空间中, 在退出R时可以选择是否保存工作空间。 这也是R与其他如C、Java这样的语言的区别之一。

我们可以使用命令ls() 查看环境中的变量。

当环境中加载的变量过多需要清空时,可以使用rm() 指定需要删除的变量。

  1. rm(d, h, name, rec, sex, x)

或者指定参数list=ls() 可以删除环境中的全部变量:

  1. rm(list = ls())

避免空间混乱的技巧

  • 方法一

我们可以利用browser 创建browser 命令行,提示符变成了“Browser[n]”,其中n代表层次序号。 在自定义函数中创建的browser命令行中随意定义变量, 定义的变量不会保存到工作空间中。 用“Q”命令可以退出这个沙盘环境, 接连回车也可以退出:

  1. sandbox <- function(){
  2. cat('沙盘:接连的空行回车可以退出。\n')
  3. browser()
  4. }
  5. ##运行sandbox()函数,将出现如下的browser命令行:
  6. 沙盘:接连的空行回车可以退出。
  7. Called from: sandbox()
  8. Browse[1]>
  • 方法二

在定义的函数中创建局部变量,将最终计算的结果return 返回,保存函数,并将函数最终计算结果赋值:

  1. > final
  2. function(){
  3. a = 10
  4. b = 20
  5. c = a+b
  6. return(c)}
  7. > c = final()
  8. > c
  9. [1] 30

变量赋值与绑定

R 的变量赋值类似python,并不是像C++、JAVA等语言那样, x代表某个存储位置, “x <- c(1,2,3)”代表将1到3这些值存储到x所指向的存储位置。

<-右边的c(1,2,3)是一个表达式, 其结果为一个R对象(object), 而x只是一个变量名, 并没有固定的类型、固定的存储位置, 赋值的结果是将x绑定到值为(1,2,3)的R对象上。 R对象有值,但不必有对应的变量名; 变量名必须经过绑定才有对应的值和存储位置。

我们可以通过变量获得对象所在的地址(存储位置),并获得对象的值。

类似py 中的id,我们也可以通过tracemem 追踪变量指向的对象在内存中的位置:

  1. x <- c(1,2,3)
  2. cat(tracemem(x), "\n")
  3. ## <0000000018288290>
  4. y <- x # 这时y和x绑定到同一R对象
  5. cat(tracemem(y), "\n")
  6. ## <0000000018288290>
  7. y[3] <- 0 # 这时y制作了副本
  8. ## tracemem[0x0000000018288290 -> 0x00000000183c5190]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous> do.call eval eval eval eval eval.parent local
  9. x
  10. ## [1] 1 2 3
  11. y
  12. ## [1] 1 2 0
  13. untracemem(x); untracemem(y)

当使用tracemem 追踪变量后,如果变量绑定的对象发生了变化,会将变化结果输出到屏幕,如果不希望继续追踪,可以使用函数untracemem。

三种情况

对于调用函数时内部变量的赋值,分为三种情况。

1)在调用函数时, 如果函数内部不修改自变量的元素值, 输入的自变量并不制作副本, 而是直接被函数使用实参绑定的对象。 如:

  1. x <- c(1,2,3)
  2. cat(tracemem(x), "\n")
  3. ## <0000000018DA05A0>
  4. f <- function(v){ return(v) }
  5. z <- f(x)
  6. cat(tracemem(z), "\n")
  7. ## <0000000018DA05A0>
  8. untracemem(x); untracemem(z)

2)如果修改了内部变量的元素的值,则会制作副本,并将修改的副本输出:

  1. x <- c(1,2,3)
  2. cat(tracemem(x), "\n")
  3. ## <000000001931EE70>
  4. f2 <- function(v){ v[1] <- -999; return(v) }
  5. z <- f2(x)
  6. ## tracemem[0x000000001931ee70 -> 0x00000000193d8880]

3)如果在函数中对自变量重新赋值, 这实际是重新绑定, 也不会制作输入的实参的副本。

其他副本创建的类型

在上面的示例中, 用了基本类型的向量讲解是否制作副本。 考虑其它类型的复制。

  • 列表

如果x是一个有5个元素的列表, 则y <- x使得y和x指向同一个列表对象。 但是, 列表对象的每个元素实际上也相当于一个绑定, 每个元素指向一个元素值对象。 所以如果修改y:y[[3]] <- 0, 这时列表y首先被制作了副本, 但是每个元素指向的元素值对象不变, 仍与x的各个元素指向的对象相同; 然后, y[[3]]指向的元素值进行了重新绑定, 不再指向x[[3]], 而是指向新的保存了值0的对象, 但y的其它元素指向的对象仍与x公用。 列表的这种复制方法称为浅拷贝, 表格对象及各个元素绑定被复制, 但各个元素指向(保存)的对象不变。 这种做法节省空间也节省运行时间。 在R的3.1.0之前则用的深拷贝方法, 即复制列表时连各个元素保存的值也制作副本。

(python 中的copy 方法也是一种浅拷贝)

浅拷贝即是只复制列表对象的每一个第一层元素,而元素内部的对象则不会复制创建,而是继续指向本来的对象。

  • 数据框

如果x是一个数据框, 这类似于一个列表, 每个变量相当于一个列表元素, 数据框的每一列实际绑定到一个对象上。 如果y <- x, 则修改y的某一列会对y进行浅拷贝, 然后仅该列被制作了副本并被修改, 其它未修改的列仍与x共用值对象。

但是如果修改数据框y的一行, 因为这涉及到所有列, 所以整个数据框的所有列都会制作副本。

垃圾收集器

在当前的R语言中, 一个对象的引用(如绑定的变量名)个数, 只区分0个、1个或多个这三种情况。 在没有引用时, R的垃圾收集器会定期自动清除这些对象。 rm(x)只是删除绑定, 并不会马上清除x绑定的对象。 如果已经有多个引用, 即使是只有2个, 减少一个引用也还是“多个”状态, 不会变成1个。

垃圾收集器是在R程序要求分配新的对象空间时自动运行的, R函数gc()可以要求马上运行垃圾收集器, 并返回当前程序用道的存储量; lobstr包的mem_used()函数则报告当前会话内存字节数。

存储大小

用lobstr包的obj_size()函数可以求变量的存储大小, 如obj_size(x), 也可以求若干个变量的总大小, 如obj_size(x,y)。 因为各种绑定到同一对象的可能性, 所以变量的存储大小可能会比想象要少, 比如, 共用若干列的两个数据框, 字符型向量, 等等。 基本R软件的object.size()则不去检查是否有共享对象, 所以对列表等变量的存储大小估计可能会偏高。

  1. > lobstr::obj_size(a)
  2. 80 B

从R 3.5.0版开始,1:n这种对象仅保存其开始值和结束值。

在自定义函数时, 自变量通常是按引用使用的, 函数内部仅使用自变量的值而不对其进行修改时不会制作副本, 但是如果函数内部修改了自变量的值, 就会制作副本, 当自变量的存储很大而且返回调用这个函数时就会造成运行速度缓慢。 在函数内应慎重修改自变量的值。

在循环中修改数据框的列, 也会造成反复的复制。