В прошлой публикации мы разобрали 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 классы.
ссылка на оригинал статьи https://habr.com/ru/post/521310/
Добавить комментарий