В прошлой публикации мы разобрали 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 классы.


Подписывайтесь на мой канал R4marketing в Telegram и YouTube.