R

トップページ
Google
WWWを検索
サイト内を検索

Rではありきたりの統計解析にRを用いている限りにおいて、オブジェクト指向プログラミングを意識することはないです1。総称関数は便利に使われているのですが、JavaやC++と言った所謂クラス型オブジェクト指向言語でするようにGoFデザインパターンでガリガリ書くのには無理があるからでしょう2。しかし、上手く使うとコードの見通しをよくできるので、不完全3でも有用な機能にはなっています。

Rのオブジェクト指向システムは歴史的な理由でS3/S4/RC/R6と4種類もあるのですが、標準のS3とS4とRC(Reference Classes)を確認していきましょう。

1 S3クラスによるオブジェクト指向

もっとも素朴なS3クラスは特に労力がかかりません。リストにクラス名の属性がついて、総称関数が使えるようになっただけです。インスタンスと言う概念すらありません。

1.1 S3クラスを作ってみる

空のlistにクラス名を指定してもS3クラスは完成するのですが、後のことを考えてもう少し手の込んだオブジェクトをつくります。

n <- 7
obj1 <- list(a=1:n, b=rpois(n, n*2/3))
class(obj1) <- "bigger one lover"
obj1
$a
[1] 1 2 3 4 5 6 7

$b
[1] 5 8 7 5 5 6 3

attr(,"class")
[1] "bigger one lover"

1.2 既存の総称関数のメソッドを作る

このリスト構造にはmax関数が使えません。

max(obj1)
 max(obj1) でエラー:  引数 'type' (list) が不正です

“bigger one lover”クラスのために、総称関数maxの実装を用意しましょう。

"max.bigger one lover" <- function(obj, na.rm){
    c(max(obj$a), max(obj$b))
}

総称関数にあわせて第2引数が必要になっていることと、max(..., na.rm)は既にgeneric関数になっているためUseMethod不要なことに注意してください。

max(obj1)
[1] 7 8

maxが使えるようになりました。

1.3 新たな総称関数をつくる

新しい総称関数をつくるのも簡単です。

choose <- function(obj) UseMethod("choose")

"choose.bigger one lover" <- function(obj){
    pmax(obj1$a, obj1$b)
}

これでリスト内のベクターaとbの順番が対応する要素を比較して大きい方を戻すメソッドとなります。定義した関数chooseが振り分けを行なうUseMethodを呼び、UseMethodobjのクラス名に応じて4メソッドを実装した関数を呼ぶ仕組みです。作ったchooseを消せば、総称関数では無くなります。

choose(obj1)
[1] 5 8 7 5 5 6 7

演算子も定義できます。

"%+%" <- function(v, w) UseMethod("%+%")
"%+%.bigger one lover" <- function(v, w){
    c <- list(a=v$a + w$a, b=v$b + w$b)
    class(c) <- class(v)
    c
}

obj2 <- list(a=(-1)^(1:n), b=(-1)^(2:(n+1)))
class(obj2) <- c("bigger one lover")

obj1 %+% obj2
$a
[1] 0 3 2 5 4 7 6

$b
[1] 6 7 8 4 6 5 4

attr(,"class")
[1] "bigger one lover"

複数の引数を持つ場合はUseMethodを呼んでいる関数の引数が複数になることに注意してください。

1.4 S3クラスの継承と言うか親の設定

obj1obj3にコピーして、obj3のクラス名smaller one loverを書き替えて、メソッドを定義します。

obj3 <- obj1
class(obj3) <- c("smaller one lover")

"choose.smaller one lover" <- function(obj){
    pmin(obj1$a, obj1$b)
}

すると、同じ総称関数でも結果が変わります。

# オブジェクト内のベクターaとbの順番が対応する要素の大きい方を戻す
choose(obj1) 
[1] 5 8 7 5 5 6 7
# 小さい方を戻す
choose(obj3) 
[1] 1 2 3 4 5 6 3

また、smaller one loverクラスで実装されていないmaxobj3には使えません

max(obj3) 
 max(obj3) でエラー:  引数 'type' (list) が不正です

親クラスを追加すると、実装されていないメソッドは、親のメソッドを呼ぶようになります。

class(obj3) <- c("smaller one lover", class(obj1))
max(obj3)
[1] 7 8

Java™の派生クラスは実子、RのS3クラスの派生は養子感がありますね。

2 S4クラスによるオブジェクト指向

Rには、もう少しクラス定義を厳密にしたformalなクラスと呼ばれるS4クラスもあります。クラス定義とインスタンスの生成が明確にサポートされます5

2.1 S4クラスの宣言

前節のS3クラスの作成をS4クラスに書きなおすと、

n <- 7
BOL <- setClass("bigger one lover", 
    representation(a = "numeric", b = "numeric"), 
    prototype(a = rep(0, n), b = rep(0, n)),
    validity = function(object){
        if(length(object@a)==length(object@b)) return(TRUE);
        "a and b are different lengths!";
    })

representationによる型宣言、prototypeによる省略値、validityによる整合性チェックが設定できます。

継承もcontainesで出来ます。

SOL <- setClass("smaller one lover", contains = "bigger one lover")

オブジェクトのメンバーはgetSlotsで確認できます。

getSlots("bigger one lover")
        a         b 
"numeric" "numeric" 

なお定義に関する情報は環境内に特殊な変数名で保存されています6。またsetClassの戻り値はコンストラクタになる関数です。

定義したクラスを削除する場合は、以下のようにしますがドキュメントには非推奨とされています7

removeClass("bigger one lover")

2.2 S4クラスからのインスタンス生成

実際にインスタンスを生成してみましょう。

obj1 <- new("bigger one lover", a = 1:n, b = rpois(n, n*2/3))

コンストラクタを使うこともできます。

obj1 <- BOL(a = 1:n, b = rpois(n, n*2/3))

相変わらず実際はリストですが、(slotもしくはattributeと呼ばれる)メンバー変数になる要素を参照するときは、$ではなく@を用いることが推奨されています。

一応、中身が見られることを確認します。

obj1@a
[1] 1 2 3 4 5 6 7
obj1@b
[1] 7 3 0 2 2 9 4

obj1とメンバー変数の値が同じの派生クラスもつくっておきます。

obj3 <- new("smaller one lover", a = obj1@a, b = obj1@b)
obj3@a
[1] 1 2 3 4 5 6 7
obj3@b
[1] 7 3 0 2 2 9 4

2.3 S4クラスのメソッド

S3クラスと同様に、総称関数を宣言しメソッドを実装します。

setGeneric("choose", function(object) {
    standardGeneric("choose")
})
[1] "choose"
setMethod("choose", 
    signature(object = "bigger one lover"), function(object) {
        pmax(object@a, object@b)
    }
)

setMethod("choose", 
    signature(object = "smaller one lover"), function(object) {
        pmin(object@a, object@b)
    }
)

第1引数でメソッド名、第2引数でメソッドの引数の型、第3引数でメソッドの実態を定義します。第2引数は"bigger one lover"とクラス名だけ書いておいても問題ないです。

メソッドの存在確認と、メソッド呼び出しもできます。

existsMethod(choose, "bigger one lover")
[1] TRUE

動かして確認すれば間に合いそうですが。

choose(obj1)
[1] 7 3 3 4 5 9 7
choose(obj3)
[1] 1 2 0 2 2 6 4

元クラスも派生クラスも動きますね。

定義したメソッドの削除もできます。

removeMethod(choose, "bigger one lover")

2.4 既存の総称関数のメソッドを作る

maxも同様に定義したいわけですが、既存の総称関数のメソッドを作るときには注意が必要です。登録されている総称関数の宣言に第2引数の変数名もあわせないと、エラーになるからです。最初に、総称関数maxの宣言を確認しましょう。

getGeneric("max")
standardGeneric for "max" defined from package "base"
  belonging to group(s): Summary 

function (x, ..., na.rm = FALSE) 
standardGeneric("max", .Primitive("max"))
<bytecode: 0x000000001ce73960>
<environment: 0x000000001ce7b620>
Methods may be defined for arguments: x, na.rm
Use  showMethods(max)  for currently available ones.

xna.rmを使っていますね。ここに注意して以下のように定義します。

setMethod("max", 
    signature(x="bigger one lover",
         na.rm = "logical"), 
    function(x, na.rm) {
        c(max(x@a), max(x@b))
    }
)

...は無視して無問題のようです。仕様上は...も使えるはずですが8

加算もgetGeneric("+")で定義を確認して変数名e1e2に注意して実装してみましょう。

setMethod("+", 
    c(e1 = "bigger one lover", e2 = "bigger one lover"),
    function(e1, e2) {
        new(class(e1)[1], a = e1@a + e2@a, b = e1@b + e2@b)
    }
)

obj1 + obj3 
An object of class "bigger one lover"
Slot "a":
[1]  2  4  6  8 10 12 14

Slot "b":
[1] 14  6  0  4  4 18  8

なお、multi-dispatchなので、第2引数をc(e1 = "bigger one lover", e2 = "numeric")としたメソッド、c(e1 = numeric, e2 = "bigger one lover")としたメソッドを定義し、インスタンスと数値の加算も実現できます9

2.5 S3クラス/メソッドのS4クラス/メソッドの相互運用

クラス名が分かればよいので、@でなくて$でメンバー変数(S4用語ではslot)を参照することになりますが、S4クラスのインスタンスを処理するS3クラスのメソッドを作ることは簡単です。逆にS4クラスでS3クラスのオブジェクトを取り扱うこともできます。 S3クラスを作成してみます。

obj1 <- list(a=1:5)
obj2 <- list(a=-1^(1:5))
class(obj1) <- class(obj2) <- "example"

このままでは扱えないのですが、setOldClassを使うとS4クラス用の書き方で処理できるようになります。

setOldClass("example")

setGeneric("plus", function(v, w) {
    standardGeneric("plus")
})
[1] "plus"
setMethod("plus", c("example", "example"), function(v, w) {
    c <- list(a=v$a + w$a)
    class(c) <- class(v)
    c
})

plus(obj1, obj2)
$a
[1] 0 1 2 3 4

attr(,"class")
[1] "example"

ただし(よく確認できていないのですが)+の実装が上手くいかなかったので、完璧な後方互換と言えるかは分かりません。

2.6 S4クラスの利点

S4クラスの方が優れている点としては、

  • S3クラスよりは厳密な定義が可能
  • 引数の型に応じてメソッドを変えられるmulti-dispatch
  • メソッド名のsuffixにクラス名がつかない
  • +-などの演算子も定義可能

ところが挙げられます。煩雑なのでad-hocなコードで使うのは億劫ですが、パッケージ化などをする場合は有用だと思います。

3 RC(Reference Classes)

利用頻度は低いらしいですが、Java™使いなどには自然に思えるのがRCです。

Rは関数の引数が原則として値渡しなので、メモリー利用効率と付随して処理速度が良くありません。S3とS4も同様の欠点があります。しかし環境(environment)が例外的に参照渡しであることを利用して、RCはこの欠点を緩和しています。記述も他のオブジェクト指向型言語に近いものですし、ユーザー定義クラスはすべてenvRefClassの派生になっているなど、クラスのつくりも似ています。

3.1 RCクラスの宣言/メソッド定義

オブジェクト指向らしくクラス宣言とメソッド定義が同時に済みます。

BOL <- setRefClass("bigger one lover RC",
    fields = c(a = "numeric", b = "numeric"), 
    methods = c(
        initialize = function(a = rep(0, n), b = rep(0, n)){
            if(length(a)!=length(b)) stop("a and b are different lengths!");
            .self$a <- a
            .self$b <- b
        },
        max = function(.self){
            c(max(.self$a), max(.self$b))
        },
        choose = function(.self){
            c(pmax(.self$a, .self$b))
        },
        add = function(.self, obj2){
            .self$a <- .self$a + obj2$a
            .self$b <- .self$b + obj2$b
        }
    )
)

Java/C++/Python/PHPあたりに慣れている人だと説明不要な気がするのですが、bigger one lover RCがクラス名、fieldsがプロパティ、methodsがメソッドの定義です。initializeが初期化メソッドで、各メソッドでは.sefで自分を参照できます。

containsで親クラスを指定し、プロパティやメソッドを上書きすることで継承もできます。初期化メソッドinitializeも継承してくれます10

SOL <- setRefClass("smaller one lover RC",
    contains = "bigger one lover RC",
    methods = c(
        choose = function(.self){
            c(pmin(.self$a, .self$b))
        }
    )
)

デフォルトで実装しているプロパティやメソッドは?setRefClassで見られるドキュメントに網羅されているので、文書が散らばっていて全体構造が見えづらいS3/S4クラスより話が分かりやすいです。なお、演算子のオーバーライドのサポートは無いようです。Rの文法上実装不能なのだと思いますが、アクセス修飾子もありません。

3.2 RCクラスのインスタンス生成

インスタンスを作ります。

n <- 7
obj1 <- BOL(1:n, rpois(n, n*2/3))
obj2 <- SOL(1:n, rpois(n, n*2/3))
obj3 <- BOL(-1^(1:n), -1^(2:(n+1)))

中身もしっかり詰まっています。

obj1
Reference class object of class "bigger one lover RC"
Field "a":
[1] 1 2 3 4 5 6 7
Field "b":
[1] 5 5 5 5 3 6 5
obj2
Reference class object of class "smaller one lover RC"
Field "a":
[1] 1 2 3 4 5 6 7
Field "b":
[1] 5 2 7 3 3 5 2
obj3
Reference class object of class "bigger one lover RC"
Field "a":
[1] -1 -1 -1 -1 -1 -1 -1
Field "b":
[1] -1 -1 -1 -1 -1 -1 -1

3.3 RCクラスのメソッド呼び出し

もちろん同じ結果になります。

obj1$choose()
[1] 5 5 5 5 5 6 7
obj2$choose()
[1] 1 2 3 3 3 5 2
obj1$max()
[1] 7 6
obj1$add(obj3)
obj1
Reference class object of class "bigger one lover RC"
Field "a":
[1] 0 1 2 3 4 5 6
Field "b":
[1] 4 4 4 4 2 5 4

4 まとめ

S3/S4/RCを俯瞰してきましたが、オブジェクト指向の部分だけとは言え3つのシステムが共存していて困惑する面があります。さらにRCより高速に動作するのが謳い文句のR6パッケージが存在します。どれを使えばいいのかと言う感じになりますが、仕様の自然さや簡素さと、オブジェクト指向の徹底度を天秤にかけて、個人利用ではS3、小規模チームではS4、大規模チームではR6と言うガイドラインを示している人がいます。Javaなどに慣れていれば、個人利用でもR6で問題ないとも思いますが。


  1. lm関数の戻り値をsummary関数にいれたら詳細が出ると言うことは利用者のほぼ全員が知っているわけですが、summary関数が総称関数で実態が別にあることを知らないか忘れてしまっている利用者は過半では無いでしょうか。↩︎

  2. 標準APIを含めてライブラリがOOPで書かれていないと、それを利用するコードもOOPになりづらいです。RはAPIが非OOPでエンドユーザーのコードが非OOPなので、利用者がOOP好きでないとOOPを使う場面がやってきません。↩︎

  3. アクセス修飾子が無いのでカプセル化が十分ではないし、Java™で言うinterface/abstrat classなども定義できないです。↩︎

  4. 親環境を辿って勝手に引数の内容を確認するのが気持ち悪いですね。↩︎

  5. S3でもオブジェクト生成関数をつくれば似たような構造にはなるわけですが、クラスの構造を調べたりするシステム的なサポートはないので。↩︎

  6. ls.str(all.names=TRUE)でそれらしいprefixの変数を確認できます。↩︎

  7. setClassでつくったクラスからインスタンスを生成後、クラスをremoveClassで消して、同名のクラスをsetRefClassでつくった後に、setRefClassでつくった方のクラスからのインスタンス生成にエラーが出たり、生成されたインスタンスが異常になるので、しっかり機能していないかも知れません。↩︎

  8. ? dotsMethodsで利用例が確認できる。↩︎

  9. numericやlogicalやcharacterもクラスとして扱われていると言うことです。↩︎

  10. Java™のコンストラクタは継承されません。↩︎