ООП в языке R (часть 2): R6 классы

от автора

В прошлой публикации мы разобрали S3 классы, которые являются наиболее популярными в языке R.

Теперь разберёмся с R6 классами, которые максимально приближённые к классическому объектно ориентированному программированию.

Содержание

Если вы интересуетесь анализом данных возможно вам будут интересны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.

  1. Введение
  2. Правила именования
  3. Создаём собственный класс
  4. Цепочка методов
  5. Методы $initialize() и $print()
  6. Добавление новых свойств и методов в класс после его определение, метод $set()
  7. Наследование
  8. Приватные методы и свойства
  9. Активные методы
  10. Финализатор класса
  11. Добавление R6 классов в пакет
  12. Полезные ссылки
  13. Заключение

Введение

В этой статье мы не будем останавливать на определении термина объектно — ориентированное программирование, и на его принципах, т.е. наследовании, инкапсуляции и полиморфизме.

R6 будут наиболее понятны пользователям Python, и тем кто привык к классическому ООП. В отличие от S3 классов, у R6 методы привязаны к самим объектам, в то время как у S3 всё строится на обобщённых (generic) функциях.

Итак, всё-таки небольшой глоссарий по ООП я предоставлю:

  • Класс — это шаблон, по которому мы можем создавать некоторые объекты. Например, классом может быть кот, собака, автомобиль и так далее.
  • Экземпляр класса — если класс это шаблон, в нашем случае пусть это будет кот, то экземпляр класса это конкретный объект созданный по шаблону. Т.е. мы можем создать любое количество котов, по созданному ранее шаблону.
  • Свойства класса — это переменные которые хранят информацию о каждом отдельном экземпляре класса, например кличка и порода кота.
  • Методы класса — это функции которые хранятся внутри класса, ну к примеру кот может есть, играться и так далее, всё это будут его методы.

Для работы с R6 классами вам необходимо изначально установить и подключить одноимённый пакет R6.

install.packages("R6") library(R6)

Правила именования

Вам необязательно придерживаться данных правил, но они являются общепринятыми:

  1. Имена классов задаются в UpperCamelCase.
  2. Имена методов объектов, и его свойства задаются в 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.

Полезные ссылки

Данная статья не является свободным переводом какой-либо англоязычной публикации, но при её написании я пользовался следующими источниками:

  1. Advanced R, Hadley Wickham
  2. R6: Encapsulated object-oriented programming for R, Winston Chang

Заключение

Как вы убедились R6 классы это реализация классического объектно ориентированного программирования в языке R.

Тем не менее данные класс используется в R достаточно редко, т.к. родной, и общепринятой реализацией ООП в R по-прежнему считаются S3 классы.

ссылка на оригинал статьи https://habr.com/ru/post/521310/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *