R和Stata中全局变量危险的例子

时间:2011-04-02 22:29:35

标签: r scope global-variables stata

在最近与同学的对话中,我一直主张避免全局,除了存储常量。这是一种典型的应用统计类型程序,每个人编写自己的代码和项目大小都很小,所以人们很难看到由于草率的习惯造成的麻烦。

在讨论避免使用全局变量时,我会关注以下为什么全局变量可能导致麻烦的原因,但我想在R和/或Stata中有一些示例< / strong>遵循原则(以及您可能认为重要的任何其他原则),并且我很难想出可信的原则。

  • 非本地化:Globals使调试更难,因为它们更难理解代码流
  • 隐式耦合:Globals通过允许远程代码段之间的复杂交互来打破函数式编程的简单性
  • 命名空间冲突:重用通用名称(x,i等),导致命名空间冲突

这个问题的一个有用的答案是一个可重现且自包含的代码片段,其中全局变量会导致特定类型的麻烦,理想情况下是另一个代码片段,其中问题得到纠正。如有必要,我可以生成更正的解决方案,因此问题的例子更为重要。

相关的链接

Global Variables are Bad

Are global variables bad?

10 个答案:

答案 0 :(得分:27)

我也很乐意为没有编程经验的本科生教授R.我发现的问题是大多数全局变量很糟糕的例子都相当简单,并没有真正说明问题。

相反,我尝试说明the principle of least astonishment。我用一些例子来弄清楚发生了什么。以下是一些例子:

  1. 我要求全班写下他们认为i的最终价值的内容:

    i = 10
    for(i in 1:5)
        i = i + 1
    i
    

    有些课程正确猜测。然后我问你应该写这样的代码吗?

    在某种意义上,i是一个正在改变的全局变量。

  2. 以下代码返回的是什么:

    x = 5:10
    x[x=1]
    

    问题是x

  3. 究竟是什么意思
  4. 以下函数是返回全局变量还是局部变量:

     z = 0
     f = function() {
         if(runif(1) < 0.5)
              z = 1
         return(z)
      }
    

    答案:两者。再次讨论为什么这很糟糕。

答案 1 :(得分:16)

哦,精彩的smell全局变量......

本文中的所有答案都给出了R示例,OP也想要一些Stata示例。所以,让我来谈谈这些。

与R不同,Stata会处理其本地宏(使用local命令创建的本地宏)的位置,因此问题是“这是一个全局z还是正在返回的本地z ?” 从不出现。 (天哪......如果没有强制执行地点,你怎么能写任何代码?)Stata有一个不同的怪癖,即不存在的本地或全局宏被评估为空字符串,可能是也可能不是。

我看过使用全局变量有几个主要原因:

  1. 全局变量通常用作变量列表的快捷方式,如

    sysuse auto, clear
    regress price $myvars
    

    我怀疑这种构造的主要用途是为那些在交互式打字和在do文件中存储代码的人进行切换,因为他们尝试了多种规范。假设他们尝试使用同方差标准误差,异方差标准误差和中位数回归进行回归:

    regress price mpg foreign
    regress price mpg foreign, robust
    qreg    price mpg foreign
    

    然后他们用另一组变量运行这些回归,然后用另一组变量运行,最后他们放弃并将其设置为带有

    的文件myreg.do
    regress price $myvars
    regress price $myvars, robust
    qreg    price $myvars
    exit
    

    伴随着全球宏的适当设置。到现在为止还挺好;摘录

    global myvars mpg foreign
    do myreg
    

    产生理想的结果。现在让我们说他们通过电子邮件发送他们着名的do文件,声称可以为协作者提供非常好的回归结果,并指示他们输入

    do myreg
    

    他们的合作者会看到什么?在最好的情况下,mpg的平均值和中位数,如果他们开始一个新的Stata实例(失败的耦合:myreg.do并不真的知道你打算使用非空变量列表来运行它。但是,如果合作者有一些工作,并且也有一个全球myvars定义(名称冲突)...男人,那将是一场灾难。

  2. 全局变量用于目录或文件名,如:

    use $mydir\data1, clear
    

    上帝只知道将要装载什么。但是,在大型项目中,它确实很方便。您可能希望在主文件中的某处定义global mydir,甚至可能是

    global mydir `c(pwd)'
    
  3. Globals可用于存储不可预测的垃圾,就像整个命令一样:

    capture $RunThis
    

    上帝只知道将要执行什么。这是隐式强耦合的最坏情况,但由于我甚至不确定RunThis是否包含任何有意义的东西,我在它前面放了capture,并准备好对待非零返回码_rc。 (但请参阅下面的示例。)

  4. Stata自己使用全局变量是为了神的设置,就像I型错误概率/置信度一样:全局$S_level总是被定义(你必须是一个完全重新定义这个全局的白痴,尽管当然,这在技术上是可行的)。然而,这主要是版本5及以下(大致)代码的遗留问题,因为相同的信息可以从不太脆弱的系统常数中获得:

    set level 90
    display $S_level
    display c(level)
    
  5. 值得庆幸的是,在Stata中全局变量非常明确,因此很容易调试和删除。在上述某些情况中,当然在第一种情况下,您需要将参数传递给do文件,这些文件在do文件中被视为本地`0'。我可能会将其编码为

    ,而不是在myreg.do文件中使用全局变量
        unab varlist : `0'
        regress price `varlist'
        regress price `varlist', robust
        qreg    price `varlist'
        exit
    

    unab事件将作为保护元素:如果输入不是合法的varlist,程序将停止并显示错误消息。

    在我见过的最糟糕的情况中,全局在被定义后仅被使用过一次。

    有时你确实想要使用全局变量,因为否则你必须将血腥的东西传递给其他所有的文件或程序。我发现全局变量几乎不可避免的一个例子是编码最大似然估计,我事先并不知道我将拥有多少个方程和参数。 Stata坚持认为(用户提供的)可能性评估器将具有特定的方程式。所以我不得不在全局变量中累积我的方程式,然后在Stata需要解析的语法描述中用全局变量调用我的求值器:

    args lf $parameters
    

    其中lf是目标函数(对数似然)。我在正常混合物包(denormix)和验证性因子分析包(confa)中至少遇到过两次这种情况;当然,你可以findit两个。

答案 2 :(得分:11)

分析意见的全局变量的一个R示例是将数据读入R或创建数据框的stringsAsFactors问题。

set.seed(1)
str(data.frame(A = sample(LETTERS, 100, replace = TRUE),
               DATES = as.character(seq(Sys.Date(), length = 100, by = "days"))))
options("stringsAsFactors" = FALSE)
set.seed(1)
str(data.frame(A = sample(LETTERS, 100, replace = TRUE),
               DATES = as.character(seq(Sys.Date(), length = 100, by = "days"))))
options("stringsAsFactors" = TRUE) ## reset

由于在R中实现选项的方式,这无法真正得到纠正 - 任何事情都可能在您不知情的情况下改变它们,因此不能保证相同的代码块返回完全相同的对象。 John Chambers在他最近的book中哀叹这个功能

答案 3 :(得分:8)

R中的一个病态示例是使用R中可用的一个全局变量pi来计算圆的面积。

> r <- 3
> pi * r^2
[1] 28.27433
> 
> pi <- 2
> pi * r^2
[1] 18
> 
> foo <- function(r) {
+     pi * r^2
+ }
> foo(r)
[1] 18
> 
> rm(pi)
> foo(r)
[1] 28.27433
> pi * r^2
[1] 28.27433

当然,可以通过强制使用foo()来防御性地编写函数base::pi,但除非打包并使用NAMESPACE,否则这些追索可能无法在普通用户代码中使用:

> foo <- function(r) {
+     base::pi * r^2
+ }
> foo(r = 3)
[1] 28.27433
> pi <- 2
> foo(r = 3)
[1] 28.27433
> rm(pi)

这突出了你可以依靠任何不仅仅在你的函数范围内或明确作为参数传递的东西而陷入困境。

答案 4 :(得分:8)

这是一个有趣的病态示例,涉及替换函数,全局赋值和x在全局和本地定义......

x <- c(1,NA,NA,NA,1,NA,1,NA)

local({

    #some other code involving some other x begin
    x <- c(NA,2,3,4)
    #some other code involving some other x end

    #now you want to replace NAs in the the global/parent frame x with 0s
    x[is.na(x)] <<- 0
})
x
[1]  0 NA NA NA  0 NA  1 NA

替换函数使用由[1] 1 0 0 0 1 0 1 0的本地值返回的索引,而不是返回is.na(x),即使您正在分配x的全局值。此行为在R语言定义中为documented

答案 5 :(得分:5)

R中一个快速但令人信服的例子是运行如下行:

.Random.seed <- 'normal'

我选择'正常'作为某人可能选择的东西,但你可以在那里使用任何东西。

现在运行使用生成的随机数的任何代码,例如:

rnorm(10)

然后你可以指出任何全局变量都可能发生同样的事情。

我也使用了以下例子:

x <- 27
z <- somefunctionthatusesglobals(5)

然后问学生x的价值是多少;答案是我们不知道。

答案 6 :(得分:5)

通过反复试验,我了解到我需要非常明确地命名我的函数参数(并确保在开始和函数中进行足够的检查),以使一切尽可能健壮。如果您将变量存储在全局环境中,但是您尝试使用自定义贵重物品调试函数,则尤其如此 - 并且某些内容无法添加!这是一个结合不良检查和调用全局变量的简单示例。

glob.arg <- "snake"
customFunction <- function(arg1) {
    if (is.numeric(arg1)) {
        glob.arg <- "elephant"
    }

    return(strsplit(glob.arg, "n"))
}

customFunction(arg1 = 1) #argument correct, expected results
customFunction(arg1 = "rubble") #works, but may have unexpected results

答案 7 :(得分:3)

今天尝试教这个时出现的示例草图。具体来说,这侧重于试图给出关于为什么全局变量可能导致问题的直觉,因此它尽可能抽象地试图说明只能从代码中得出什么和不能得出结论(将函数留作黑盒子)。

设置

这是一些代码。根据给定的标准确定是否会返回错误。

代码

stopifnot( all( x!=0 ) )
y <- f(x)
5/x

标准

案例1:f()是一个行为正常的函数,它只使用局部变量。

案例2:f()不一定是行为正常的函数,可能会使用全局赋值。

答案

案例1:代码不会返回错误,因为第一行检查没有x等于零,第三行除以x

案例2:代码可能会返回错误,因为f()可以例如从x中减去1并将其分配回父环境中的x,其中任何等于1的x元素可以设置为零,第三行将返回除以零错误。

答案 8 :(得分:2)

这是对一个对统计类型有意义的答案的尝试。

  • 命名空间冲突:重用通用名称(x,i等),导致命名空间冲突

首先我们定义一个对数似然函数,

logLik <- function(x) {
   y <<- x^2+2
   return(sum(sqrt(y+7)))
}

现在我们编写一个不相关的函数来返回输入的平方和。因为我们很懒,所以我们会把它作为全局变量传递,

sumSq <- function() {
   return(sum(y^2))
}

y <<- seq(5)
sumSq()
[1] 55

我们的对数似然函数似乎完全符合我们的预期,接受一个参数并返回一个值,

> logLik(seq(12))
[1] 88.40761

但是我们的其他功能是什么呢?

> sumSq()
[1] 633538

当然,这是一个简单的例子,复杂程序中不存在的任何例子都是如此。但希望它能引发一场关于跟踪全局比跟当地人更难的讨论。

答案 9 :(得分:0)

R 中,您也可以尝试向他们展示通常无需来使用全局变量,因为您可以访问函数范围中定义的变量< / strong>来自 功能本身只需更改环境。例如下面的代码

zz="aaa"
x = function(y) { 
     zz="bbb"
     cat("value of zz from within the function: \n")
     cat(zz , "\n")
     cat("value of zz from the function scope: \n")
     with(environment(x),cat(zz,"\n"))
}