Menu

Циклы for в R: синтаксис, seq_along, break, next и когда не стоит писать цикл

Как работают циклы for в R — обход векторов и индексов, сбор результатов без ловушки растущего вектора, next и break и случаи, когда векторизованный вызов лучше цикла.

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

Базовый цикл 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, обычно это и быстрее, и понятнее.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ