В прошлой публикации мы разобрали S3 классы, которые являются наиболее популярными в языке R.
Теперь разберёмся с R6 классами, которые максимально приближённые к классическому объектно ориентированному программированию.
Содержание
Если вы интересуетесь анализом данных возможно вам будут интересны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.
- Введение
- Правила именования
- Создаём собственный класс
- Цепочка методов
- Методы $initialize() и $print()
- Добавление новых свойств и методов в класс после его определение, метод $set()
- Наследование
- Приватные методы и свойства
- Активные методы
- Финализатор класса
- Добавление R6 классов в пакет
- Полезные ссылки
- Заключение
Введение
В этой статье мы не будем останавливать на определении термина объектно — ориентированное программирование, и на его принципах, т.е. наследовании, инкапсуляции и полиморфизме.
R6 будут наиболее понятны пользователям Python, и тем кто привык к классическому ООП. В отличие от S3 классов, у R6 методы привязаны к самим объектам, в то время как у S3 всё строится на обобщённых (generic) функциях.
Итак, всё-таки небольшой глоссарий по ООП я предоставлю:
- Класс — это шаблон, по которому мы можем создавать некоторые объекты. Например, классом может быть кот, собака, автомобиль и так далее.
- Экземпляр класса — если класс это шаблон, в нашем случае пусть это будет кот, то экземпляр класса это конкретный объект созданный по шаблону. Т.е. мы можем создать любое количество котов, по созданному ранее шаблону.
- Свойства класса — это переменные которые хранят информацию о каждом отдельном экземпляре класса, например кличка и порода кота.
- Методы класса — это функции которые хранятся внутри класса, ну к примеру кот может есть, играться и так далее, всё это будут его методы.
Для работы с R6 классами вам необходимо изначально установить и подключить одноимённый пакет R6
.
install.packages("R6")
library(R6)
Правила именования
Вам необязательно придерживаться данных правил, но они являются общепринятыми:
- Имена классов задаются в
UpperCamelCase
. - Имена методов объектов, и его свойства задаются в
snake_case
.
Так же как и в Python, методы класса могут получить доступ к другим его методам и свойствам через конструкцию self$method()
.
Создаём собственный класс
Из пакета R6
вы будете использовать всего одну функцию R6Class()
. Основные её аргументы:
classname
— имя класса, должно соответствовать переменной, в которую вы записываете класс;public
— принимает список (list()
) с публичными свойствами и методами класса.
library(R6)
# создаём класс Cat
Cat <- R6Class(classname = "Cat",
public = list(
name = "Tom",
breed = "Persian",
age = 3,
rename = function(name = NULL) {
self$name <- name
invisible(self)
},
add_year = function(ages = 1) {
self$age <- age + ages
invisible(self)
}
)
)
Мы создали класс Cat
, с тремя свойствами name
, breed
и age
, и двумя методами $rename()
и $add_year()
. Метод $rename()
меняет свойство name
, как я писал ранее к свойству метод может обращаться через self$name
, а метод $add_year()
увеличивает возраст кота на заданное количество лет.
Для создания экземпляра класса необходимо использовать встроенный метод $new()
:
# инициализируем объект класса Cat
tom <- Cat$new()
# смотрим результат
tom
<Cat>
Public:
add_year: function (ages = 1)
age: 3
breed: Persian
clone: function (deep = FALSE)
name: Tom
rename: function (name = NULL)
Используем метод $rename()
:
# используем метод rename
tom$rename('Tommy')
# смотрим результат
tom
<Cat>
Public:
add_year: function (ages = 1)
age: 3
breed: Persian
clone: function (deep = FALSE)
name: Tommy
rename: function (name = NULL)
Как видите объекты созданные с помощью R6
классов меняют свои компоненты на лету, т.е. нет необходимости создавать копии этих объектов.
Т.к. мы создали класс с публичными методами и свойствами то мы имеем к ним доступ вне класса, и соответственно можем их изменять.
# меняем свойство
tom$name <- 'Tom'
# смотрим результат
tom
<Cat>
Public:
add_year: function (ages = 1)
age: 3
breed: Persian
clone: function (deep = FALSE)
name: Tom
rename: function (name = NULL)
Цепочка методов
Если вы обратили внимание, то при создании методов мы всегда скрываем объект self
с помощью оператора invisible(self)
. Это делается для того, что бы мы могли использовать цепочку методов:
# используем цепочку методов
tom$add_year(1)$add_year(3)
# смотрим результат
tom
<Cat>
Public:
add_year: function (ages = 1)
age: 7
breed: Persian
clone: function (deep = FALSE)
name: Tom
rename: function (name = NULL)
Опять же такой подход хорошо знаком пользователям Python и JavaScript.
Методы $initialize() и $print()
Важные методы, которые вы наверняка будете использовать при создании R6 классов это методы $initialize()
и $print()
.
Метод $initialize() переопределяет стандартное поведение метода $new()
, т.е. используя его вы можете отдельно прокидывать в создаваемый экземпляр класса данные, например имя, породу и возраст кота.
$print()
переопределяет метод печати объекта в консоли.
# создаём класс Cat
Cat <- R6Class(classname = "Cat",
public = list(
name = NA,
breed = NA,
age = 0,
initialize = function(name, breed, age) {
self$name <- name
self$breed <- breed
self$age <- age
},
print = function(...) {
cat("<Cat>: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Age: ", self$age, "\n", sep = "")
cat(" Breed: ", self$breed, "\n", sep = "")
invisible(self)
},
rename = function(name = NULL) {
self$name <- name
invisible(self)
},
add_year = function(ages = 1) {
self$age <- self$age + ages
invisible(self)
}
)
)
# создаём экземпляр класса
tom <- Cat$new(name = 'Tom',
breed = 'Scottish fold',
age = 1)
# смотрим результат
tom
<Cat>:
Name: Tom
Age: 1
Breed: Scottish fold
Добавление новых свойств и методов в класс после его определение, метод $set()
Даже после определения класса вы в любой момент можете добавить в него свойства или методы, используя метод $set()
и указав уровень приватности.
Ниже приведён пример кода, в котором мы сначала создаём класс с основными методами, после чего через метод $set()
добавляем методы $rename()
и $add_year()
.
# создаём класс Cat
Cat <- R6Class(classname = "Cat",
public = list(
name = NA,
breed = NA,
age = 0,
initialize = function(name, breed, age) {
self$name <- name
self$breed <- breed
self$age <- age
},
print = function(...) {
cat("<Cat>: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Age: ", self$age, "\n", sep = "")
cat(" Breed: ", self$breed, "\n", sep = "")
invisible(self)
}
)
)
# добавляем метод rename
Cat$set( 'public',
'rename',
function(name = NULL)
{
self$name <- name
invisible(self)
})
# добавляем метод add_year
Cat$set( 'public',
'add_year',
function(ages = 1) {
self$age <- self$age + ages
invisible(self)
})
При этом, добавленные методы никак не изменят созданные до их добавления экземпляры класса, а будут распространяться лишь на те экземпляры класса, которые будут созданы после их добавления в класс.
Так же вы можете запретить переопределение методов класса, используя при его создании аргумент lock_class = TRUE
. В дальнейшем это поведение можно изменить, и разблокировать класс методом $unlock()
, и опять заблокировать методом $lock()
.
# Создаём класс с блокировкой переопределения методов
Simple <- R6Class("Simple",
public = list(
x = 1,
getx = function() self$x
),
lock_class = TRUE
)
# При попытке переопределить метод мы получим ошибку
Simple$set("public", "y", 2)
# Разблокируем класс
Simple$unlock()
# Теперь мы можем переопределять существующие свойства и методы класса
Simple$set("public", "y", 2)
# Повторно блокируем класс
Simple$lock()
Пример кода взят из официальной документации к пакету R6, автор Winston Chang
Наследование
R6 классы поддерживают механизм наследования, т.е. вы можете создавать супер классы и подклассы, которые будут наследовать от супер классов методы и свойства. При необходимости вы можете переопределять методы супер класса.
На изображении класс "Животные" является главным супер классом, его подклассом являются "Домашние животные" и "Дикие животные", т.е. они наследуют все свойства и методы класса (шаблона) животные, но могут их переопределять, а так же могут иметь свои дополнительные методы и свойства.
Далее мы создаём подклассы "Кот" и "Собака", для которых класс "Домашние животные" уже будет родительским, т.е. супер классом. Так же мы создаём классы "Олень" и "Медведь", для которых супер классом будет "Дикие животные".
Эту цепочку можно было продолжить породами котов, собак, оленей и медведей.
Для реализации наследования в R6 классах необходимо использовать аргумент inherit
.
library(R6)
# создаём супер класс Cat
Cat <- R6Class(classname = "Cat",
public = list(
name = NA,
breed = NA,
age = 3,
initialize = function(name, breed, age) {
self$name <- name
self$breed <- breed
self$age <- age
},
rename = function(name = NULL) {
self$name <- name
invisible(self)
},
add_year = function(ages = 1) {
self$age <- self$age + ages
invisible(self)
}
)
)
# создаём подкласс ScottishCat
ScottishCat <- R6Class("ScottishCat",
inherit = Cat,
public = list(
breed = "Scottish Fold",
add_year = function() {
cat("Увеличили возраст ", self$name, " на 1 год", sep = "")
super$add_year(ages = 1)
},
initialize = function(name, age, breed = "Scottish Fold") {
self$name <- name
self$breed <- breed
self$age <- age
}
)
)
# создаём экземпляр класса
scottish <- ScottishCat$new(name = 'Arnold', age = 1)
# используем метод подкласса
scottish$add_year()
В данном примере мы создали супер класс Cat
, и подкласс ScottishCat
. В подклассе мы переопредели метод $add_year()
, тем не менее, мы можем внутри подкласса использовать унаследованные от супер класса методы обращаясь к ним через super$method()
.
Приватные методы и свойства
В аргумент public
функции R6Class()
мы передаём методы и свойства класса с общим доступом, т.е. эти методы и свойства доступны как внутри класса, так и за его пределами.
Так же вы можете создавать приватные свойства и методы, которые будут доступны исключительно внутри класса. Такие свойства и методы необходимо передавать в аргумент private
, внутри класса доступ к приватным методам и свойствам осуществляется через private$methode_name
.
library(R6)
# создаём класс
User <- R6Class('User',
public = list(
name = NA,
initialize = function(name, password, credits = 100) {
self$name <- name
private$password <- password
private$credits <- credits
},
print = function(...) {
cat("<User>: ", self$name, sep='')
},
get_credits = function() {
cat(private$credits)
}
),
private = list(
password = NULL,
credits = NULL
)
)
# экземпляр класса
user_1 <- User$new('Alex', 'secretpwd')
# метод который использует приватное свойство
user_1$get_credits()
100
В данном случае мы получили значение приватного свойства credits
через специальный метод. Но если мы напрямую попробуем обратиться к данному свойству, то у нас ничего не получится:
# обращение к приватному свойству вне класса
user_1$credits
NULL
Активные методы
Активные методы для пользователя выглядят как свойства, но при обращении к ним они выполняют заданную функцию.
При каждом обращении к активному методу будет выполняться определённая функция, это удобно к примеру для запуска генератора случайных чисел, или выбора случайного значения из вектора.
Давайте вернёмся к нашему коту, и напишем класс, в котором будет свойство dictionary
, в котором будет вектор звуков, которые может воспроизводить кот. И активный метод $say()
, который случайным образом будет выводить одну из заданных фраз.
library(R6)
# создаём класс Cat
Cat <- R6Class(classname = "Cat",
public = list(
name = NA,
breed = NA,
dictionary = NA,
initialize = function(name, breed, dictionary) {
self$name <- name
self$breed <- breed
self$dictionary <- dictionary
}
),
active = list(
say = function(value) {
if (missing(value)) {
return(paste0(self$name, ' say ', sample(self$dictionary, size = 1)))
} else {
self$dictionary <- value
}
}
)
)
# создаём экземпляр класса
cat <- Cat$new('Tom',
'Persian',
c('meow', 'mrrr', 'frrr'))
# запускаем активный метод
cat$say
[1] "Tom say meow"
Как видите к активным методам мы обращаемся как к свойствам, т.е. без скобок и аргументов, но при этом выполняется функция say
.
В функции мы реализовали проверку if (missing(value))
, т.е. мы проверяем если идёт обращение к активному методу, то мы просто выводим случайную фразу из self$dictionary
. Если в активный метод передать значение, то будет выполняться условие else
, в нашем случае переопределение self$dictionary
.
# переопределяем свойство
cat$say <- c('grrr', 'waw', 'chfw')
# используем активный метод
cat$say
"Tom say grrr"
Финализатор класса
Вы можете добавить в класс специальный метод $finalize
, который будет запускаться после удаления объекта при завершении R сессии.
Это полезно в тех случаях когда ваш класс работает с файлами или базами данных, тогда вы можете написать финализатор для того, что бы быть уверенным, что соединение с файлом или базой будет разорвано.
TemporaryFile <- R6Class("TemporaryFile", list(
path = NULL,
initialize = function() {
self$path <- tempfile()
},
finalize = function() {
message("Cleaning up ", self$path)
unlink(self$path)
}
))
Пример кода взят из книги Advanced R, автор Hadley Wickham
Добавление R6 классов в пакет
Ещё одно отличие R6 классов от S3 заключается в том, что вам не надо прописывать ваши R6 классы в фале NAMESPACE. Достаточно просто включить пакет R6 в поле Imports
файла DESCRIPTION.
Полезные ссылки
Данная статья не является свободным переводом какой-либо англоязычной публикации, но при её написании я пользовался следующими источниками:
Заключение
Как вы убедились R6 классы это реализация классического объектно ориентированного программирования в языке R.
Тем не менее данные класс используется в R достаточно редко, т.к. родной, и общепринятой реализацией ООП в R по-прежнему считаются S3 классы.
Подписывайтесь на мой канал R4marketing в Telegram и YouTube.
kablag
В своё время сделал пакет, который основан на R6 классах RDML. Штука прикольная и для «серьёзных» программистов больше всего напоминает стандартные классы из ООП. Однако второй раз я бы так не делал :) Основная причина – скорость создания объектов. Сейчас бы я, наверное, сделал просто S3.
Из важного к статье можно добавить про то, что может быть непривычно для R: копирование объекта не создаёт новый объект, а только ссылку на старый.
Про это подробней тут
selesnow Автор
Спасибо большое, действительно надо будет добавить эту информацию в статью, и я это обязательно в ближайшем будущем сделаю.
Так же мне рекомендовали добавить сравнение с
refClass
, в общем у статьи будет апдейт 100%.