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
- название пакета.