Примеры опасностей глобалов в R и Stata - PullRequest
44 голосов
/ 03 апреля 2011

В недавних беседах с однокурсниками я выступал за то, чтобы избегать глобальных переменных, кроме как для хранения констант.Это своего рода типичная прикладная программа типа статистики, где каждый пишет свой собственный код, а размеры проектов малы, поэтому людям может быть трудно увидеть проблемы, вызванные небрежными привычками.

Говоря об избежании глобальных переменных, я концентрируюсь на следующих причинах, по которым глобальные переменные могут вызывать проблемы , но я хотел бы иметь несколько примеров в R и / илиStata , чтобы следовать принципам (и любым другим принципам, которые вы можете найти важными), и мне трудно придумать правдоподобные.

  • Нелокальность: глобальные отладкисложнее, потому что они затрудняют понимание потока кода
  • Неявная связь: глобальные переменные нарушают простоту функционального программирования, позволяя сложные взаимодействия между удаленными сегментами кода
  • Столкновения пространства имен: общие имена (x, iи т. д.) использовать повторно, вызывая коллизии в пространстве имен

Полезным ответом на этот вопрос будет воспроизводимый и автономный фрагмент кода, в котором глобальные вызовы вызывают определенный тип проблем, в идеале сдругой фрагмент кода, в котором исправлена ​​проблема.Я могу сгенерировать исправленные решения, если это необходимо, поэтому пример проблемы более важен.

Релевантный ссылки :

Глобальные переменные неверны

Являются ли глобальные переменные плохими?

Ответы [ 10 ]

28 голосов
/ 03 апреля 2011

Я также с удовольствием преподаю 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)
      }
    

    Ответ: оба.Снова обсудите, почему это плохо.

16 голосов
/ 02 августа 2011

О, чудесный запах глобалов ...

Все ответы в этом посте содержали R примеров, и ОП хотел также несколько примеров 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
    

    И затем они запускают эти регрессии с другим набором переменных, затем с еще одной, и, наконец, они сдаются и устанавливают это как do-файл 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 где-нибудь в вашем главном do-файле, может быть даже как

    global mydir `c(pwd)'
    
  3. Глобалы могут использоваться для хранения непредсказуемого дерьма, как целая команда:

    capture $RunThis
    

    Бог знает только, что будет казнено. Это наихудший случай неявной сильной связи, но поскольку я даже не уверен, что RunThis будет содержать что-либо значимое, я поставлю перед ним capture и буду готов обработать ненулевой код возврата _rc. (См., Однако, мой пример ниже.)

  4. Использование Stata глобальных переменных - для настроек Бога, например, вероятность ошибки типа I / уровень достоверности: глобальный $S_level всегда определен (и вы должны быть полным идиотом, чтобы переопределить этот глобальный, хотя, конечно, это технически выполнимо). Однако это в основном устаревшая проблема с кодом версии 5 и ниже (примерно), поскольку ту же информацию можно получить из менее хрупкой системной константы:

    set level 90
    display $S_level
    display c(level)
    

К счастью, глобальные переменные в Stata достаточно явные и, следовательно, их легко отлаживать и удалять. В некоторых из вышеперечисленных ситуаций, и, конечно, в первой, вы захотите передать параметры в do-файлы, которые рассматриваются как локальные `0' внутри do-файла. Вместо использования глобалов в файле 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 оба, конечно.

11 голосов
/ 03 апреля 2011

Одним из примеров глобальной переменной 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 - все может изменить их, не зная об этом, и, таким образом, один и тот же кусок кода не гарантирует возвращение точно одного и того же объекта.Джон Чэмберс оплакивает эту функцию в своей недавней книге .

8 голосов
/ 01 августа 2012

Вот интересный патологический пример, включающий функции замены, глобальное присвоение и 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.

8 голосов
/ 04 апреля 2011

Патологическим примером в 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)

Это подчеркивает беспорядок, в который вы можете попастьполагаться на все, что находится не только в области вашей функции или явно передано в качестве аргумента.

5 голосов
/ 03 апреля 2011

Методом проб и ошибок я узнал, что мне нужно быть очень явным при именовании аргументов своей функции (и обеспечить достаточное количество проверок в начале и вдоль функции), чтобы сделать все как можно более надежным. Это особенно верно, если у вас есть переменные, хранящиеся в глобальной среде, но затем вы пытаетесь отладить функцию с пользовательскими ценностями - и что-то не складывается! Это простой пример, сочетающий плохие проверки и вызов глобальной переменной.

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
5 голосов
/ 03 апреля 2011

Один быстрый, но убедительный пример в R - это запустить строку вроде:

.Random.seed <- 'normal'

Я выбрал «нормальный» как то, что кто-то может выбрать, но вы можете использовать все что угодно.

Теперь запустите любой код, который использует сгенерированные случайные числа, например:

rnorm(10)

Затем вы можете указать, что то же самое может произойти с любой глобальной переменной.

Я также использую пример:

x <- 27
z <- somefunctionthatusesglobals(5)

Затем спросите студентов, каково значение x;Ответ в том, что мы не знаем.

3 голосов
/ 26 октября 2011

Пример скетча, который возник при попытке научить этому сегодня.В частности, это делается для того, чтобы попытаться дать интуитивное представление о том, почему глобальные переменные могут вызывать проблемы, поэтому он максимально абстрагируется, пытаясь указать, что можно и что нельзя сделать только из кода (оставляя функцию в виде черного ящика).

Настройка

Вот некоторый код.Решите, будет ли возвращаться ошибка или нет, основываясь только на данных критериях.

Код

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

Критерии

Случай 1: f() - это функция с правильным поведением, которая использует только локальные переменные.

Сценарий 2: f() не обязательно является функцией с правильным поведением, которая потенциально может использовать глобальное присваивание.

Ответ

Случай 1: код не вернет ошибку, так как в первой строке проверяется, что x нет равных нулю, а в третьей строкеделится на x.

Случай 2: Код потенциально может вернуть ошибку, поскольку f() может, например, вычесть 1 из x и присвоить его обратно x в родительской среде, гделюбой элемент x, равный 1, может быть затем установлен на ноль, а третья строка вернет ошибку деления на ноль.

2 голосов
/ 04 апреля 2011

Вот одна попытка ответа, которая имела бы смысл для статистических типов.

  • Коллизии пространства имен: повторное использование общих имен (x, i и т. Д.) Вызывает коллизии пространства имен

Сначала мы определяем логарифмическую функцию правдоподобия,

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

Теперь мы пишем несвязанную функцию, которая возвращает сумму квадратов входных данных.Поскольку мы ленивы, мы сделаем это, передав ей y как глобальную переменную,

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

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

Кажется, что наша функция логарифмического правдоподобия ведет себя точно так, как мы ожидаем, принимая аргумент и возвращая значение,

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

Но что случилось с нашей другой функцией?

> sumSq()
[1] 633538

Конечно, это тривиальный пример, как и любой пример, который не существует в сложной программе.Но, надеюсь, это вызовет дискуссию о том, насколько труднее отслеживать глобалы, чем местные жители.

0 голосов
/ 03 апреля 2011

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