Список хранит что угодно
У вектора есть одно жёсткое правило: все элементы должны быть одного типа. У списка такого правила нет. Каждый элемент списка может быть другого типа, другой длины и даже другим списком. Это универсальный контейнер R:
length() сообщает 4 — четыре элемента, хотя один из них (scores) сам содержит три числа. Список считает свои отсеки, а не то, что внутри них. str() (structure) — самый быстрый способ увидеть, что на самом деле хранит список: по строке на элемент с именем, типом и предпросмотром. Сделайте str() рефлексом: как только функция вернула вам что-то непонятное, примените к нему str().
Имена необязательны (list(1, "a", TRUE) вполне законен), но почти всегда стоят того — именованный список документирует сам себя.
Три пути внутрь: [, [[ и $
Это та часть списков, в которой все хоть раз ошибаются, так что разберёмся точно. Есть три оператора доступа, и они выполняют две разные задачи:
[возвращает меньший список — контейнер того же вида с тем, что вы выбрали.[[возвращает сам элемент — реальное значение внутри отсека.$— сокращение для[[с буквальным именем:person$ageэтоperson[["age"]].
Разница невидима при беглой печати и очень заметна в момент, когда вы пытаетесь считать:
У person["scores"] класс "list" — список длины 1 с вектором баллов внутри. У person[["scores"]] класс "numeric" — сам вектор, готовый для mean(). Вызовите mean() на версии с одинарными скобками — и получите ошибку о том, что аргумент не числовой; это сообщение почти всегда означает, что вы применили [ там, где имели в виду [[.
Метафора, которая запоминается: список — это поезд. [ даёт вам поезд покороче — всё ещё поезд. [[ открывает вагон и подаёт груз.
Так когда же [ — правильный инструмент? Когда вам нужно несколько элементов и вы хотите оставить их упакованными:
Извлечь два элемента «сами по себе» одновременно нельзя — двум значениям нужен контейнер, — поэтому выбор нескольких элементов всегда идёт через одинарные скобки и всегда даёт список.
Ещё одно отличие: $ использует частичное сопоставление (person$sc найдёт scores, если это однозначно), что удобно в консоли и опасно в скриптах. В коде, который должен продолжать работать, предпочитайте [[ с полным именем — он ещё и принимает имя, хранящееся в переменной, чего $ не умеет.
Добавление, изменение, удаление
Списки растут и сжимаются обычным присваиванием — специальный метод append не нужен:
- Присваивание ещё не существующему имени добавляет элемент.
- Присваивание существующему имени заменяет его.
- Присваивание в позицию
length(x) + 1добавляет безымянный элемент в конец. - Присваивание
NULLполностью удаляет элемент — список становится короче. (Это единственное место, гдеNULLработает как команда удаления; если вам действительно нужно хранить «ничего» в отсеке, используйтеx["k"] <- list(NULL).)
Чтобы склеить два списка встык, работает c() — со списками так же, как с векторами: c(list_a, list_b) возвращает один более длинный список.
Вложенные списки
Поскольку элементом списка может быть другой список, списки вкладываются на любую глубину — что делает их естественной формой R для структурированных данных вроде разобранного JSON или сгруппированных результатов:
Погружение внутрь — просто сцепление операторов доступа: каждый $ или [[ спускает на уровень глубже. Когда вложенная структура запутывает, str(company) показывает всё дерево сразу, а str(company, max.level = 1) — только верхний слой.
Уплощение через unlist()
unlist() схлопывает список — сколь угодно глубоко вложенный — в один вектор:
Обратите внимание на две вещи. Во-первых, unlist() сам строит имена (math1, math2, art), так что вы по-прежнему видите, откуда пришло каждое значение. Во-вторых — и это ловушка — вектор хранит только один тип, поэтому уплощение смешанного списка приводит всё к самому гибкому из присутствующих типов. Число 1 вернулось как строка "1". Если после unlist ваши числа превратились в текст, значит, список был не так однороден, как вам казалось.
Почему списки важны
Списки могут показаться темой для начинающих, которую вы перерастёте. Всё наоборот — они несущая конструкция всего языка:
- Функции, возвращающие несколько вещей, возвращают список. Функции R возвращают один объект; список позволяет этому объекту нести и подогнанные значения, и таблицу коэффициентов. Когда вы строите регрессию, объект модели, который вы получаете, — это (большой, с классом) список;
str()по нему это докажет. - Датафрейм — это список столбцов, векторов одинаковой длины с дополнительным поведением сверху. Всё, что вы только что выучили (
$,[[, присваиваниеNULLдля удаления элемента), применимо к столбцам датафрейма буквально. - Семейство apply —
lapply()и родственники — принимает список, применяет функцию к каждому элементу и возвращает список. «Списки на входе, списки на выходе» — стандартная форма повторяющихся вычислений в R.
Что вы уносите с собой
- Списки хранят что угодно: смешанные типы, разные длины, другие списки, целые датафреймы.
[возвращает меньший список;[[возвращает сам элемент;$— это[[с буквальным именем. Собираетесь считать? Вам нужен[[или$.- Добавляйте присваиванием новому имени, удаляйте присваиванием
NULL, объединяйте черезc(). unlist()уплощает в вектор и по пути приводит смешанные типы — проверяйте класс результата.- Датафреймы — это списки столбцов, поэтому эти знания переносятся напрямую.
Дальше: матрицы — аналог списка с одним типом и строками со столбцами.
Часто задаваемые вопросы
В чём разница между [ и [[ в R?
Одинарные скобки [ всегда возвращают список, содержащий выбранные элементы, — меньший контейнер того же вида. Двойные скобки [[ заглядывают внутрь и возвращают сам элемент. Если x$scores хранит числовой вектор, то x["scores"] — это список длины 1, а x[["scores"]] — сам числовой вектор. Используйте [[ (или $), когда хотите действительно работать со значением.
Как добавить элемент в список в R?
Присвойте значение ещё не существующему имени: x$email <- "rosa@example.com" или x[["email"]] <- .... Список вырастет автоматически. Чтобы добавить без имени, присвойте в следующую позицию: x[[length(x) + 1]] <- value. Чтобы удалить элемент, присвойте ему NULL: x$email <- NULL.
Как преобразовать список в вектор в R?
unlist(x) уплощает список (включая вложенные) в один вектор. Поскольку вектор хранит только один тип, всё приводится к самому гибкому из присутствующих — список из чисел и строк станет полностью символьным вектором. Проверяйте class() результата после unlist.