Урок 3 Рекомендации по организации R кода


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


Данный урок основан на главе "R code" книги "R Packages (2e)", под авторством Хедли Викхема и Дженни Брайан.


3.1 Видео

3.1.1 Тайм коды

00:00 Вступление
00:44 Из каких компонентов состоит пакет
01:07 Как организовать функции пакета в файлы
03:55 Про функции library() и source() в коде пакет
05:32 Не изменяйте настройки глобальной среды R: функция on.exit() и пакет withr
14:09 Как задать локальные опции пакета, функция .onLoad()
17:10 Код пакета определённый вне функций
18:20 Заключение

3.2 Презентация

3.3 Конспект

3.3.1 Организация функций в файлы

По поводу организаций функций в файлы нет строгих правил, есть два крайних подхода:

  1. Поместить каждую функцию в отдельный R файл
  2. Поместить код всех функций в один R файл

Оба этих подхода далеки от лучших практик, и затрудняют навигацию по коду пакета, поэтому для организации функций придерживайтесь следубщих правил:

  1. Если у вас есть семейство функций поместите их в один R файл
  2. Если у вас есть функция и набор вспомогательных к ней функций, поместите основную функцию и её помощников в один R файл
  3. Если функция не имеет помощников, и не входит ни в какое семейство функций, поместите её отдельно в R файл

3.3.2 Команды library() и source()

Никогда не используйте в коде пакета команды library(), require() и source():

  • library() и require() изменяют путь поиска, влияя на то, какие функции доступны из глобальной среды. Вместо этого вы должны использовать DESCRIPTION для указания требований вашего пакета. Это также гарантирует, что эти пакеты будут установлены при установке вашего пакета.
  • source() изменяет текущую среду, вставляя результаты выполнения кода. Для использования добавленных функций в ваш пакет используйте load_all() или test().

3.3.3 Пакет не должен изменять настройки глобального окружения R

Фукнции вашего пакета не должны изменять глобальных настроек, т.е. опций, переменных среды, счётчика генерации случайных чисел, и т.д. глобальной среды R. Поэтому старайтесь избегать, или использовать с крайней осторожностью такие команды как:

Если вам всё таки необходимо внутри функций вашего пакета переопределять состояние среды, делайте это так, что бы при выходе из функции все переопределённые опции возвращались к исходному значению. Сделать это можно с помощью:

  • Базовой функции on.exit()
  • Функционала пакета withr

3.3.3.1 Пример неправильного определения опций пакета:

# Неправильная установка опций --------------------------------------------
## определяем какую то опцию в рамках сеанса
options(test.opt = 10)
## запрашиваем значение опции
getOption('test.opt')
#> [1] 10

# определяем функцию изменяющую значение опции
f1 <- function(x) {

  options(test.opt = x)
  getOption('test.opt')

}

## запускаем функцию
f1(15)
#> [1] 15
## работа функции изменила значение опции, определённое в глобальной среде
getOption('test.opt')
#> [1] 15

Функция f1() в своём коде переопределяет значение опции test.opt, до запуска функции данная опция имела значение 10, функция, незаметно для её пользователя, изменила это значение на 15 в глобальной среде. Это может вызвать проблемы, если данная опция используется шде-то далее в вашем коде, при чём обнаружиь такую проблему зачастую довольно сложно.

3.3.3.2 Пример локального изменения опция с помощью функции on.exit():

# базовая конструкция on.exit() -------------------------------------------
## возвращаем дефолтное значение опции
options(test.opt = 10)

## пишем код функции, работающий с опцией локально
f2 <- function(x) {

  old <- options(test.opt = x)
  on.exit(options(old))
  getOption('test.opt')

}

# запускаем функцию
f2(15)
#> [1] 15
# проверяем значение опции после её выполнения
getOption('test.opt')
#> [1] 10

В функции f2() мы добавили конструкцию old <- options(test.opt = x), которая созраняет прежнее значение опции перед тем, как присвоить новое. Далее с помощью функции on.exit(options(old)) мы говорим нашей фукнции вернуть исходное значение заданой внутри функции опции test.opt. Как видим, теперь функция не изменяет значение опции в глобальной среде, заданное изменение действует только внутри функции.

3.3.3.3 Пример изменения опции с помощью функционала пакета withr:

Пакет withr предоставляет более гибкий функционал для локального изменения значений опций. Пример изменения опций с помощью функции with_options():

# установка опций с помощью withr -----------------------------------------
## определяем какую то опцию в рамках сеанса
options(test.opt = 10)
## запрашиваем значение опции
getOption('test.opt')
#> [1] 10

## with_*() - функции лучше всего подходят для выполнения небольших фрагментов кода с временно измененным состоянием.
f3 <- function(x) {

  print(getOption('test.opt'))

  withr::with_options(
    list(test.opt = x),
    print(getOption('test.opt'))
  )

  print(getOption('test.opt'))

}

# запускаем функцию
f3(15)
#> [1] 10
#> [1] 15
#> [1] 10
## запрашиваем значение опции
getOption('test.opt')
#> [1] 10

При использовании функций с префиксом with_*() область действия изменений внесённых в опции или переменные среды распространяется только на код, прописанный в качестве второго аргумента самой функций. Поэтому внутри функции f3() изначально значение опции test.opt равно 10, после внутри функции with_options() мы его меняем на 15, после выхода из функции with_options() опция test.opt опять имеет глобально определённое значение 10, не смотря на то, что мы ещё не вышли из основной функции f3().

Пример изменения опций с помощью функции with_local():

## local_*() - функции определяют значения опций, которые будут действоваьт до выхода из функции.

f4 <- function(x) {

  print(getOption('test.opt'))
  withr::local_options(list(test.opt = x))
  print(getOption('test.opt'))

}

# запускаем функцию
f4(15)
#> [1] 10
#> [1] 15
## запрашиваем значение опции
getOption('test.opt')
#> [1] 10

Функции с префиксом local_*() определяют изменения среды, которые действуют внутри вашей функции, т.е. так же как и базовая функция on.exit().

3.3.4 Как задать локальные опции своего пакета

Иногда вам может понадобиться определить внутренние опции, ищменяющие поведение вашего пакета. Код определния опций следует прописывать внутри специальной функции .onLoad(). Данная функция выполняется каждый раз при загрузке пакета в память, т.е. при выполнении команды library(package_name). Код определения функций пакета принято помещать в файл zzz.R, делается это для того, что бы данный фрагмент кода пакета выполнялся в последнюю очередь.

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

Пример очень урезанного кода определения внутренних опций пакета dplyr:

.onLoad <- function(libname, pkgname) {
  op <- options()
  op.dplyr <- list(
    dplyr.show_progress = TRUE
  )
  toset <- !(names(op.dplyr) %in% names(op))
  if (any(toset)) options(op.dplyr[toset])

  invisible()
}

Что делает приведённый выше код:

  1. Изначально командой op <- options() мы считываем все опции определённые в глобальном окружении
  2. Далее мы задаём список (package_name.op) опций нашего пакета, именуя опции согласно шаблону package_name.option_name
  3. Команда toset <- !(names(op.dplyr) %in% names(op)) проверяет - небыли ли установлены глобально значения каких либо опций нашего пакета
  4. Если хотя бы одна из опций пакета не имеет значений в глобальном окружении (if (any(toset))) то устанавливаем для неопределнных опций дефолтные значения (options(op.dplyr[toset])).

3.4 Тест