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이라는 클래스를 생성해서 표준정규분포를 따르는 난수를 발생시킬 수 있도록 작성했습니다
n
과 vector
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으로 복제한 인스턴스의 값은 변하지 않는 것을 볼 수 있습니다