Object Oriented Programming in R

R 베이스타입

  • 모든 베이스 R 개체는 그 객체가 메모리에 저장되는 방법을 기술하는 C 구조가 있다.
  • 베이스 타입은 R 코어팀만 만들 수 있다. 실제 객체 시스템은 아니다.
  • typeof 함수는 R base type 을 반환한다. (vector, list, function, builtin)
typeof(c("a")) # character vector 
#> [1] "character"
typeof(mean) # 함수는 closure
#> [1] "closure"
typeof(sum) # 원시함수는 built in 
#> [1] "builtin"
typeof(abs) # builtin 
#> [1] "builtin"
typeof(pnorm) # closure 
#> [1] "closure"
typeof(array(c(1.1, 2.2, 3.3), 3)) 
#> [1] "double"
typeof(matrix(1:3, 3)) 
#> [1] "integer"
df <- data.frame(x = 1:10, y = letters[1:10]) # dataframe 의 base type 은 list 이다. 
typeof(df)
#> [1] "list"

S3

  • S3는 가장 단순한 OO 시스템이고, base와 stats 패키지에서 사용된 유일한 OO 시스템이다.
  • CRAN 에 있는 많은 패키지들에서 가장 공통적을 사용되는 시스템이다.
  • pryr 패키지의 otype 함수는 해당 객체가 base type 그 자체인지 어떤 base type 에 기반한 S3 인지 S4 등 인지를 출력한다.
library(pryr)
df <- data.frame(x = 1:10, y = letters[1:10])
otype(df) # data frame은 S3 객체이다. 
#> [1] "S3"
otype(c("A")) # vector는 S3 객체가 아니다. R 베이스타입이다. 
#> [1] "base"
otype(c(1,2,3)) 
#> [1] "base"
otype(array(1:3, 3)) # array는 R 베이스타입이다.
#> [1] "base"
otype(matrix(1:3, 3)) # matrix 는 R 베이스타입이다. 
#> [1] "base"
otype(list(a=c(1:3))) # list 는 R 베이스타입이다. 
#> [1] "base"
otype(factor(1)) # factor는 S3 객체이다. 
#> [1] "S3"

S3 클래스 정의하기

# 한 번에 클래스를 생성하고 할당하기
# structuer(base object, class = class_name)
# structure 함수는 해당 attribute 를 갖는 객체를 만드는 함수이다. 
foo <- structure(list(), class="foo") 
foo
#> list()
#> attr(,"class")
#> [1] "foo"

# 클래스를 생성하고 난 후 설정하기
foo <- list()
class(foo) <- "foo"

상속 (Inheritance)

inherits(foo, "foo") # foo 객체가 foo class 를 상속하는지 체
#> [1] TRUE
  • 예를들어, glm 은 lm 클래스의 하위 클래스이다.
model <- glm(mpg ~ cyl + hp, data=mtcars) 
class(model) # glm class 는 이와 같이 상속받은 lm 클래스를 class 에 포함하고 있다. 
#> [1] "glm" "lm"
inherits(model, "lm") 
#> [1] TRUE
  • lm 에는 없고, glm 에는 있는 generic function 찾아보기
methods(class="glm") # lm 에 추가하여 glm 에 추가로 정의된 generic function 이다. 
#>  [1] add1           anova          coerce         confint       
#>  [5] cooks.distance deviance       drop1          effects       
#>  [9] extractAIC     family         formula        influence     
#> [13] initialize     logLik         model.frame    nobs          
#> [17] predict        print          residuals      rstandard     
#> [21] rstudent       show           slotsFromS3    summary       
#> [25] vcov           weights       
#> see '?methods' for accessing help and source code
methods(class="lm") 
#>  [1] add1           alias          anova          case.names    
#>  [5] coerce         confint        cooks.distance deviance      
#>  [9] dfbeta         dfbetas        drop1          dummy.coef    
#> [13] effects        extractAIC     family         formula       
#> [17] hatvalues      influence      initialize     kappa         
#> [21] labels         logLik         model.frame    model.matrix  
#> [25] nobs           plot           predict        print         
#> [29] proj           qr             residuals      rstandard     
#> [33] rstudent       show           simulate       slotsFromS3   
#> [37] summary        variable.names vcov          
#> see '?methods' for accessing help and source code

클래스의 생성자 만들기

  • 생성자 함수는 일반적으로 클래스와 동일한 이름을 갖도록 한다.
foo <- function(x) { 
  if (!is.numeric(x)) stop("X must be numeric")
  structure(list(x), class="foo")
}
  • 생성자를 통해 변수 생성하기
foo_var <- foo(c(1,2,3)) 
print(foo_var)
#> [[1]]
#> [1] 1 2 3
#> 
#> attr(,"class")
#> [1] "foo"
str(foo_var) 
#> List of 1
#>  $ : num [1:3] 1 2 3
#>  - attr(*, "class")= chr "foo"
typeof(foo_var)
#> [1] "list"
otype(foo_var)
#> [1] "S3"
class(foo_var)
#> [1] "foo"
length(foo_var) # length generic function 의 default 가 실행되며, 여기서 base type 이 list 임을 확인하고, list 의 길이 1을 반환한다.
#> [1] 1
  • R 에서는 기존 객체의 클래스를 변경할 수 있다.
mod <- lm(log(mpg) ~ log(disp), data = mtcars) 
class(mod) 
#> [1] "lm"
typeof(mod)  
#> [1] "list"
print(mod)
#> 
#> Call:
#> lm(formula = log(mpg) ~ log(disp), data = mtcars)
#> 
#> Coefficients:
#> (Intercept)    log(disp)  
#>      5.3810      -0.4586
  • lm 클래스에 데이터프레임 class 를 추가하기
class(mod) <- "data.frame"
print(mod) # data frame s3 method 를 호출한다. 
#>  [1] coefficients  residuals     effects       rank          fitted.values
#>  [6] assign        qr            df.residual   xlevels       call         
#> [11] terms         model        
#> <0 rows> (or 0-length row.names)
mod$coefficients
#> (Intercept)   log(disp) 
#>   5.3809725  -0.4585683

S3 새로운 메소드와 제너릭 생성하기

  • 새로운 제너릭을 만들고 싶을 대, UseMethod() 를 call 하는 function 을 만든다.
  • UseMethod 는 제너릭 메소드의 이름과, argument를 input 으로 받는다.
f <- function(x) UseMethod("f") # 이렇게하면 f 라는 이름의 제너릭이 생성된 것이다. 

# 제너릭은 메소드가 없다면 쓸모가 없다. 아래와 같이 제너릭 메소드를 구현할 수 있다.
f.a <- function(x) "Class a" 
a <- structure(list(), class = "a") 
class(a) 
#> [1] "a"
  • 제너릭 함수의 호출
f(a)
#> [1] "Class a"
  • 원래 있는 제너릭에 메소드를 추가하기
mean.a <- function(x) "a"
mean(a)
#> [1] "a"

메소드 디스패치

  • S3에서 메소드 디스패치하는 법은 심플하다.
  • default 메소드를 정의하면 해당 클래스에 대한 메소드가 없을 경우 실행됨
f <- function(x) UseMethod("f") 
f.a <- function(x) "Class a" 
f.default <- function(x) "Unknown class" 

f(structure(list(), class = "a"))
#> [1] "Class a"
# b메소드에 대한 메소드가 없기 때문에 a 클래스에 대한 행된다. 
f(structure(list(), class = c("b", "a")))
#> [1] "Class a"
# c class에 대한 메소드 구현이 없기 때문에 default 메소드가 실행된다.
f(structure(list(), class=c("c"))) 
#> [1] "Unknown class"
c <- structure(list(), class = "c")
# Call the correct method:
f.default(c)
#> [1] "Unknown class"
  • 다른 클래스의 제네릭 메소드를 실행할 수도 있다.
f.a(c)
#> [1] "Class a"
  • S3 object가 아닌 것도, S3 제너릭 메소드를 실행할 수 있다.
  • 이 경우 R base type 을 이용해 메소드를 실행한다.
  • 이 R base type 을 알아내는 것은 힘들 수 있지만 아래와 같은 함수로 가능하다.
iclass <- function(x) {
  if (is.object(x)) {
    stop("x is not a primitive type", call. = FALSE)
  }

  c(
    if (is.matrix(x)) "matrix",
    if (is.array(x) && !is.matrix(x)) "array",
    if (is.double(x)) "double",
    if (is.integer(x)) "integer",
    mode(x)
  )
}
iclass(matrix(1:5)) 
#> [1] "matrix"  "integer" "numeric"
iclass(array(1.5))
#> [1] "array"   "double"  "numeric"

Group 제너릭

  • Group 제너릭이라는 것도 있는데 상당히 advanced 된 내용이다.
  • 여러개의 제너릭들을 한데 모아, 다양한 클래스의 제너릭 메소드를 정의함
  • 예를 들어, abs, sign, sqrt 등의 제너릭은 Math라는 이름의 그룹 제너릭이다.

Exercises

  • Read the source code for t() and t.test() and confirm that t.test() is an S3 generic and not an S3 method. What happens if you create an object with class test and call t() with it
# t 는 원래 matrix 나 datafame 등을 받아, transpose 를 return 하는 함수이다. 
array(1:6, list(2,3))
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
t(array(1:6, list(2,3)))
#>      [,1] [,2]
#> [1,]    1    2
#> [2,]    3    4
#> [3,]    5    6
# t.test 는 제너릭이다.
a <- structure(c(1,2,3,4,5), class="test")

# 해당 코드는 one sample t-test 를 실행한다! 
# test 클래스를 보고, t.test 를 실행하기 때문 
# 이것은 t.test가 t 제너릭 메소드가 아니라 제너릭이기 때문이다. 
# 만약에 t.test가 t의 제너릭 메소드였다면, t(c(1,2,3,4,5)) 가 실행될 것이다. 
# 따라서 어떤 generic 을 생성할 때는 .을 포함하지 않도록 하는 것이 좋다. 
# t 를 실행하고 싶은데, t.test가 실행되는 등의 현상이 발생할 수 있다. 
t(a)
#> 
#>  One Sample t-test
#> 
#> data:  a
#> t = 4.2426, df = 4, p-value = 0.01324
#> alternative hypothesis: true mean is not equal to 0
#> 95 percent confidence interval:
#>  1.036757 4.963243
#> sample estimates:
#> mean of x 
#>         3
# 이런것도 가능하다. 
t.test <- function(a){
  print(a)
}
t(a)
#> [1] 1 2 3 4 5
#> attr(,"class")
#> [1] "test"
rm(t.test)
  • What classes have a method for the Math group generic in base R? Read the source code. How do the methods work?
  • abbc, sign, sqrt 등은 “Math” 그룹 제너릭이다.

Group “Math”:

abs, sign, sqrt, floor, ceiling, trunc, round, signif … 등등

methods(Math)
#> [1] Math,nonStructure-method Math,structure-method   
#> [3] Math.Date                Math.POSIXt             
#> [5] Math.data.frame          Math.difftime           
#> [7] Math.factor             
#> see '?methods' for accessing help and source code
  • R has two classes for representing date time data, POSIXct and POSIXlt, which both inherit from POSIXt. Which generics have different behaviours for the two classes? Which generics share the same behaviour?
methods(class="POSIXt") # 세 클래스에서 공통으로 정의된 제너릭 메소드
#>  [1] +            -            Axis         Math         Ops         
#>  [6] all.equal    as.character coerce       cut          diff        
#> [11] hist         initialize   is.numeric   julian       months      
#> [16] pretty       quantile     quarters     round        seq         
#> [21] show         slotsFromS3  str          trunc        weekdays    
#> see '?methods' for accessing help and source code
methods(class="POSIXct") # POSIXct 의 제너릭 메소드. 이 메소드는 POSIXt 에 추가하여 POSIXct 에서 새롭게 구현된 것이다.
#>  [1] Summary       [             [<-           [[            as.Date      
#>  [6] as.POSIXlt    as.data.frame as.list       c             coerce       
#> [11] format        initialize    length<-      mean          print        
#> [16] rep           show          slotsFromS3   split         summary      
#> [21] weighted.mean xtfrm        
#> see '?methods' for accessing help and source code
methods(class="POSIXlt") # POSIXlt 의 제너릭 메소드. 메 메소드는 마찬가지로 POSIXlt 에서 새롭게 구현된 것이다. 
#>  [1] Summary       [             [<-           [[            anyNA        
#>  [6] as.Date       as.POSIXct    as.data.frame as.double     as.list      
#> [11] as.matrix     c             coerce        duplicated    format       
#> [16] initialize    is.na         length        length<-      mean         
#> [21] names         names<-       print         rep           show         
#> [26] slotsFromS3   sort          summary       unique        weighted.mean
#> [31] xtfrm        
#> see '?methods' for accessing help and source code
  • Which base generic has the greatest number of defined methods?
library("methods") 
objs <- mget(ls("package:base"), inherits = TRUE)
funs <- Filter(is.function, objs) 
generics <- Filter(function(x) ("generic" %in% pryr::ftype(x)), funs)
  
sort(
  lengths(sapply(names(generics), function(x) methods(x), USE.NAMES = TRUE)),
  decreasing = TRUE
  )[1]
#> print 
#>   208
  • UseMethod() calls methods in a special way. Predict what the following code will return, then run it and read the help for UseMethod() to figure out what’s going on. Write down the rules in the simplest form possible.
y <- 1
g <- function(x) { 
  y <- 2
  UseMethod("g")
}
g.numeric <- function(x) y
g(10) # Internal variable 을 먼저 찾기 때문에 2가 된다.
#> [1] 2
#> [1] 2

h <- function(x) {
  x <- 10
  UseMethod("h")
}
h.character <- function(x) paste("char", x)
h.numeric <- function(x) paste("num", x)

h("a")
#> [1] "char a"
#> [1] "char a"
  • Internal generics don’t dispatch on the implicit class of base types. Carefully read ?“internal generic” to determine why the length of f and g is different in the example below. What function helps distinguish between the behaviour of f and g?
f <- function() 1 
g <- function() 2 
class(g) <- "function" 
class(f) 
#> [1] "function"
#> [1] "function"
class(g)
#> [1] "function"
#> [1] "function"

length.function <- function(x) "function" 

length(f)
#> [1] 1
#> [1] 1
length(g)
#> [1] "function"
#> [1] "function"

Answer : f는 implicit class 가 function 인 것이고, g 는 class=function 으로 정의된 것이다.

library(pryr)
# fytype 함수는 해당 function 이 internal/S3/S4/RC 인지를 알려준다. 
ftype(f) 
#> [1] "function"
ftype(g) 
#> [1] "function"
ftype(t.test) 
#> [1] "s3"      "generic"
ftype(length) 
#> [1] "primitive" "generic"
# 둘 다 function 으로 되는데 이것이 implicit class 인지는 아래처럼 확인할 수 있다. 
is.object(f) # f는 implicit class "function"" 이기 때문에 FALSE가 반환된다. 
#> [1] FALSE
is.object(g) # g는 s3 객체이기 때문에 TRUE 가 반환된다.
#> [1] TRUE