R6 basics

by Minho Lee — on  , 

cover-image

R6

R6는 기존에 존재하던 RC(Reference Class)와 유사한 Class의 기능을 제공합니다. 하지만 RC와는 다르게 S4 시스템에 의존하지 않으면서 RC보다 더 효율적으로 동작합니다

R에서는 기본적으로 copy-on-modify가 적용됩니다. 다시 말하자면, object를 조금이라도 수정하게 되면 object를 복사한 다음에 수정을 하게 됩니다. 하지만 RC, R6의 경우에는 copy-on-modify가 적용되지 않기 때문에 주의해서 사용한다면 메모리를 더 효율적으로 사용할 수 있습니다. 기존 S3나 S4와는 크게 다른 성질을 가지기 때문에 주어진 상황에 적합한 방식을 선택하여 사용할 수 있습니다

그리고 다른 객체지향 언어에 익숙한 사람들이 더 쉽게 R에 익숙해질 수 있을 것 같습니다




R6 : Basics

R6로 클래스를 생성하는 방법을 알아보도록 하겠습니다. R6에서 클래스는 R6Class라는 함수를 통해서 만들 수 있습니다.

첫번째 요소는 class의 이름입니다. 인스턴스를 생성했을 때 class() 함수를 통해 class를 확인하면 classname 요소와 R6가 출력됩니다.

그리고 public에는 public member들을 리스트의 형태로 담을 수 있습니다



Random이라는 클래스를 생성해서 표준정규분포를 따르는 난수를 발생시킬 수 있도록 작성했습니다

nvector field는 NA값으로 초기화를 시킵니다.

initialize 메소드 안에 있는 내용들은 new()메소드를 통해 처음 인스턴스를 생성할 때 시행됩니다.

클래스 내부에서 public member에 접근하기 위해서는 self$member, self$method()와 같은 방식을 사용합니다

library(R6)
Random = R6Class("Random",
  public = list(
    n = NA,
    vector = NA,
    initialize = function(n){
      self$n = n
    },
    gen_normal = function(){
      self$vector = rnorm(self$n)
    }
  ))



클래스의 인스턴스를 생성하기 위해서는 $new() 를 사용합니다

rd = Random$new(10)

Public member에 접근하기 위해서는 $를 사용하면 됩니다. 기존의 리스트나 data.frame의 열에 접근할 때와 비슷한 방식으로 사용할 수 있습니다

rd$n
## [1] 10
rd$vector
## [1] NA
rd$gen_normal()
rd$vector
##  [1]  1.60938489  0.22661340  0.39826320  0.62156489 -0.71540379
##  [6]  0.26136030  0.51512304  0.02617344 -0.81198997  0.42580841




Private members

Class 내부에 private member를 추가할 수 있습니다. public member들이 self$n과 같은 형태로 접근가능했다면, private member들은 private$sample_mean의 형태로 접근할 수 있습니다



private member로 sample_mean이라는 항목을 만들어서 초기화시킵니다. calc_sp_mean, print_sp_mean이라는 메소드를 추가해서 private member에 값을 저장하고 불러올 수 있게 합니다

Random = R6Class("Random",
   public = list(
     n = NA,
     vector = NA,
     initialize = function(n){
       self$n = n
     },
     gen_normal = function(mu = 0, sd = 1){
       self$vector = rnorm(self$n, mu, sd)
     },
     calc_sp_mean = function(){
       private$sample_mean = mean(self$vector)
     },
     print_sp_mean = function(){
       print(private$sample_mean)
     }
   ),
   private = list(
     sample_mean = NA
   )
)

rd = Random$new(10)
rd$gen_normal()
rd$calc_sp_mean()
rd$print_sp_mean()
## [1] -0.3280799


private member로 설정하게 되면 외부에서 직접적으로 호출할 수 는 없습니다

rd$sample_mean
## NULL




Active bindings

Active binding의 요소들은 접근할 때마다 함수를 불러옵니다. 해당 요소들은 항상 외부에서 접근할 수 있습니다(public)


아래 코드는 npp요소에 접근할 때마다 n의 값이 1씩 증가하도록 되어있습니다. 그리고 npp 요소에 값을 입력하려고 하면 n의 값이 해당 값으로 변경됩니다

Count = R6Class('Count',
  public = list(
    n = 0
  ),
  active = list(
    npp = function(value){
      if(missing(value)){
        self$n = self$n + 1
        return(self$n)
      } else {
        self$n = value
        print(self$n)
      }
    },
    nmm = function(){
      self$n = self$n - 1
      print(self$n)
    }
  ))
n_count = Count$new()
n_count$n
## [1] 0
n_count$npp
## [1] 1
n_count$npp
## [1] 2
n_count$npp
## [1] 3


이 경우 처럼 npp 요소에 값을 할당하려고 하면 해당 값을 함수의 value 인자로 간주하고 함수를 실행시킵니다.

n_count$npp = 8
## [1] 8


active binding의 함수가 인자를 갖지 않을 경우에는 위와 같은 방식으로 값을 할당할 수 없습니다

n_count$nmm = 5
# Error in (function ()  : unused argument (quote(5))




Inheritance

R6 class는 다른 class로부터 상속받을 수 있습니다. 자식 클래스(subclass)는 부모 클래스(superclass)로부터 메소드를 받아서 기능을 변경하거나, 추가적인 메소드를 생성할 수 있습니다


다음 예제에서는 맨 처음 만들었던 Random class를 상속받아서 gen_normal의 기능을 확장시키도록 하겠습니다. Random에서는 rnorm에 평균과 표분편차를 입력하지 않았습니다. 그래서 평균 0, 분산 1의 기본값이 적용된 표준정규분포를 따르는 난수가 생성되었습니다. 이번에는 gen_normal 메소드에 평균과 표준편차를 입력할 수 있도록 함수를 변경하겠습니다

상속은 Class를 생성할 때 inherit 요소에 상속받으려는 R6 클래스를 입력하는 방식으로 동작합니다

RandomNormal = R6Class("RandomNormal",
  inherit = Random, # 여기서 Random class를 상속받습니다
  public = list(
    gen_normal = function(mu, sd){
      self$vector = rnorm(self$n, mu, sd)
    }
  ))
rdn = RandomNormal$new(10)
rdn$gen_normal(20, 2)
rdn$vector
##  [1] 18.40037 18.78328 20.72541 17.22007 19.16256 21.45393 19.95351
##  [8] 16.35129 14.99379 17.76512
rdn$n
## [1] 10



부모 클래스의 메소드는 super$method() 의 형태로 호출할 수 있습니다. 아래 예제에서는 gen_normal() 메소드를 실행시킬 때 표본평균까지 볼 수 있도록 RandomNormal class를 수정하겠습니다

RandomNormal = R6Class("RandomNormal",
   inherit = Random,
   public = list(
     gen_normal = function(mu, sd){
       self$vector = rnorm(self$n, mu, sd)
       super$calc_sp_mean()
       super$print_sp_mean()
     }
   ))


rdn = RandomNormal$new(10)
rdn$gen_normal(20, 2)
## [1] 20.71161

Copy-on-modify vs Modify in place

글을 시작할 때 R6는 R에서 기본적으로 사용하는 copy-on-modify가 적용되지 않는다고 말씀드렸습니다. 아래에서는 실제 예제를 통해서 살펴보겠습니다

보통 R에서 동작하는 방식은 다음과 같습니다

a = 10

b = a

a = 20

b
## [1] 10

보셨다시피 b = a를 실행시키는 순간 b는 a의 값을 그대로 받지만 이것은 복사된 객체이기 때문에 이후에 a를 변경하는 작업은 b에 영향을 주지 못합니다. 하지만 이러한 객체의 복사가 모든 영역에서 발생한다면, 큰 데이터를 다루는 작업에서는 성능에 큰 영향을 미칠 수도 있습니다



R6 class에서는 어떻게 되는지 살펴보겠습니다. 상수값 한 개를 가지는 Constant class를 생성합니다

Constant = R6Class("Constant",
  public = list(
    a = 10
  ))

Constant class의 인스턴스를 하나 생성합니다

r6_a = Constant$new()

생성한 인스턴스를 통해서 다른 인스턴스를 하나 더 만듭니다

r6_b = r6_a

새로 만든 인스턴스의 값을 확인합니다

r6_b$a
## [1] 10

먼저 생성한 인스턴스의 값을 변경해보겠습니다

r6_a$a = 20

나중에 만든 인스턴스도 함께 값이 변경된 것을 볼 수 있습니다

r6_b$a
## [1] 20

만약 기존처럼 서로 연결되지 않는 별개의 인스턴스를 만들고 싶다면 clone() 메서드를 사용하면 됩니다

r6_c = r6_a$clone()

r6_c$a
## [1] 20
r6_a$a = 30

r6_c$a
## [1] 20

그러면 기존 인스턴스의 값을 변경하더라도 clone으로 복제한 인스턴스의 값은 변하지 않는 것을 볼 수 있습니다

Comments