44

在最近与同学的对话中,我一直主张避免使用全局变量,除了存储常量。这是一种典型的应用统计类程序,每个人都编写自己的代码,项目规模偏小,因此人们很难看到马虎的习惯带来的麻烦。

在谈论避免全局变量时,我将重点放在全局变量可能会造成麻烦的以下原因上,但我想在 R 和/或 Stata 中提供一些示例来遵循这些原则(以及您可能认为重要的任何其他原则) ),而且我很难想出可信的。

  • 非局部性:全局变量使调试变得更加困难,因为它们使理解代码流变得更加困难
  • 隐式耦合:全局变量通过允许远距离代码段之间的复杂交互来打破函数式编程的简单性
  • 命名空间冲突:公共名称(x、i 等)被重用,导致命名空间冲突

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

相关链接

全局变量不好

全局变量不好吗?

4

10 回答 10

29

我也有幸向没有编程经验的本科生教授 R。我发现的问题是,大多数全局变量不好的例子都相当简单,并没有真正理解重点。

相反,我试图说明最小惊讶原则。我使用了一些很难弄清楚发生了什么的例子。这里有些例子:

  1. 我要求全班写下他们认为的最终值是什么i

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

    有些班级猜对了。然后我问你应该写这样的代码吗?

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

  2. 以下代码返回什么:

    x = 5:10
    x[x=1]
    

    问题是我们到底是什么意思x

  3. 以下函数是返回全局变量还是局部变量:

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

    答:两者都有。再次讨论为什么这是不好的。

于 2011-04-03T08:41:09.877 回答
18

哦,全球的美妙气味......

这篇文章中的所有答案都给出了 R 示例,OP 也想要一些 Stata 示例。所以让我附和这些。

与 R 不同,Stata 确实关注其本地宏(您使用local命令创建的宏)的局部性,因此“这是一个全局 z 还是正在返回的本地 z?”的问题。永远不会出现。(天哪……如果不强制执行局部性,你们 R 人怎么能编写任何代码???)Stata 有一个不同的怪癖,即不存在的本地或全局宏被评估为空字符串,这可能是可取的,也可能不是可取的。

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

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

    sysuse auto, clear
    regress price $myvars
    

我怀疑这种结构的主要用途是用于在尝试多种规范时在交互式输入和将代码存储在 do-file 之间切换的人。假设他们尝试使用同方差标准误差、异方差标准误差和中值回归进行回归:

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

然后他们用另一组变量运行这些回归,然后用另一个变量,最后他们放弃并将其设置为一个do- myreg.dofile

    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定义(名称冲突)......男人,那将是一场灾难。

  1. 全局变量用于目录或文件名,如下所示:

    use $mydir\data1, clear
    

    只有天知道会装载什么。不过,在大型项目中,它确实派上用场。您可能想global mydir在您的主文件中定义某处,甚至可能是

    global mydir `c(pwd)'
    
  2. 全局变量可用于存储不可预测的废话,例如整个命令:

    capture $RunThis
    

    上帝只知道会被执行什么;让我们希望它不是! format c:\。这是隐式强耦合的最坏情况,但由于我什至不确定它RunThis是否包含任何有意义的东西,所以我capture在它前面放了一个,并准备处理非零返回码_rc。(但是,请参阅下面的示例。)

  3. Stata 自己对全局变量的使用是针对上帝设置的,例如 I 类错误概率/置信度级别:$S_level始终定义全局变量(您必须完全是个白痴才能重新定义这个全局变量,尽管它在技术上当然是可行的)。然而,这主要是版本 5 及以下(大致)代码的遗留问题,因为可以从不太脆弱的系统常量中获得相同的信息:

    set level 90
    display $S_level
    display c(level)
    

值得庆幸的是,全局变量在 Stata 中非常明确,因此很容易调试和删除。在上述某些情况下,当然在第一种情况下,您希望将参数传递给在 do-file 中被视为本地的 do-files `0'。而不是在myreg.do文件中使用全局变量,我可能会将其编码为

    unab varlist : `0'
    regress price `varlist'
    regress price `varlist', robust
    qreg    price `varlist'
    exit

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

在我见过的最糟糕的情况下,全局变量在定义后只使用了一次。

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

args lf $parameters

lf目标函数(对数似然)在哪里。我至少遇到过两次,在正常混合包(denormix)和验证性因子分析包(confa)中;当然,你可以findit同时使用它们。

于 2011-08-02T19:02:13.470 回答
12

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

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 中的实现方式,这无法真正得到纠正——任何事情都可能在你不知情的情况下改变它们,因此不能保证相同的代码块返回完全相同的对象。约翰·钱伯斯在他最近的书中哀叹这一特点

于 2011-04-02T22:45:12.440 回答
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)

这突出了您可以通过依赖不仅在函数范围内或作为参数显式传递的任何内容而陷入的混乱。

于 2011-04-04T08:35:28.380 回答
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 语言定义中。

于 2012-08-01T16:28:13.943 回答
5

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

.Random.seed <- 'normal'

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

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

rnorm(10)

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

我还使用以下示例:

x <- 27
z <- somefunctionthatusesglobals(5)

然后问学生值x是什么;答案是我们不知道。

于 2011-04-03T02:39:42.703 回答
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
于 2011-04-03T05:03:24.517 回答
3

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

设置

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

编码

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

标准

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

情况 2:f()不一定是行为正确的函数,它可能会使用全局分配。

答案

情况 1:代码不会返回错误,因为第一行检查没有x' 等于 0 并且第三行除以x

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

于 2011-10-26T18:05:35.403 回答
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

当然,这是一个微不足道的例子,复杂程序中不存在的任何例子也是如此。但希望它会引发关于跟踪全局变量比跟踪本地变量的难度的讨论。

于 2011-04-04T16:36:21.873 回答
0

R中,您还可以尝试向他们展示通常不需要使用全局变量,因为您可以通过仅更改环境来从函数本身内部访问函数范围内定义的变量。例如下面的代码

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"))
}
于 2011-04-02T22:54:13.873 回答