Базовый цикл for
Цикл for в R выполняет блок кода один раз для каждого элемента вектора или списка. Переменная цикла по очереди принимает каждое значение:
Три элемента — три прохода. Не нужно заводить счётчик и поддерживать условие: for (fruit in fruits) читается как «для каждого fruit из fruits», и делает он ровно это. Такая же форма перебирает элементы списка, последовательность чисел (for (i in 1:10)) или имена чего угодно через names().
Печати внутри цикла нужны print() или cat()
В консоли набранное x печатает своё значение. Внутри тела цикла — нет: автопечать работает только на верхнем уровне, а голое выражение внутри цикла вычисляется и выбрасывается:
Так что когда цикл «ничего не делает», проверьте это первым делом. Используйте print(), чтобы быстро взглянуть на любое значение, или cat(), когда сами составляете строку вывода (не забудьте "\n" — cat() его не добавляет).
Обход индексов через seq_along()
Иногда нужна позиция вместе со значением — чтобы записать в другой вектор по тому же индексу или напечатать нумерованный список. Обходите индексы через seq_along():
В старом коде вам встретится for (i in 1:length(prices)), и в нём кроется настоящий баг. Когда вектор пуст, length() равно 0, а 1:0 означает не «ноль итераций» — двоеточие считает вниз:
Цикл по 1:length(empty) выполняется дважды, индексируя несуществующие элементы (empty[1] это NA, а присваивания создают фантомные записи). seq_along() возвращает пустую последовательность для пустого вектора, поэтому тело цикла просто не выполняется. Сделайте seq_along() привычкой; её родственник seq_len(n) делает то же самое, когда у вас есть количество, а не вектор.
Сбор результатов: выделяйте память заранее, а не наращивайте
Естественный первый порыв при накоплении результатов выглядит так:
squares <- c()
for (i in 1:10000) {
squares <- c(squares, i^2) # copies the ENTIRE vector every pass
}
Это работает, но каждый c(squares, ...) выделяет совершенно новый вектор и копирует в него все старые элементы. К концу вы скопировали примерно 50 миллионов чисел, чтобы получить 10 000. Этот антипаттерн растущего вектора — главная причина, по которой люди считают циклы R медленными.
Решение: сначала создайте результат нужного размера, затем присваивайте по индексу:
Для результатов, которые не являются числами, работает та же идея с character(n), logical(n) или vector("list", n) для списка произвольных объектов. Циклы с преаллокацией совершенно не медленные.
next и break
Два ключевых слова управляют циклом изнутри его тела: next бросает текущий проход и переходит к следующему элементу; break полностью выходит из цикла.
Это печатает 2 4 6 8: нечётные значения пропускаются через next, а когда доходит до 10, break завершает цикл до печати. Используйте next, чтобы отсеивать случаи пораньше (это оставляет основное тело без лишних отступов), и break, когда цикл нашёл то, что искал, и продолжать незачем.
Вложенные циклы
Тело цикла может содержать другой цикл. Внутренний цикл проходит целиком на каждом отдельном проходе внешнего — классическая демонстрация это таблица умножения:
Для каждой строки i внутренний цикл проходит все столбцы j, а перенос строки печатается после завершения внутреннего цикла. Вложенные циклы нормальны на двух уровнях; на трёх и более тело обычно просится стать функцией, а всё вычисление — стать outer() или матричной операцией.
Вложенность также покрывает обход списка векторов — внешний цикл по списку, внутренний (а лучше векторизованный вызов) по каждому элементу:
Обратите внимание: внутреннего цикла в итоге и нет — mean() обрабатывает весь внутренний вектор одним вызовом. Это наблюдение обобщается, что подводит нас к честной части.
Когда не стоит писать цикл
R — векторизованный язык: его базовые операции уже работают сразу над целыми векторами. Цикл, который преобразует элементы по одному, часто оказывается длинным способом написать однострочник:
Предпочитайте векторизованную форму всякий раз, когда она существует, — она короче, в ней труднее ошибиться и она быстрее. Для применения функции к каждому элементу списка ту же роль играет семейство apply (sapply, lapply, vapply).
Но не превращайте это в культ «циклы — зло». Цикл — правильный инструмент, когда итерации зависят от предыдущих (текущее состояние, симуляции), когда вы выполняете побочные эффекты вроде записи файлов или просто когда цикл — та версия, которую вы и ваши читатели понимаете с первого взгляда. Понятный цикл лучше остроумного однострочника, который никто не может разобрать.
Что вы уносите с собой
for (x in v) { ... }выполняет тело один раз на элемент — без возни со счётчиком.- Внутри цикла печатайте явно через
print()илиcat(); голые выражения отбрасываются. - Обходите позиции через
seq_along(v), никогда через1:length(v)— последний делает два прохода на пустом векторе. - Выделяйте результат заранее через
numeric(n)/vector("list", n)и присваивайте по индексу; наращивание черезc()копирует всё на каждом проходе. nextпропускает проход,breakвыходит; предпочитайте векторизованные операции, когда они есть, но не бойтесь читаемого цикла.
Дальше: циклы, которые выполняются пока что-то не произойдёт, а не по одному разу на элемент, — while и repeat.
Часто задаваемые вопросы
Как написать цикл for в R?
for (x in v) { ... } — переменная цикла x по очереди принимает каждый элемент вектора (или списка) v, и тело выполняется один раз на элемент. Чтобы обходить позиции, а не значения, используйте for (i in seq_along(v)) и индексируйте через v[i].
Почему в R лучше seq_along, а не 1:length(v)?
Когда v пуст, length(v) равно 0, поэтому 1:length(v) превращается в 1:0 — вектор из двух элементов c(1, 0), — и тело цикла выполняется дважды для несуществующих элементов. seq_along(v) возвращает пустую последовательность для пустого вектора, поэтому цикл корректно выполняется ноль раз.
Почему мой цикл for в R ничего не печатает?
Автопечать в R происходит только для выражений на верхнем уровне в консоли. Внутри тела цикла голое выражение вроде x вычисляется и отбрасывается. Оберните его в print(x) или используйте cat(...), если хотите форматировать вывод сами.
Медленные ли циклы for в R?
Сам цикл нормален — медленным его делает наращивание вектора внутри через result <- c(result, ...), что копирует весь вектор на каждом проходе. Выделите память заранее через numeric(n) или vector("list", n) и присваивайте по индексу — и цикл будет работать прекрасно. При этом, если ту же идею выражает векторизованная операция или вызов из семейства apply, обычно это и быстрее, и понятнее.