词法作用域

上一节我们介绍了复制—修改机制,示例中给出了触发该机制的两种情况。当一个对
象有多个名称或作为参数传递给函数时,修改这个对象会先复制它,然后再修改它的副本。
对于在函数外修改对象,我们介绍了<<- 的用法,首先找到函数外的变量,然后修改
该对象而不是局部复制。这就引出了一个重要的概念,即函数有内部和外部之分。在函数
内部,我们能够以某种方式引用外部的变量和函数。
例如,下列函数就使用了两个外部变量:
start_num <- 1
end_num <- 10
fun1 <- function(x) {
c(start_num, x, end_num)
}
首先,我们创建了两个变量(start_num 和end_num),并定义了一个名为 fun1 的函数。
此函数将变量 start_num、参数 x 和 end_num 组合成一个新的向量。很明显,start_num
和end_num 不是在函数内部而是在函数外部定义的,x 是函数的参数。我们看看函数能否运行:
fun1(c(4, 5, 6))
## [1] 1 4 5 6 10
结果显示函数成功获取了两个函数外部变量的值。你可能会认为我们在定义函数时两
个值就已经存在,所以 fun1 中的 start_num 和 end_num 直接从函数外部获取值。事实
上,我们可以通过两个试验证明这种想法是错误的。
第 1 个试验很简单,先移除这两个变量,再调用函数:
rm(start_num, end_num)
fun1(c(4, 5, 6))
## Error in fun1(c(4, 5, 6)): 找不到对象'start_num'
此时,函数便不能运行。如果在定义函数时就得到了两个变量的值,移除它们不应该
影响函数的运行。
第 2 个试验从另一个方向考虑。同时移除函数和两个变量(因为上述代码已经移除了
变量,所以这里只需再将函数 fun1( )移除即可)。然后定义函数:
rm(fun1, start_num, end_num)
## Warning in rm(fun1, start_num, end_num): object 'start_num'
## not found
## Warning in rm(fun1, start_num, end_num): object 'end_num'
## not found
fun1 <- function(x) {
c(start_num, x, end_num)
}
如果函数的创建必须要获取两个已经不存在的变量,那么上述代码的结果应该会报错,
因为找不到 start_num 和 end_num。结果很明显,并没有报错,函数也成功创建了。现
在调用它:
fun1(c(4, 5, 6))
## Error in fun1(c(4, 5, 6)): 找不到对象'start_num'
因为没有找到两个变量,函数不能运行。接着我们定义两个变量并再次调用此函数:
start_num <- 1
end_num <- 10
fun1(c(4, 5, 6))
## [1] 1 4 5 6 10
函数又可以运行了。至此我们可以得出结论,函数在被调用时才试图寻找变量。实际
上,函数在执行过程中遇到一个符号时,首先会在函数内部寻找。更具体地说,如果一个
符号作为参数传递到函数内或在函数内创建,它就会被解析并且其值也会被使用。
假设我们先创建一个变量 p,然后定义函数 fun2( ),在函数内部创建另一个变
量 p 并且出现在返回值表达式里:
p <- 0
fun2 <- function(x) {
p <- 1
x + p
}
当调用函数时,返回值表达式 x+p 会用到哪一个 p 呢?我们来揭晓答案:
fun2(1)
## [1] 2
从结果可以很明显地看出: x+p 使用了函数内部定义的 p 的值。过程很简单:首先,
p <- 1 创建了一个值为 1 的变量 p 而不是修改函数外部的 p;然后计算 x+p 表达式,其
中 x 取值为输入参数的值,p 为刚刚定义的局部变量。规则就是只有函数内部不存在某个
变量时才会在外部搜索该变量。
那“外部”究竟是什么意思呢?这个问题看起来更加微妙。假设我们创建下面两个函数:
f1 <- function(x) {
x + p
}
g1 <- function(x) {
p <- 1
f1(x)
}
第 1 个函数 f1( ) 简单地将两个变量相加求和:x 是一个参数,p 是一个外部变量。
第 2 个函数 g1( ) 在内部定义了一个变量 p,然后调用 f1( )。那么问题来了,调
用 g1( ) 时,f1( ) 会在 g1( ) 内部找到 p 么?
g1(0)
## [1] 0
可惜的是,即使 f1( ) 在 g1( ) 内部被调用,还是不能找到在 g1( ) 内部的 p。如
果我们先定义 p 再调用 g1( ),结果就不一样了:
p <- 1
g1(0)
## [1] 1
当 f1( ) 被调用且在其内部找不到 p 时,它就会在自己被定义的地方而不是被调用的
地方搜索,并且搜索到了 p 的值,所以函数 g1( ) 得以运行,这个机制被称作词法作用域。
在前一段代码中,我们定义 p 和定义 f1( ) 的地方是同一个域,所以,在 g1( ) 内部调
用 f1( ) 时,f1( ) 便能在同一个域中找到 p。
相同作用域的规则也适用于<<- 寻找变量的机理。例如,下列代码在同一个域内定义
了一个变量 m 和两个函数 f2( ) 和 g2( )。在 f2( ) 中,m 被设置为 2,但在 g2( ) 中
定义一个局部变量 m,然后又调用 f2( ):
m <- 1
f2 <- function(x) {
m <<- 2
x
}
g2 <- function(x) {
m <- 1
f2(x)
cat(sprintf("[g2] m: %d\n", m))
}
只要调用了 g2( ),系统就会输出 g2( ) 中的 m 的值。我们调用 g2( ) 来看看会发
8.3 词法作用域 231
生什么:
g2(1)
## [g2] m: 1
输出结果显示 g2( ) 中的 m 的值没有改变,但是可以证实 f2( ) 和 g2( ) 外部
的 m 的值发生了改变:
m
## [1] 2
前面的试验证明 m <<- 2 服从词法作用域规则。
下面的两个例子看起来更复杂,其函数是嵌套的。我们在 f( ) 中不仅创建了局部变
量 p 和 q,还创建了一个局部函数 f2( ),在 f2( ) 中又定义了一个局部变量 p:
f <- function(x) {
p <- 1
q <- 2
cat(sprintf("1. [f1] p: %d, q: %d\n", p, q))
f2 <- function(x) {
p <- 3
cat(sprintf("2. [f2] p: %d, q: %d\n", p, q))
c(x = x, p = p, q = q)
}
cat(sprintf("3. [f1] p: %d, q: %d\n", p, q))
f2(x)
}
如果理解了词法作用域,那么输入任意一个 x 就应该知道此函数的返回结果。为了更
方便地追踪每层域中变量的值,我们使用了 cat( )函数。cat( )返回的信息包含了次序、
函数域以及 p 和 q 的值。现在我们来运行 f(0),可以尝试预测一下结果:
f(0)
## 1. [f1] p: 1, q: 2
## 3. [f1] p: 1, q: 2
## 2. [f2] p: 3, q: 2
## x p q
## 0 3 2
3 个 cat( )函数的运行顺序为 1、3、2,每一个域的 p 和 q 的值都符合词法作用域规
则。下面的示例中我们将用到<<-:
g <- function(x) {
p <- 1
q <- 2
cat(sprintf("1. [f1] p: %d, q: %d\n", p, q))
g2 <- function(x) {
p <<- 3
p <- 2
cat(sprintf("2. [f2] p: %d, q: %d\n", p, q))
c(x = x, p = p, q = q)
}
cat(sprintf("3. [f1] p: %d, q: %d\n", p, q))
result <- g2(x)
cat(sprintf("4. [f1] p: %d, q: %d\n", p, q))
result
}
可以通过预测其运行顺序和输出变量的值来分析函数的流程:
g(0)
## 1. [f1] p: 1, q: 2
## 3. [f1] p: 1, q: 2
## 2. [f2] p: 2, q: 2
## 4. [f1] p: 3, q: 2
## x p q
## 0 2 2
如果没能成功预测上述函数的运行结果,那就再仔细看一看本节中的示例。

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