May 22: Функции и циклы

Запись занятия

Запись занятия, которое было 22 мая, можно найти здесь:

Все записи организованы в плейлист


Циклы

Наряду с условными операторами, циклы в R аналогичны циклы в других языках программирования. Три основных вида циклов: for, while и repeat. Циклы задаются с помощью оператора цикла, последовательности или условия, ограничивающих работу цикла и, собственно, выполняемого выражения. Если это одна строка, то выражение можно не заключать в фигурные скобки, во всех прочих случаях фигурные скобки необходимы.

Циклы традиционно редко используются в R, в немалой части это вызвано спецификой использования памяти во время выполнения выражений в цикле, точнее, не очень эффективным кодом. Для циклов существуют альтернативы — векторизованные вычисления и неявные циклы, а так же оптимизация кода путем преаллокации памяти или параллелизации.

for

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

for (i in letters[1:3]) {
  cat('letter', i, '\n')
}
## letter a 
## letter b 
## letter c
cat('i =', i)
## i = c


while (Advanced)

Циклы while и repeat используются намного реже. Если в цикле for количество циклов определяется длиной заданной последовательности, то в while количество циклов может быть бесконечным, до тех пор, пока поставленное условие будет верным.

Для цикла надо задать начальное значение счетчика циклов, задать условие для этого счетчика и не забыть дополнить тело цикла увеличением счетчика при каждой итерации. Либо же добавить любое другое изменение значения счетчика, которое может привести срабатыванию условия. Второй вариант цикла while — это сначала создать объект с логическим значением TRUE и его поставить в условие, а потом прописать в теле цикла, что при определённых условиях значение сменится на FALSE, что и приведет к остановке цикла.

Выведем первые три элемента вектора letters с помощью цикла while.

i <- 1
while (i < 4) {
  my_l <- letters[i]
  cat('letter', my_l, '\n')
  i <- i + 1
}
## letter a 
## letter b 
## letter c
cat('i =', i)
## i = 4

Как правило, while нужен тогда, когда надо подсчитать количество попыток до какого-то результата, либо же неизвестно, сколько попыток может потребоваться. Самый показательный пример: сбор данных с POST-запросом, когда сервер может не отвечать, соединение может рваться, и так далее.

repeat (Advanced)

Цикл repeat схож с циклом while, только он выполняется до тех пор, пока пока при выполнении выражения не будет достигнут желаемый результат и не будет вызвана команда прерывания цикла. На том же примере с буквами:

i <- 1
repeat {
    my_l <- letters[i]
    if (i == 4) {
      break()
    } else {
      cat('letter', my_l, '\n')
      i <- i + 1
    }
}
## letter a 
## letter b 
## letter c
cat('i =', i)
## i = 4


Прерывание циклов (Advanced)

В какие-то моменты возникает необходимость прервать цикл или же пропустить последующие действия и начать новую итерацию цикла. Для этих целей используют функции break() и next(). Выше в цикле repeat мы уже использовали break(), вот еще один пример цикла с прерыванием:

for (i in letters[1:10]) {
  cat(i, '\n')
  if (i == 'c')
    break()
}
## a 
## b 
## c

Эффективнее всего функции прерывания во вложенных циклах — если прервать выполнение вложенного цикла, то родительский цикл не будет прерван и продолжит итерироваться.

Cоздание функций

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

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

my_fun <- function(arg1, arg2) {
  # тело функции, операции, перемножаем переданные значения
  tmp <- arg1 * arg2
  
  # возвращаем результат
  return(tmp)
}

В этом примере создания функции:

  • Выражение my_fun <- function(arg1, arg2) — это создание объекта-функции под названием my_fun
  • arg1 и arg2 — два аргумента функции (функция может принять два разных значения и произвести над ними какие-то операции)
  • код в фигурных скобках - собственно тело функции, набор операций, которые должны совершаться над переданными значениями.
  • return(tmp) — результат выполнения функции, который будет передан в глобальное окружение.

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


Аргументы функции

Аргументы функции указываются в круглых скобках при определении функции. В теле функции имена аргументов служат своего рода абстрактными названиями для любых объектов, которые переданы в аргументы при вызове функции. Собственно, “передать какое-то значение в аргумент функции” означает, что при выполнении функции над этим объектом будут проведены те операции, которые в коде (теле) функции проводятся над этим аргументом. В принципе, выражения “передать значение в аргумент” тождественно “использовать значение в качестве аргумента”, второе, возможно, даже более корректно.

Простейший пример функции с одним аргументом. Функция вычисляет квадратный корень и округляет результат до второго знака:

# создаем функцию
my_fun <- function(x) {
  z <- sqrt(x)
  z <- round(z, 2)
  z
}

# используем функцию
my_fun(5)
## [1] 2.24

Как правило, все функции имеют свой набор аргументов, однако в редких случаях возможно создание функций вообще без аргументов. В таких случаях функции либо вычисляют и возвращают какое-то определенное значение, либо производят какие-то операции с объектами родительского окружения. Оба эти варианта, следует уточнить, крайне не рекомендуются к использованию, так как либо бессмысленны и усложняют код, либо просто вредны и некорректны с точки зрения R. Редкими примерами осмысленного использования функций без аргументов могут послужить функции getwd(), Sys.time() и им подобные.

Ниже пример функции, которая вычисляет квадратный корень из числа 5 и округляет его до второго знака:

# создаем функцию
my_fun <- function() {
  x <- sqrt(5)
  x <- round(x, 2)
  x
}

# используем функцию
my_fun()
## [1] 2.24


Значения аргументов по умолчанию (Advanced)

Нередко в практике встречаются ситуации, когда один из аргументов функции принимает какое-то определенное значение (или значение из определенного вектора значений) намного чаще, чем все прочие возможные значения. В таких случаях разумно задать значение этому аргументу по умолчанию - то есть, если не указано обратное, то будет использоваться заданное значение. Например, функция sort() имеет значение аргумента decreasing по умолчанию равное FALSE. Соответственно, если не задавать этот аргумент, то функция сортирует вектор по возрастанию. И наоборот, если нужна сортировка по убыванию, следует прямо задать значение аргумента decreasing = TRUE:

sort(1:5)
## [1] 1 2 3 4 5
sort(1:5, decreasing = TRUE)
## [1] 5 4 3 2 1

Если посмотреть в справке описание аргументов функции sort(), то видно, что аргументу x никакое значение не передается, а аргументу decreasing передается значение FALSE.

args(sort)
## function (x, decreasing = FALSE, ...) 
## NULL

Собственно, таким образом и задаются значения по умолчанию: при объявлении функции аргументу уже передается какое-то значение. Например, функция ниже умножает значение, переданное в качестве первого аргумента, на 2, если значение второго аргумента не указано

# объявляем функцию my_fun, которая перемножает два переданных объекта
# если второй аргумент не указан, то считаем, что он равен 2
my_fun_def <- function(arg1, arg2 = 2) {
  tmp <- arg1 * arg2
  return(tmp)
}

Используем созданную функцию и в аргумент, у которого есть значение по умолчанию, ничего не передаем (игнорируем его):

# не указываем аргумент
x <- 9
my_fun_def(x)
## [1] 18


Тело функции

Тело функции — это код на языке R, который описывает действия, которые необходимо совершить над объектами. Соответственно, когда функция вызывается, этот набор действий применяется к тем объектам, которые были переданы в аргументы функции. Как правило, код (тело) функции заключается в фигурные скобки. Однако если тело состоит из одного выражения, то фигурные скобки можно опустить:

# объявляем функции
my_fun1 <- function(x) {x ^ 5}
my_fun2 <- function(x) x ^ 5

# вызываем функции
my_fun1(5)
## [1] 3125
my_fun2(5)
## [1] 3125

Код функции, как минимум, описывает обязательные действия над объектами. Тем не менее, для повышения устойчивости и абстрактности функции рекомендуется использовать различные дополнительные инструменты - например, проверку аргументов, обработку ошибок, а так же проверку типов переданных объектов, информирование пользователя о каких-то процессах и так далее.


Результат функции

Почти все функции в результате своей работы возвращает один объект. Объектом может быть вектор значений, список, таблица, другая функция и так далее. Для того, чтобы указать, какой именно объект должна вернуть функция, используется return() и, что важно, использовать эту функцию можно в любом месте тела функции. Впрочем, возможен и более лаконичный вариант, когда самой последней строчкой тела функции указывается название возвращаемого объекта. Следует учитывать, что это должно быть именно имя объекта или какое-то выражение, создающее новый объект (*pply-функции, function(), data.frame() и так далее), за исключением операции присвоения:

# используем return(x) в середине кода
my_fun1 <- function(x) {
  x <- x ^ 3
  return(x)
  x <- x * 2
}

# возвращаем x просто последней строчкой
my_fun2 <- function(x) {
  x <- x ^ 3
  x
}

# проверяем
my_fun1(2)
## [1] 8
my_fun2(2)
## [1] 8

Функции возвращают только один объект (или же вообще ничего не возвращают в результате своей работы). Если необходимо, чтобы функция возвращала несколько разных значений или объектов, в таком случае необходимо их все собрать в список (list()) или таблицу (любой вариант: data.table, data.frame, tibble).

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

my_f <- function(x, y) {
  mult <- x * y
  pw <- x ^ y
  result <- list(
    x_y_mult = mult, 
    x_y_power = pw
  )
  return(result)
}

my_res <- my_f(3, 4)
str(my_res)
## List of 2
##  $ x_y_mult : num 12
##  $ x_y_power: num 81
my_res$x_y_mult
## [1] 12
my_res$x_y_power
## [1] 81


Дополнительные материалы


Домашние задания

level 1 (IATYTD)

  • напишите функцию, которая добавляет к переданному в аргумент x значению строку x =. Вам потребуется еще функция paste(). То есть:
my_f(5)
## [1] "x =  5"


level 2 (HNTR)

  • прочитайте справку по функции list.files() и/или list.dirs(). Импортируйте названия файлов в какой-нибудь из ваших папок (или названия подпапок). В цикле выведите на печать первые пять названий. Например:
## [1] NA
## [1] NA
## [1] NA
## [1] NA
## [1] NA


level 3 (HMP)

  • Создайте функцию, которая возвращает среднее и стандартное отклонение вектора, переданного в аргумент x, а также высчитывает медиану и моду.


level 4 (UV)

  • Разберитесь и прокомментируйте, что и зачем происходит в этом отрывке скрипта.
library(rvest)
library(data.table)

article_url <- 'https://ecsoc.hse.ru/2020-21-1/337414467.html'

article_fetcher <- function(article_url) {
  page <- read_html(article_url)
  
  path_author <- html_element(page, xpath = '//div[@class="centercolumn"]/h3/i') %>% html_text()
  path_author <- paste(path_author, collapse = ',')
  
  path_title <- html_element(page, xpath = '//div[@class="centercolumn"]/h2[@class="article-header"]') %>%
    html_text()
  path_ann <- html_element(page, xpath = '//div[@class="centercolumn"]/div[@class="annot"]' ) %>%
    html_text() 
  path_keywords <- html_element(page, xpath = '//div[@class="centercolumn"]/div[@class="keywords"]') %>%
    html_text()
  
  article_content <- data.table(
    author = path_author,
    title = path_title,
    annotation = path_ann,
    keywords = path_keywords,
    url = article_url
  )
  return(article_content)
}


level 5 (N)

Напишите функцию, которая принимает на вход название пакета в строковом виде, а на выходе возвращает табличку с колонками: package (название пакета), publish_date (дата публикации), version (версию пакета), reference_manual (ссылку на мануал). Вся информация берется с страницы пакета на сайте CRAN, ссылка на страницу формируется по маске https://CRAN.R-project.org/package=PACKAGENAME, где вместо PACKAGENAME - название пакета.