【问题标题】:Examples of the perils of globals in R and StataR 和 Stata 中全局变量的危险示例
【发布时间】:2011-07-28 10:24:10
【问题描述】:

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

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

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

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

相关链接

Global Variables are Bad

Are global variables bad?

【问题讨论】:

    标签: r scope global-variables stata


    【解决方案1】:

    哦,很棒的全局变量smell...

    这篇文章中的所有答案都提供了 R 示例,OP 也想要一些 Stata 示例。所以让我来谈谈这些。

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

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

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

      sysuse auto, clear
      regress price $myvars
      

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

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

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

      regress price $myvars
      regress price $myvars, robust
      qreg    price $myvars
      exit
      

      伴随着适当的全局宏设置。到目前为止,一切都很好; sn-p

      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. 全局变量可用于存储不可预知的废话,就像一个完整的命令:

      capture $RunThis
      

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

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

      set level 90
      display $S_level
      display c(level)
      

    幸运的是,全局变量在 Stata 中非常明确,因此很容易调试和删除。在上述某些情况下,当然在第一种情况下,您希望将参数传递给 do-file,这些 do-file 被视为 do-file 中的本地 `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他们两个。

    【讨论】:

    • 漂亮的答案 StasK。谢谢你。在您的解释中,我认识到我自己的很多 Stata 痛苦。就个人而言,尤其是在宏可能包含半命令和其他疯狂的 Stata 中,我尝试坚持 Code Complete 建议并仅使用它们来代替常量。
    【解决方案2】:

    这是一个有趣的病态示例,涉及替换函数、全局分配以及全局和本地定义的 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

    【讨论】:

      【解决方案3】:

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

      设置

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

      代码

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

      标准

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

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

      答案

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

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

      【讨论】:

        【解决方案4】:

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

        • 命名空间冲突:公共名称(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
        

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

        【讨论】:

          【解决方案5】:

          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
          

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

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

          这凸显了依赖任何不只是在函数范围内或作为参数显式传递的东西可能会陷入的混乱。

          【讨论】:

            【解决方案6】:

            我也有幸向没有编程经验的本科生教授 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. 以下函数是返回全局变量还是局部变量:

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

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

            【讨论】:

            • 这些都很聪明,但我真的喜欢这里的#3,因为它为更广泛地讨论范围和返回值提供了一个很好的入口。
            • #2 中的 x=1 是一个错误,还是故意分配给意外的可怜的本科生发呆和迷惑?
            • @RomanLuštrik 我遇到了这个例子,当时一个贫穷、困惑的本科生提交了他们的作业。我只是用它来让人思考。他们认为正在发生什么?
            • 对#2 的有趣修改:x&lt;-5:10;x[xx 现在在基本环境中为 1。
            • x[x=x] 可能是一个有趣的修改。 x[x=x&lt;-1]x[x&lt;-x&lt;-1] 也一样。后者在第一次和第二次运行时给出不同的结果。但我想这可能有点不必要,到那时你会让你的一些学生感到无聊......
            【解决方案7】:

            通过反复试验,我了解到我需要非常明确地命名我的函数参数(并确保在开始时和沿函数进行足够的检查)以使一切尽可能健壮。如果您将变量存储在全局环境中,则尤其如此,但随后您尝试使用自定义值来调试函数 - 并且有些东西没有加起来!这是一个结合了错误检查和调用全局变量的简单示例。

            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
            

            【讨论】:

              【解决方案8】:

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

              .Random.seed <- 'normal'
              

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

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

              rnorm(10)
              

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

              我也用了这个例子:

              x <- 27
              z <- somefunctionthatusesglobals(5)
              

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

              【讨论】:

                【解决方案9】:

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

                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"))
                }
                

                【讨论】:

                • 嗯,很有趣。我倾向于认为编写一个使用其范围之外的东西的函数是一个坏主意。如果你想在函数中使用某些东西,它应该作为参数传入。如果这样做,该函数是自包含的 - 相同的函数调用将始终给出相同的结果。
                • 我不太了解 R 的工作原理,但对象是通过值或引用传递的。两种方法都可以吗?
                • 我认为你可以实现两者,但一般对象是按值传递的。因此,当您将对象传递给函数时,它们会被复制以将其值提供给参数。
                • 为了跟进 Gavin 的评论,这里讨论了传递引用:stackoverflow.com/questions/2603184/r-pass-by-reference/…。谢谢加文。总是从你的帖子中学到很多东西。
                【解决方案10】:

                一个划分意见的全局变量的 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 中实现的方式,这无法真正纠正 - 任何事情都可能在您不知情的情况下更改它们,因此不能保证相同的代码块返回完全相同的对象。 John Chambers 在他最近的book 中哀叹这个功能

                【讨论】:

                • 这是一个很好的例子,实际上让我大开眼界,因为我可能比我应该更多地依赖函数默认值。然而,这是其他人(S/R 设计者)如何使用全局变量来制造麻烦的一个例子,而不是一个基本的应用统计程序员可能会遇到麻烦。所以它很有帮助,非常感谢,但不是我可以用作示例的确切内容。
                • "Chamber 哀叹..." 什么页面?该索引没有“因素”、“全局”或“字符串AsFactors”的条目,我刚刚浏览了第 6.5 节,该节涵盖了数据帧但没有找到它。
                • @DWin 的一般想法是,在提供相同输入时,通常不能保证函数返回相同的输出。 IIRC 在本书的开篇章节中。我的副本正在工作,所以现在找不到。
                • @DWin,参见第 3.2 节函数和函数式编程:'“更阴险的是函数,例如 options(),通常在 C 代码中创建隐藏的副作用。”'
                猜你喜欢
                • 2012-06-09
                • 1970-01-01
                • 2016-03-02
                • 2015-04-19
                • 1970-01-01
                • 1970-01-01
                • 2010-11-17
                • 1970-01-01
                相关资源
                最近更新 更多