블로그 보관함

2011년 7월 6일 수요일

R에서 객체 지향 프로그래밍: S3 기초

R S3 OOP 기초

R에는 3가지 유형의 객체 지향 시스템이 있다. S3, S4, R5 이렇게 세 가지이다. S3는 R의 핵심적인 패키지들에서 광범위하게 사용되고 있으므로 반드시 알아두어야 한다. S4는 S3와 기저에 깔린 아이디어는 유사하지만 구현 방식이 더 엄격하고 요즘 대세인 OO 시스템(Java 등의)에 좀더 비슷하다. R5는 R 2.12에서야 등장한 최신 시스템으로 요즘의 OO 시스템에 더 비슷하다.

S3는 1990년경에 S언어에 도입된 것으로 'generic-function' 방식으로 불리는 스타일의 객체 지향 프로그래밍 기법이다. 'message-passing' 방식으로 불리는 OO를 구현한 Java, C++ 등의 객체 지향과는 다르다. 'message-passing' 방식인 Java, C++에서는 객체가 어떤 function을 실행할지 결정하는 데에 비해 S3에서는 generic function이라는 특별한 함수가 어떤 method를 호출할지를 결정한다.

클래스

S3에서는 클래스라고 해봐야 별 것 없다. 단지 class라는 속성(attribute)의 값에 따라 클래스가 결정될 뿐이다. 딱히 instance/class 구별이 큰 의미도 없다. 객체를 만드는 방법은 간단하다. class() 함수를 이용하여 class 속성을 지정할 수 있다. 다음처럼 사각형과 원을 만들어 보자.

a <- c(xleft=10, ybottom=7, xright=15, ytop=9)
class(a) <- "rectangle"

b <- list(center=c(10, 5), radius=5)
class(b) <- "circle"

어떤 객체가 어떤 내용을 담고 있는지 들여다 보는 데에 여러가지 함수들이 사용될 수 있다. 위처럼 사각형 a와 원 b를 만들었다면 class() 함수로 클래스를 확인해 볼 수 있다.

> class(a)
[1] "rectangle"
> class(b)
[1] "circle"
객체에 대해 attributes(), str(), structure() 명령을 사용해서 어떤 내용이 출력되는지 살펴보라. 그러니까
> str(a)
Class 'rectangle'  Named num [1:4] 10 7 15 9
  ..- attr(*, "names")= chr [1:4] "xleft" "ybottom" "xright" "ytop"
와 같은 식으로 명령을 내려 보라. attr() 함수를 이용하면 원하는 속성을 지정해서 살펴볼 수 있다.
> attr(a, "class")
[1] "rectangle"
아까 사용한 class() 함수는 단지 "class"라는 이름의 속성값에 접근하게 해주는 함수일 뿐이다.

메소드

넓이를 계산해 주는 메소드 area()를 만들어 보자. 사각형과 원은 넓이를 계산하는 방법이 다르다. 그러니까, 클래스마다 다른 메소드가 실행되어야 한다. 그런데 다음처럼 실행되게 하려고 한다.

> area(a)
[1] 10
> area(b)
[1] 78.53982
이것을 S3에서 구현하는 방법을 method dispatch라고 하는데 여기서 area() 함수를 generic function이라고 한다. 우선 필요하는 것은 generic function을 정의하는 것이다. 방법은 간단하다. 다음처럼 한다.
area <- function(x, ...) {
    UseMethod("area", x)
}
이것으로 area()라는 generic function이 만들어졌다. 이것은 인자로 주어진 객체의 class를 체크해서 적절한 메소드를 dispatch해 준다. 각 클래스를 위한 method는 generic function의 이름 다음에 점을 찍고 클래스 이름을 붙여서 만든다. 말로 하는 것보다 한번 보는 것이 쉽다.
area.rectangle <- function(x, ...) {
  as.numeric((x["xright"] - x["xleft"]) * (x["ytop"] - x["ybottom"]))
}

area.circle <- function(x, ...) {
  pi * x$radius^2
}
이것으로 끝이다. 이제 generic function area()rectangle 객체를 받으면 area.rectangle() 메소드가 실행되고 circle 객체를 받으면 area.circle() 메소드가 실행된다.

한 generic function에 어떤 메소드들이 있는지 알고 싶다면 methods() 함수를 이용한다.

> methods("area")
[1] area.circle area.rectangle
한번 methods(plot) 명령을 내려보라. 여러가지 메소드들을 볼 수 있을 것이다. 만약 우리가 MyClass라는 새로운 클래스를 만들었고 plot()로 그것에 알맞은 그래프가 그려지게 하고 싶다면 plot.MyClass()라는 이름의 함수를 새로 정의하기만 하면 된다. plot()가 이미 generic function이므로 알아서 해준다.

S3 generic function에서 주어진 클래스에 맞는 method를 얻는 것은 getS3method()로 가능하다. 즉 다음처럼 작동한다.

> getS3method("area", "circle")
function(x, ...) {
  pi * x$radius^2
}

생성자

S3에서는 생성자(constructor)를 형식적으로 지원하지 않는다. 프로그램 짜는 사람이 알아서 해야한다. 어쨌든 언제나 생성자는 만드는 습관을 들이는 것이 좋다. 생성자는 데이터를 받아서 대개는 리스트 형태로 만들고 class 속성값을 붙여주는 형태가 된다. 예를 들면:
new_circle <- function(x, y, r) {
  circle <- list(center=c(x,y), radius=r)
  class(circle) <- "circle"
  circle
}

참고

댓글 없음:

댓글 쓰기