113-R编程16-R的内部机制1

2022-04-05 15:34:44 浏览数 (1)

  • 参考:
    • R的内部机制 - 王诗翔 (shixiangwang.github.io)[1]
    • 19 函数进阶 | R语言教程 (pku.edu.cn)[2]

前言

其实之前读了李东风老师的内容,感觉收获颇丰;但因为自己的业务逻辑过于简单,渐渐又荒废掉了。

最近碰巧看到王诗翔的这篇文章,再次学习,顺便将笔记从yuque 发至公众号。共勉。

因为内容有些多,拆成两个部分介绍。

第一部分:

  • 惰性求值 (Lazy evaluation)
  • 词法作用域 (Lexical scoping)

惰性求值

这里引用李东风老师的原话:

★R函数在调用执行时, 除非用到某个形式变量的值才求出其对应实参的值。这一点在实参是常数时无所谓, 但是如果实参是表达式就不一样了。形参缺省值也是只有在函数运行时用到该形参的值时才求值。 ”

这个特点可以用下面的例子理解,比如行参的默认值不是常量而是一个表达式:

代码语言:javascript复制
f <- function(x, y=ifelse(x>0, TRUE, FALSE)){
  x <- -111
  if(y) x*2 else x*10
}
f(5)

这时候的输出结果并不是,10,而是-1100。

这是因为形参y 并没按x=5 被赋值为TRUE, 而是到函数体中第二个语句才被求值, 这时x 的值已经变成了-111, 故y的值是FALSE。

同样利用这个特性,如果形参在函数主体中并没有被使用,则程序也不会报错:

代码语言:javascript复制
test0 <- function(x, y) {
    if (x > 0) x else y
}

test0(1)
#> [1] 1

我们可以简单的验证一下:

代码语言:javascript复制
test0(1, stop("Stop Now!"))
#> [1] 1
test0(-1, stop("Stop Now!"))
#> Error in test0(-1, stop("Stop Now!")): Stop Now!

程序只在第二个语句中起作用了,也就是满足y 在函数中被调用的条件,x 不大于0 的情况。

为了记录形参是否在主体中被使用,在函数内部, 用missing(x) 对形参x判断用户是否没有提供对应的实参, 对位置形参和有缺省值的形参都适用。

代码语言:javascript复制
> a <- function(x=3,y) missing(x)
> a(1,3)
[1] FALSE
> a(x = 1,3)
[1] FALSE
> a(3)
[1] FALSE
> a(y=3)
[1] TRUE

其判断用户是否没有提供对应的实参,如果提供了则为F,没有则是T。

懒惰求值的好处是,实现了我们程序的按需分配。

比如说有人上来就要十万块钱的烤肉饭,他连钱都没掏出来,我凭什么给他做呢?

代码语言:javascript复制
> test0 <- function(x, y) {
      if (x > 0) x else y
  }
> system.time(test0(1, rnorm(10000000)))
用户 系统 流逝 
   0    0    0 
   
> system.time(rnorm(10000000))
 用户  系统  流逝 
0.695 0.033 0.740 

因为只有在函数运行时用到该形参的值时才求值,所以如果参数表达式语法上没有问题,而实际值存在问题,也只有在调用该函数时才会发生报错:

代码语言:javascript复制
test3 <- function(x, n=floor(length(m) / 2)){
    x[1:n]
}

test3(1:10)
#> Error in test3(1:10): object 'm' not found

不过这种tradeoff,个人觉得牺牲并不算太大,还是蛮赚的。

词法作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

这里举一个例子:

代码语言:javascript复制
x <- -1
f0 <- function(x){
  f1 <- function(){
    x   100
  }
  f1()
}

R语言允许在函数体内定义函数,其中内嵌的函数f1() 称为一个closure(闭包)。

内嵌的函数体内在读取某个变量值时, 如果此变量在函数体内还没有被赋值,它就不是局部的,会向定义的外面一层查找;外层一层找不到,就继续向外查找,直到找到为止,如果在global 环境中还没有该变量,则会抛出异常。

上面例子f1()定义中的变量x不是局部变量, 就向外一层查找, 找到的会是f0的自变量x,而不是全局空间中x。

代码语言:javascript复制
f0(1)
## [1] 101

这样的变量查找规则叫做动态查找。即函数运行中需要使用某个变量时, 从其定义时的环境向外层逐层查找, 而不仅仅只是在调用时的环境中查找。

再看看下面的例子:

代码语言:javascript复制
f0 <- function(){
  f1 <- function(){
    x <- -1
    f2 <- function(){
      x   100
    }
    f2()
  }
  x <- 1000
  f1()
}
f0()

请思考0.00001ms,答案是多少?

其中f2()运行时, 用到的xf1()函数体内的局部变量x=-1, 而不是被调用时f0()函数体内的局部变量x=1000, 所以结果是-1 100 = 99

代码语言:javascript复制
f0 <- function(){
  f1 <- function(){
    x <- -1
    f2 <- function(){
      x   100
    }
    x <- 99
    f2()
  }
  x <- 1000
  f1()
}
f0()

此时的运行结果就是99,这是因为,在调用f2之前,其只会访问变量x 的当前值99,而不是历史值-1

“句法作用域”指的是函数调用变量时,查找其定义时的变量对应的存储空间, 而不是定义时变量所取的历史值。

函数运行时在找到某个变量对应的存储空间后, 会使用该变量的当前值,而不是函数定义的时候该变量的历史值。

简单来说,就是越靠近函数的变量,才是函数使用的那个变量。

有时我们还会讨论到函数作用域,也即在函数的内部,我们能够使用外部变量和函数,但外部不能使用内部变量和函数(除非使用<<-创建全局变量)。可以参考:[[122-R编程19-赋值运算符]] 此外,函数每一次运行都会刷新其内部的子环境。

参考资料

[1]

R的内部机制 - 王诗翔 (shixiangwang.github.io): https://shixiangwang.github.io/home/cn/post/2019-11-20-r-mechanism/

[2]

19 函数进阶 | R语言教程 (pku.edu.cn): https://www.math.pku.edu.cn/teachers/lidf/docs/Rbook/html/_Rbook/p-advfunc.html#p-advfunc-lazy

0 人点赞