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

НАЧАТЬ