Урок 6 NAMESPACE - Зависимости пакета


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


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


6.1 Видео

6.1.1 Тайм коды

00:00 Вступление
00:50 Преимущества и недостатки зависимостей
02:41 Анализ зависимостей пакетов
05:50 Какие компоненты пакета отвечают за его зависимости
07:37 Файл NAMESPACE
08:35 Рабочий процесс установки зависимостей
14:56 Когда стоит импортировать объекты из других пакетов
16:38 Как обращаться к функциям импортированным из других пакетов в коде, тестах и примерах вашего пакета, если поля указаны в поле Imports
18:04 Как обращаться к функциям импортированным из других пакетов в коде вашего пакета, если поля указаны в поле Suggest
21:34 Как обращаться к функциям импортированным из других пакетов в тестах вашего пакета, если поля указаны в поле Suggest
23:10 Как обращаться к функциям импортированным из других пакетов в виньетках и примерах к функциям вашего пакета, если поля указаны в поле Suggest
24:26 Как обращаться к функциям импортированным из других пакетов в коде, тестах и примерах вашего пакета, если поля указаны в поле Depends
26:05 Импорт и экспорт S3 методов
28:30 Заключение

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

6.3 Конспект

Зависимости в вашем пакете появляются когда вы в коде своего пакета используете функции из сторонних пакетов.

6.3.1 Преимущества и недостатоки зависимости

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

Но, за всё надо платить, поэтому в использовании зависимостей есть и ряд недостатоков:

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

6.3.2 Анализ зависимостей пакета

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

# анализ зависимостей
## просмотр дерева зависимостей
### низкоуровневые пакеты
pak::pkg_deps_tree("tibble")
✔ Updated metadata database: 4.68 MB in 5 files.                          
✔ Updating metadata database ... done                                     
tibble 3.2.1 [new][dl] (690.77 kB)                                         
├─fansi 1.0.4 [new][dl] (312.84 kB)
├─lifecycle 1.0.3 [new][dl] (139.02 kB)
│ ├─cli 3.6.1 [new][dl] (1.33 MB)
│ ├─glue 1.6.2 [new][dl] (162.52 kB)
│ └─rlang 1.1.1 [new][dl] (1.57 MB)
├─magrittr 2.0.3 [new][dl] (226.89 kB)
├─pillar 1.9.0 [new][dl] (659.28 kB)
│ ├─cli
│ ├─fansi
│ ├─glue
│ ├─lifecycle
│ ├─rlang
│ ├─utf8 1.2.3 [new][dl] (149.69 kB)
│ └─vctrs 0.6.3 [new][dl] (1.33 MB)
│   ├─cli
│   ├─glue
│   ├─lifecycle
│   └─rlang
├─pkgconfig 2.0.3 [new][dl] (22.45 kB)
├─rlang
└─vctrs

Key:  [new] new | [dl] download

Также вы можете использовать функцию tools::package_dependencies():

## высокоуровневые пакеты
n_hard_deps <- function(pkg) {
  deps <- tools::package_dependencies(pkg, recursive = TRUE)
  sapply(deps, length)
}

n_hard_deps(c("tidyverse", "devtools", "rlang", "cli"))
tidyverse  devtools     rlang       cli 
      114       101         1         1 

Пакеты tidyverse и devtools являются не просто высокоуровневыми, они являются мета-пакетами, т.е. коллекциями из других пакетов, поэтому у них в зависомтях более 100 сторонних пакетов, в то время как у низкоуровневых rlang и cli всего 1 зависимость. Крайне изегайте использования в своих зависимостях мета-пакетов, при необходимости используйте нужный из коллекции пакет. Ниже пример, если вам необходимы функции из dplyr или tidyr, которые входят в tidyverse, то импортируйте именно конкретные пакеты, т.к. у них гораздо меньше зависимостей:

n_hard_deps(c("dplyr", "tidyr"))
dplyr tidyr 
   20    26 

6.3.3 Какие компоненты пакета отвечают за зависимости

  • Файл DESCRIPTION, позволяет указать какие пакеты будут установлены или рекомендованы к установке вместе с вашим пакетом:
    • Поле Imports: указанные пакеты будут установлены вместе с вашим пакетом;
    • Поле Suggest: указанные пакеты будут рекомендованы к установке;
    • Поле Depends: указанные пакеты будут установлены и экспортированы вместе с вашим пакетом.
  • Файл NAMESPACE, управляет экспортом объектов в рабочее окружение

Пакеты указанные в поле Imports файла DESCRIPTION не обязательно должны быть указаны в NAMESPACE, но все пакеты и функции перечисленные в файле NAMESPACE, так же обязательно должны быть указаны в полях Imports или Depends файла DESCRIPTION.

6.3.4 Директивы файла NAMESPACE

Файл NAMESPACE зачустую выглядит примерно следующим образом:

# Generated by roxygen2: do not edit by hand

S3method(compare,character)
S3method(print,testthat_results)
export(compare)
export(expect_equal)
import(rlang)
importFrom(brio,readLines)
useDynLib(testthat, .registration = TRUE)
  • export(): экспортировать функцию (включая дженерики S3 и S4).
  • S3method(): экспортировать метод S3.
  • importFrom(): импортировать выбранный объект из другого пространства имен (включая дженерики S4).
  • import(): импортировать все объекты из пространства имен другого пакета.
  • useDynLib(): регистрирует процедуры из DLL (для пакетов с скомпилированным кодом).

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

6.3.5 Рабочий процесс

Весь рабочий процесс по добавлению зависимостей в пакет состоит из следующих этапов:

  1. Изначально добавляете с помощью команды usethis::use_package() необходимые пакеты в нужные поля файла DESCRIPTION.
  2. Над кодом функций используйте специальные roxygen комментарии import для импорта всего пространства имён стороннего пакета, или importFrom, для импорта отдельный функций из сторонних пакетов.
#' @importFrom aaapkg aaa_fun
#' @import bbbpkg
#' @export
foo <- function(x, y, z) {
  ...
}
  1. Запускаете функцию devtools::document() для генерации файла NAMESPACE.

Но, где удобнее всего прописать roxygen комментарии для импорта функций и целых пакетов, если вы их многократно используете в своём коде? Первое, что наверняка придёт вам в голову - писать roxygen комментарии для импорта над. каждой функцией, в которой используются импортируемые объекты. Но это слишком избыточно, ведь один roxygen комментарий уже добавит нужную директутиву в файл NAMESPACE, поэтому имеет смысл прописать все комментарии для импорта объектов в одном месте, для чего наиболее удобно использовать функцию usethis::use_package_doc(). Данная функция создаёт файл R/pkg-package.R, в котом и будут собираться все ваши roxygen комментари для импорта над пустым объектом NULL, выглядит этот файл примерно так:

# The following block is used by usethis to automatically manage
# roxygen namespace tags. Modify with care!
## usethis namespace: start
#' @importFrom glue glue_collapse
## usethis namespace: end
NULL

Далее вы будете добавлять в этот файл roxygen комментари с помощью use_import_from().

6.3.6 Когда необходимо экспортировать объекты из сторонних пакетов

В ходе курса я неоднократно говорил о том, что зачастую вам не потербуется экспортировать в рабочее окружение функции из стороних пакетов, а вместо этого просто пропишите все необходимые вам пакеты в поле Imports файла DESCRIPTION, а в коде ваших функций образайтесь к функциям импортированных пакетов с помощью package_name::function(). Но из этого правила есть некоторые исключения:

  • Оператор: Вы не можете вызвать оператора из другого пакета через ::, поэтому его необходимо импортировать. Примеры: оператор объединения NULL %||% из rlang или пайплайн %>% из magrittr.
  • Функция, которую вы часто используете. Если импорт функции делает ваш код более читабельным, это достаточная причина для ее импорта. Это буквально уменьшает количество символов, необходимых для вызова внешней функции.
  • Функция, которую вы вызываете в жестком цикле с ::. Поиск объекта вызванного через два двоеточия составляет порядка 100 нс, поэтому оно будет иметь значение только в том случае, если вы вызываете функцию миллионы раз.

6.3.7 Как обращаться к функциям сторонних пакетов

В зависимости от того как вы импортируете в свой пакет сторонние пакеты зависит то, как вы будете вызывать функции этих пакетов. На это влияет то, какие поля файла DESCRIPTION вы используете.

6.3.7.1 При импорте пакетов через поле Imports

  • В коде пакета, т.е. в папке R/ обращайтесь к функциям из указанных в поле Imports пакетов package::function().
  • В тестах обращайтесь к функциям из указанных в поле Imports пакетов package::function(). Но если вы импортировали определенную функцию отдельно или как часть всего пространства имен, вы можете просто вызвать ее непосредственно в тестовом коде.
  • Если вы используете пакет, который указанный в Imports в одном из ваших примеров или виньеток, вам нужно будет либо прикрепить пакет с помощью, library(package) либо использовать package::function().

6.3.7.2 При импорте пакетов через поле Suggest

В отличае от поля Imports, пакеты указанные в поле Suggest не обязательно будут установлены у конечного пользователя вашего пакета, в связи с чем вам необходимо делать дополнительные проверки на наличие их установки.

В коде пакета, т.е. в папке R/ вы должны проверить наличие установленного пакета с помощью базовой функции requireNamespace(), или функций из пакета rlang: is_installed() и check_installed().

# Проверка установки пакета через базовую requireNamespace()
## Проверка установлен ли пакет
my_fun <- function(a, b) {
  if (!requireNamespace("aaapkg", quietly = TRUE)) {
    stop(
      "Package \"aaapkg\" must be installed to use this function.",
      call. = FALSE
    )
  }
  # code that includes calls such as aaapkg::aaa_fun()
}

# Альтрнативный сценарий выполнения
my_fun <- function(a, b) {
  if (requireNamespace("aaapkg", quietly = TRUE)) {
    aaapkg::aaa_fun()
  } else {
    g()
  }
}


# С помощью пакета rlang
## пакет указанный в Suggest обязателен для выполнения функции
my_fun <- function(a, b) {
  rlang::check_installed("aaapkg", reason = "to use `aaa_fun()`")
  # code that includes calls such as aaapkg::aaa_fun()
}

## Функция с двумя альтернативными сценариями
my_fun <- function(a, b) {
  if (rlang::is_installed("aaapkg")) {
    aaapkg::aaa_fun()
  } else {
    g()
  }
}

В тестах вы можете использовать функцию testthat::skip_if_not_installed() для пропуска тестов, если у пользовтаеля не установлены необходимые для их выполнения пакетов.

test_that("basic plot builds without error", {
  skip_if_not_installed("sf")

  nc_tiny_coords <- matrix(
    c(-81.473, -81.741, -81.67, -81.345, -81.266, -81.24, -81.473,
      36.234, 36.392, 36.59, 36.573, 36.437, 36.365, 36.234),
    ncol = 2
  )

  nc <- sf::st_as_sf(
    data_frame(
      NAME = "ashe",
      geometry = sf::st_sfc(sf::st_polygon(list(nc_tiny_coords)), crs = 4326)
    )
  )

  expect_doppelganger("sf-polygons", ggplot(nc) + geom_sf() + coord_sf())
})

Для использования пакетов указанных в Suggest в виньетках или примерах функций используйте функции require() или requireNamespace(), для проверки доступен ли необходимый пакет.

#' @examples
#' if (require("maps")) {
#'   nz <- map_data("nz")
#'   # Prepare a map of NZ
#'   nzmap <- ggplot(nz, aes(x = long, y = lat, group = group)) +
#'     geom_polygon(fill = "white", colour = "black")
#'
#'   # Plot it in cartesian coordinates
#'   nzmap
#' }

6.3.7.3 При импорте пакетов через поле Depends

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

6.3.8 Импорт и экспорт S3 методов

  • Экспортируете основную дженерик функцию через директиву export()
  • Регистрируете её методы написанные под обработку объектов различных классов с помощью директивы S3method()

Ниже пример дженерик функции и метода под обработку data.frame с её помощью:

#' ... all the usual documentation for count() ...
#' @export
count <- function(x, ..., wt = NULL, sort = FALSE, name = NULL) {
  UseMethod("count")
}

#' @export
count.data.frame <- function(
  x,
  ...,
  wt = NULL,
  sort = FALSE,
  name = NULL,
  .drop = group_by_drop_default(x)) { ... }

Для их экспорта прописываются следующие директивы в файле NAMESPACE:

...
S3method(count,data.frame)
...
export(count)
...

6.4 Тест