R如何找对象? - PART I

或者是……
如何将自己推进环境、命名空间、导入、导出、框架、封装、继承和函数调用的巨坑中?

(个人翻译,原文在全文页面底部)

动机

为何读此贴?

  1. 避免被坑
    迄今为止你都躲开了上述那些坑,但现在是时候入坑了(避免被坑的前提是熟悉它而不是碰巧躲开它)。不幸的是,你说的是人类语言,但R的帮助文档只说“原始C语言”(脑补一个80年代毛发旺盛的C语言程序员——极其聪明但是说话像含了一嘴腌萝卜一样——并不擅长沟通)。

  2. R很傻缺
    你的函数一度工作的很好,但现在却甩你一脸报错。当然,你的函数并没有做过任何修改,你只是模糊的记得报错前你装了一个新包,但是那有什么关系呢?可是,的确有关系……

  3. R找错对象
    你加载了一个包叫matlab然后调用sum()函数去计算一个矩阵。返回的结果是每列求和后的向量(base包里的colSums()函数的效果),而不是一个数值。脑海里奔腾过无数匹神兽。
    让R像Matlab一样运行,你想啥呢?

  4. 你想让R找点儿别的东西
    你喜欢一个包里的绘图函数。如果这个函数能直接被自己的计算调用就完美了。看上去像是黑魔法,但是用函数的完整功能来支持你的小把戏多少有点诡异。那么,暗黑艺术欢迎你。

  5. 写包
    你写了一个包,你的这个娃跟CRAN里其它的娃玩儿的开心伐?

R在哪儿存储对象?

R里所有的东西都存在于环境中。我们最终想解决的问题是,搞清楚什么对象存放于什么环境中,并且如何调用到它。
一个环境,也是R的一个对象。对象用于存储。环境比较特殊,只能用于存储两种东西:

  1. 框架(a frame)
    这是一个命名对象的集合。什么是命名对象?R里所有的东西都是一个对象——函数、数值、字符、逻辑值,等等。这些对象可以被命名为(比如)myFunctionmyNumericmyCharactermyLogical。当你在命令行输入myVar = "chirlie"时,你就创建了一个叫做”myVar”的字符型变量,这个变量里存储了”charlie”这个字符串。
    因为所有的对象都存储在环境中,框架就是环境存储这些对象的地方。刚刚你创建的myVar就存在于某个环境的框架中。

  2. 封装环境(the enclosing environment)
    一个环境的所有者即封装环境,它是对另一个环境的引用。
    img1

上图可以看出封装环境的链条止步于一个特殊环境,叫做空环境(empty environment)。你可以通过执行emptyenv()来获取这个对象。
对于一个给定的环境对象,你可以对其查询两件重要的事情:此环境的所有者(母环境、封装环境)还有它的框架下的对象。

对所有者和指针撒个小谎

笔者使用所有关系包含关系两个松散的概念。当我说到拥有包含的时候,那么我真的是在讨论指针。如果你对指针没有了解,那么你可以直接跳到下一个环节。

接下来我会将环境描述为拥有对象,尤其是拥有函数。事实上,函数就是存储在内存某处的指令,他们可以在查询表里检索,一旦找到,就会被指针指出内存所在位置。这篇文章的核心概念就是指针的不可知性以及对于精通R的搜索机制而言,指针并不是必须的。
事实上,R很努力的在隐藏指针。
对,技术上的不精确让我很痛苦,但我会尝试着让文章保持简单,毕竟还是有很多人能理解所有关系。我们有很多东西要谈。

跟环境玩一玩

创建一个环境(环境是个对象)

1
2
3
4
myEnvironment = new.env()
# 打印出来
myEnvironment

## <environment: 0x0000000006ce0920>

除空环境(R_EmptyEnv)外,任何环境都有个封装环境。myEnvironment的封装环境是什么?是R_GlobalEnv,用parent.env()函数来找到它。

1
parent.env(myEnvironment)
## <environment: R_GlobalEnv>

R_GlobalEnv的封装环境是什么?是一个叫package:stats的环境 (以个人电脑里包的安装顺序而定,每个人的可能有差异)。

1
parent.env(parent.env(myEnvironment))
## <environment: package:stats>
## attr(,"name")
## [1] "package:stats"
## attr(,"path")
## [1] "C:/R/R-2.14.1/library/stats"

R_GlobalEnv也可以由以下两种标识来提取。
.GlobalEnv和函数globalenv()R_GlobalEnv我们会稍后讨论。

1
parent.env(.GlobalEnv)

## <environment: package:stats>
## attr(,"name")
## [1] "package:stats"
## attr(,"path")
## [1] "C:/R/R-2.14.1/library/stats"
1
parent.env(globalenv())
## <environment: package:stats>
## attr(,"name")
## [1] "package:stats"
## attr(,"path")
## [1] "C:/R/R-2.14.1/library/stats"

emptyenv()获取空环境。

1
emptyenv()
## <environment: R_EmptyEnv>

为什么myEnvironment有个恶心的名字:0x0000000006ce0920?
那个名字只是环境在内存中的位置。我们可以对它的name属性(attribute)赋值,让它看起来不那么让人敬而远之。
然而R并不会用这个正常点儿的名字去替代原有的名字。我们可以用environmentName()函数去提取那个正常的名字。

1
2
attr(myEnvironment, "name") = "Cool Name"
myEnvironment
## <environment: 0x0000000006ce0920>
## attr(,"name")
## [1] "Cool Name"
## > environmentName(myEnvironment) 
## [1] "Cool Name"

然后我们创建一个数值变量。

1
myValue = 5

创建的对象默认存放在当前的环境下,可用environment()函数获取当前环境。

1
environment()
## <environment: R_GlobalEnv>

ls()可在当前环境下查询所属框架内的所有对象。这里我们确认了myEnvironmentmyValue都存在于当前的环境R_GlobalEnv中。

1
ls(envir = environment())
## [1] "myEnvironment" "myValue"      

当然我们也可以无视默认的存储行为,踩在当前环境的脸上把新创建的对象存储在其它环境中。我们用assign()函数去实现上述目标——在myEnvironment中创建一个叫”myLogical”的变量。
先用ls() 确认在赋值前,myEnvironment中没有任何东西。然后再赋值后再次用ls()去确认已经成功在myEnvironment中创建myLogical

1
ls(envir = myEnvironment)
## character(0)
1
2
assign("myLogical" , c(FALSE, TRUE), envir = myEnvironment)
ls(envir = myEnvironment)
## [1] "myLogical"

通过调用get()函数,我们可以从任何环境中获取任何对象。

1
get("myLogical" , envir = myEnvironment)
## [1] FALSE  TRUE

创建对象之前,我怎么知道myEnvironment的封装环境是R_GlobalEnv?
所以再强调一次,R以当前环境为默认存储环境。你可以使用parent.env()函数来替换一个环境的封装环境。

1
2
myEnvironment2 = new.env()
parent.env(myEnvironment2)
## <environment: R_GlobalEnv>
1
2
parent.env(myEnvironment2) = myEnvironment
parent.env(myEnvironment2)
## <environment: 0x0000000006ce0920>
## attr(,"name")
## [1] "Cool Name"

从另一个角度去理解“当前”或“本地”环境:我们新建一个函数,让它调用environment()去查询本地环境。当R执行一个函数时,它会自动为函数创建一个(子)环境。当变量或者对象在函数内部被创建时,他们只存在于函数内部的环境中。调用Test()函数去验证时,并不会返回R_GlobalEnv。我们没有在Test()内部创建任何对象。如果我们创建了,他们便会存在于0x0000000006ce9b58环境中。当函数完成运算,函数内部环境就会消失。

1
2
Test = function() { print(environment()) }
environment()
## <environment: R_GlobalEnv>
1
Test()
## <environment: 0x0000000006ce9b58>

当然我们也可以尝试一下返回函数中子环境的封装环境。

1
2
Test = function() { print(parent.env(environment())) }
Test()

## <environment: R_GlobalEnv>

简短的答案:R如何找对象

有跟着上面的代码运行一遍吗?从运行结果看上去,环境并不只是对象的静态仓库。R运行一段语句时,从事伴随着一个本地当前环境。简单点讲,就是一个当前被激活的环境(对立面是目前未被使用的未被激活的环境。或是更直白地说,语句总是在一个指定的环境内运行的。
也就是说,R可以在任何时间提一个问题(此处有翻译腔)“嘿,伙计,本地环境是什么?”,且R经常这么问。每当它需要找到一个命名变量的时候就需要问一次这个问题。
我们知道每当R运行一个函数他就需要创建一个心的本地环境,所以当我们优雅地跑程序时,函数会调用其它函数,而环境会随之滋生然后湮灭。

脑补一下我们在任意一段语句处冻结系统。当R搜寻那段语句中的一个名称时,它会首先在当前环境中进行查找。如果当前环境中找不到,R跑去其封装环境中找对象,以此类推。这就是R如何找对象的,它从本地环境开始遍历每一级封装环境,一级一级直至找到第一个出现对象名称的环境为止。

这次大家满足了吧?
No No No。我们继续。

世界地图

我们刚刚谈到的R找对象的方式,多少有点像一个限定了方向的寻宝游戏。要找到宝藏,我们只需要准备一个世界地图!

img2

第一次启动R,所有环境的状态如上图所示。每一个方框代表了一个唯一的环境,紫色实线代表了封装环境之间的关系。紫色虚线会稍后提到,暂时将它视作一种与封装环境类似的关系。

全局环境(the global environment)

R_GlobalEnv是一个特殊的环境。在上面的地图中,它是绿色的。绿色表示开始。精确地讲,全局环境是你启动R时的本地当前环境,如果你在命令行上进行赋值的操作,那么命名对象会被储存在R_GlobalEnv中。

ls()函数返回指定环境中定义的所有对象。下面的代码中我们用.GlobalEnv来指代全局环境。
我们会看到当我们刚刚启动R时,全局环境下没有任何对象。但是当我们给myVariable赋值后,全局环境中便包含了这个变量的名字。

1
ls(envir = .GlobalEnv)
## character(0)
1
2
myVariable = 0
ls(envir = .GlobalEnv)
## [1] "myVariable"

注意environment()函数。
当你看到它返回NULL的时候可能以为这是一个错误的结果。(然而并不是。)
查一下帮助文档,你会知道这个environment()只以函数为输入参数。
myVariable是一个数值变量,不是函数。
environment()的目的并不是告诉你一个对象的封装环境是什么。

1
environment(myVariable)
## NULL

搜索列表(the search list)

搜索列表是从R_GlobalEnvR_EmptyEnv的封装环境链条。在上面的世界地图里,你可以将它视作一条高速公路。R从R_GlobalEnv出发在这条路上行驶。所有的道路都最终汇集到这条高速公路上。你可以在命令行输入search()从而得到这个链条。

1
search
## [1] ".GlobalEnv"        "package:stats"   "package:graphics" "package:utils" "package:datasets" 
## [6] "package:grDevices" "package:methods" "Autoloads"        "package:base"

包 vs. 命名空间 vs. 导入项 vs. 环境

package vs. namespace vs. imports vs. environment

凝视我们那张世界地图一会儿,你会发现一个美女从图中显现……
阿不,这不是3d图……
你会注意到每个R的包都有3个关联的环境。如果你觉得这让人很疑惑,那么恭喜你,你还是正常人类。我第一次碰到这个关联的时候,差点儿没被它逼疯。
不过相信我,唯一吊诡的地方就是命名规则,这三个关联非常有用且设计良好。

img3

让我们从左到右分解:

  1. 包的环境(package environment)
    包内导出的对象存储在此环境中。简单来讲,这些对象是包的作者希望你看到的,且它们大多数都是函数。一个典型的已经发布的包会提供特定主题或者领域相关的函数。就传统的OOP(Object Oriented Programming)而言,这与”public”类型或方法功能相近。

  2. 命名空间的环境(namespace environment)
    包内所有的对象均存储在此环境中。其中也包括那些包的作者并不想让终端用户接触到的“隐藏”对象(并不是真正的被隐藏,如果你想获取,仍然可以接触到)。这些对象用于支持那些“可见”的对象。
    比如,HardCalculation()函数可能会使用到MakeResultsPretty()的处理复杂的文本格式的功能。但是作者并不想让你直接调用后者,后者唯一的功能就是给HardCalculation()傲娇的输出结果赋予特定的格式。这与OOP中的”private”或”internal”的类型或方法功能相近。
    等等,包的作者想让我看见的对象,既存在与包的环境又存在于命名空间的环境中?
    是又不是。
    是,两个环境均有一个框架列举了有同样名字的对象;不是,并同时不存在两个同样的对象集合。两个环境都有一个指向同一函数的指针。如果你看不懂,那就姑且认为有两份同样的对象集合好了。这个安排看上去很诡异,但是这种安排的用处马上就会显现出来。这也是为什么查询一个对象所处的环境并不是那么容易——有可能拥有同一对象的环境有两个甚至两个以上。

  3. 导入项环境(imports environment)
    这个环境存储着从其它包中导入的对象,这些包用于支持当前包的正常使用。在CRAN上发布的包大多不是孤立的,它们基于其它包提供的功能编写而成。以ggplot2为例,在CRAN的页面上,”Imports”区块中这个包要求plyr及其它包(原文截图于2012年)。那么imports::ggplot2环境便包含了plyr包中所有的对象。
    img4

导入项(imports) vs. 依赖项(depends)

上面的截图里,DependsImports很让人疑惑。如果Imports声明了一个包对其它包的需求,那Depends是用来干啥的?这是一个糟糕的命名规则。Depends同样也列举了ggplot2需要的包。两者的差别是我们在地图上的何处放置这些需求。地图限定了R寻找对象的路径,所以在Imports或者Depends中指定需求,对R寻找依赖包来说有不同的结果。

如果依赖包是在Imports中指定,那么这个包中的内容将会存储于“导入项”环境中。在ggplot2的例子中,plyr的对象会出现在imports:ggplot2环境中。要注意的是plyr并没有生成一个独立的环境,它漂亮的将自己隐藏在了imports::ggplot2环境中。下图紫色的虚线我们会在后面提到。

img5

如果依赖包是在Depends种指定(比如reshape包),那么这个包的载入方式就像你在命令行输入library()require()的效果一样。也就是说,依赖包的包、命名空间以及导入项这三个环境都会被创建。reshape包在ggplot2前被加载,package:reshape环境变成package:ggplot2的封装环境。

img6

那么我们可以在DependsImports间任意选择吗?No No No…
library()命令(或者更广泛的说加载一个库)将包的环境置于R_Global之下。更精确地说,加载包的环境变成R_Global的封装环境。R_Global的旧有封装环境现在封装加载包的环境。下图中你能看到这点。下图中的reshape2是原有的reshape包的重写/升级版。

reshapereshape2均包含了cast()函数。我们假设ggplot2有一个函数(其实并没有)叫FunctionThatCallsCast()。你可以猜到,这个函数能调用cast()函数。不用深究R怎么去找对象,我们仅仅需要跟着那条“紫色线路”走。我们从1走到2并找到FunctionThatCallsCast()函数。
提醒:包的环境和命名空间的环境都引用了一个公共函数。
运行它就需要找到cast()函数,我们从3走到5最后在6找到了它并停止检索。但这并不是我们想要寻找的那个cast()。这是reshape2cast(),但ggplot2需要的是reshape包里的那个。这种错误的调用可能会导致很可怕的后果。

img7

更好的解决方案是遵循Imports特征,直接在imports:ggplot2中找到reshapecast(),那样的话,我们只要走到3就可以停下。现在,你应该明白了为什么ImportsDepends间的选择并不是随意的。
由于CRAN上已经有太多的包,不同包的函数拥有同样名字的情况已经司空见惯。Depends现在看起来已经不那么安全,它让自己的包更容易被其它加载的包所攻击(同名函数被覆盖)。
译者注:下图是2016年1月23日的ggplot2包的截图,可以看到帮助文档里已经取消了Depends项中除R本身以外所有的依赖项。

img4bis

namespace:base

还有一个事实就是,所有的imports:<name>环境均以namespace:base作为其封装环境,就把它当做是创建一个包的赠品吧。由于基础函数被调用的频率最高,base包被绝大多数包设置为Depends或者Imports。没有namespace:base的话,R可能要走很远的路去找到package:base
基础函数跟其它包中重名的几率也很大,而包的作者既不可能预先知道谁会载入他的包,也不可能知道你决定写一个你自己的基础函数,所以,包的作者都希望R能在Imports后马上找到基础函数,不允许有任何误用。

(未完待续)

原文

How R Searches and Finds Stuff