R の S4 クラス、メソッド入門 R ユーザー会資料 (2005.12.10) 間瀬茂
以下では、R のクラスとメソッドについて簡単に説明する。R のクラスとメソッドの実装方式には S3 (S 言語第3版) 方式と、S4 (S 言語第4版) 方式が現在併用されているが、順次 S4 方式に統一されて行くと思われる。S4 クラス・メソッドは本格的なオブジェクト指向機構を実現している。R の基本パッケージ stats4 は S4 クラス・メソッドを操作する統計関数からなる。
クラス(class)とはある構造を持つデータの集まり、メソッド(method)とはクラスに対してある処理を行う関数である。R の統計関数は多く総称的(generic)であり、個別のクラスに対して実際に適当な処理を行うメソッド関数グループが多数存在する。総称的関数は、引数のクラスの素性に応じて適当なメソッド関数を選択適用(method dispatch)する。これが、単一の関数(例えば plot, summary)を使うだけで、多種多様な結果が得られる R の便利なメカニズムである。
S 言語第四版に関する原典は データによるプログラミング (日本語訳あり)です。しかし、説明は S-plus によっており、R と微妙に異なり、R ユーザーには結構わかりにくいような気がします。私の経験では一番の近道は methods パッケージ中の関数のヘルプドキュメントとその参考実行例コードを研究することのように思われます。
目次
S3クラスは本質的にオブジェクトのクラス属性(class atribute)文字列に過ぎない。それが該当クラスに相応しい構造を持つかどうかは基本的に作成コード・操作の責任となる。
> x <- lm(1:10~rnorm(10)) # 簡単な単回帰。クラス属性 ``lm'' のオブジェクト > class(x) # クラス属性 [1] "lm" > summary(x) # 総称的関数 summary の適用。実際はメソッド関数 summary.lm() が適用される Call: lm(formula = 1:10 ~ rnorm(10)) Residuals: Min 1Q Median 3Q Max -4.3527 -2.2764 0.1823 2.4843 4.5825 (...以下略...)
総称的関数はクラス属性文字列(だけ?)を頼りに、適当なメソッド関数を割り当てる。例えば plot 関数は引数がクラス属性 "ts" を持つことを確認すると、それようのメソッド関数 plot.ts を実際には起動する(メソッドの選択適用(method dispatch))。
> methods(plot) [1] plot.Date* plot.HoltWinters* plot.POSIXct* [4] plot.POSIXlt* plot.TukeyHSD plot.acf* ......................................................... [31] plot.ts plot.tskernel* Non-visible functions are asterisked
S3 クラス属性は「勝手に」与えることができる。
> y <- runif(10) > class(y) <- "lm" # 騙す(線形回帰オブジェクトにする) > class(y) # 騙された [1] "lm" > print(y) # 無意味な出力(しかしエラーにならない) Call: NULL No coefficients
S3クラスにも多少の整合性チェック機能がある。無理にクラスを変更しようとすると、つじつま合わせの強制変換(それ用の関数が存在すれば)を試みる。
> x <- matrix(1:4, 2, 2) > class(x) [1] "matrix" # 行列クラス > class(x) <- "numeric" # 騙す(困った挙げ句に白旗) 警告メッセージ: 強制変換により NA が生成されました # 文字列クラスを行列クラスに変更 > y <- "matrix" > class(x) [1] "character" # 文字列クラス > class(y) <- "matrix" # 騙す(さすがに苦情) エラー:次元属性が長さ 2 でないかぎり(0 でした)、 行列にクラスを設定するのは不正です
S4 クラスの定義には、それが含むべきデータであるスロット(slot)とその型(type)が明示的に表現(representation)され、異なった型のデータを含まないようにチェック機能が働く。また、他のクラスをデータとして継承(inheritance)することができる。また、不用意に再定義されないようにクラス定義を封印(seal)することができる。クラスのオブジェクトには原型(prototype)を与えておくことができる。S4 クラス定義は適当なパッケージ(環境)に記録される。
新しいクラスは setClass 関数で定義する。名前付きスロットは representation 欄に、名前無しスロットはcontains 欄に書く。クラスの新しいオブジェクトはスロットの値を使って new 関数で生成する。クラス定義は getClass 関数で参照できる。
> setClass("numWithId", representation(id = "character"), contains = "numeric") [1] "numWithId" > isClass("numWithId" # クラスかどうか検査。isClass(numWithId) ではエラー [1] TRUE > getClass("numWithId") # クラス定義を見る Slots: Name: .Data id # 名前無しスロットは .Data という名前 Class: numeric character Extends: Class "numeric", from data part Class "vector", by class "numeric"
# e <- new("numWithId", id = "3 random numbers", runif(3)) でもOK > ( e <- new("numWithId", runif(3), id = "3 random numbers") ) An object of class "numWithId" [1] 0.8540727 0.1615458 0.8514492 # 名前無し数値スロット Slot "id": # 名前付き文字列スロット [1] "3 random random numbers" # クラス定義は既定でパッケージ(環境) 「.GlobalEnv」 に登録 # 名前なしスロットは隠し名「.Data」を持つ > str(e) Formal class 'numWithId' [package ".GlobalEnv"] with 2 slots ..@ .Data: num [1:3] 0.854 0.162 0.851 ..@ id : chr "3 random numbers"
> setClass("numWithId2", representation(id="character", num="numeric")) [1] "numWithId2" > e2 <- new("numWithId2", id="3 sequence", num=1:3) > e2 An object of class "numWithId2" Slot "id": # 名前付き文字列スロット [1] "3 sequence" Slot "num": # 名前付き数値スロット [1] 1 2 3 > str(e2) Formal class 'numWithId2' [package ".GlobalEnv"] with 2 slots ..@ id : chr "3 sequence" ..@ num: int [1:3] 1 2 3
名前付きスロットの中身を抜き出すには「@ 演算子」を使う。名前なしスロットには疑似名 .Data を用いて
e @ .Data, e @ ".Data", slot(e,".Data")
等とする。
> e2 @ id # id スロットの中身を取り出す [1] "3 sequence" > e2 @ num # num スロットの中身を取り出す [1] 1 2 3 > slot(e2, "id") # 専用 slot 関数もある (slot(e2, id) はエラー) [1] "3 sequence"
スロットには適正なオブジェクトを付値できる。不適正な値でオブジェクトを生成したり、付値するとエラーになる(クラスの整合性検査機構)。
> ( e2@num <- rnorm(4) ) [1] -0.37293172 0.08139447 1.77335648 -0.54012836 # 数値スロットに行列を代入しようとする > ( e2 <- new("numWithId", id="3 sequence", num=matrix(1:4,2,2)) ) 以下にエラー validObject(.Object) : invalid class "numWithId" object: invalid object for slot "num" in class "numWithId": got class "matrix", should be or extend class "numeric"
> setClass("matWithId3", representation(id="character", mat="matrix")) [1] "matWithId3" > ( e3 <- new("matWithId3", id="2x2 matrix", mat=matrix(1:4,2,2)) ) An object of class "matWithId3" Slot "id": [1] "2x2 matrix" Slot "mat": [,1] [,2] [1,] 1 3 [2,] 2 4 > e3@"mat"' # スロット mat の中身。slot(e3, "mat") でも可 [,1] [,2] [1,] 1 3 [2,] 2 4 > e3@mat[1,2] # 当然行列操作が可能 [1] 3
> setClass("numWithId", representation(id = "character"), contains = "numeric") [1] "numWithId" > setClass("bar", representation(id = "character"), contains = "character") [1] "bar" > ebar <- new("bar", id = "abc", "def") # クラス "bar" のオブジェクト > e <- new("numWithId", id = "abc", ebar) # 名前無しスロットに指定外のクラスオブジェクトを代入 Warning message: 強制変換により NA が生成されました # エラーにはならないことを注意
「contains 欄」を用いて既にあるクラスを拡張(extend)するクラスを定義できる。既存のクラスは「上位クラス(super-class)」、 拡張されたクラスは「下位クラス(sub-class)」と呼ばれる。
> setClass("numWithId4", representation(id = "character"), contains = "numeric") [1] "numWithId4" > e4 <- new("numWithId4", id="3 numbers", 1:3) # 結果は単に擬似(名無し)スロットが加わるだけ > str(e4) Formal class 'numWithId4' [package ".GlobalEnv"] with 2 slots ..@ .Data: int [1:3] 1 2 3 ..@ id : chr "3 numbers"
> setClass("numWithId", representation(id = "character"), contains = "numeric") [1] "numWithId" ## クラス "numWithId" を名前つきスロットと名前無しスロットに含むクラスの定義 ## 最初は失敗(スロット名 id が同じためらしい) > setClass("foo", representation(id = "numWithId"), contains = "numWithId") 以下にエラー.validExtends(class1, class2, classDef, classDef2, obj@simple) : class "foo" cannot extend class "numWithId": slots in class "foo" must extend corresponding slots in class "numWithId": fails for id 以下にエラーsetClass("foo", representation(id = "numWithId"), contains = "numWithId") : error in contained classes ("numWithId") for class "foo"; class definition removed from '.GlobalEnv' ## スロット名を idd に変えると成功 > setClass("foo", representation(idd = "numWithId"), contains = "numWithId") [1] "foo" > e1 <- new("numWithId", id = "3 random numbers", runif(3)) > e2 <- new("numWithId", id = "4 random numbers", runif(4)) > efoo <- new("foo", idd = e1, e2) # クラス "foo" のオブジェクト作成 > str(efoo) # オブジェクトの内容を見る(結構ややこしい (^^;)) Formal class 'foo' [package ".GlobalEnv"] with 3 slots ..@ .Data: num [1:4] 0.5930 0.0884 0.2011 0.7343 (# これは名前無しスロットの名前無しスロットの中身) ..@ idd :Formal class 'numWithId' [package ".GlobalEnv"] with 2 slots .. .. ..@ .Data: num [1:3] 0.4524 0.0967 0.4248 (# これは名前 idd 付きスロットの名前無しスロットの中身) .. .. ..@ id : chr "3 random numbers" (# これは名前 idd 付きスロットの名前 id 付きスロットの中身) ..@ id : chr "4 random numbers" (# これは名前無しスロットの名前 id 付きスロットの中身)
## 以下順に中身を取り出す > efoo @ idd # 名前 idd 付きスロットの中身ークラス "numWithId" のオブジェクト An object of class "numWithId" [1] 0.5805303 0.8349548 0.9335655 Slot "id": [1] "3 random numbers" > (efoo @ idd) @ id # 名前 idd 付きスロットの中身の、名前 id 付きスロットの中身 [1] "3 random numbers" > (efoo @ idd) @ .Data # 名前 idd 付きスロットの中身の、名前無しスロットの中身 [1] 0.5805303 0.8349548 0.9335655 > efoo @ id # 名前 id 付きスロットの中身 [1] "4 random numbers" > efoo @ .Data # 名前無しスロットの中身 [1] 0.3309665 0.7076782 0.9590298 0.7051191
以上の例からわかるように efoo の構造は一見 efoo ---> 名前付きスロット idd ---> 名前付きスロット id | | | ---> 名前無しスロット .Data ---> 名前無しスロット .Data ---> 名前付きスロット id | ---> 名前無しスロット .Data となるように思われるが、実際は次のようになっている efoo ---> 名前付きスロット idd ---> 名前付きスロット id | | | ---> 名前無しスロット .Data ---> 名前無しスロット .Data | ---> 名前付きスロット id
スロットにはプロトタイプ(既定値)を与えることができる。
> setClass("coord", representation(x="numeric", y="numeric"), prototype = list(x=0, y=0) ) [1] "coord" > new("coord") An object of class "coord" Slot "x": # プロトタイプ値が入っている [1] 0 Slot "y": [1] 0 > new("coord", x=1, y=1) # スロット変数に値を指定 An object of class "coord" Slot "x": # 指定した値が入っている [1] 1 Slot "y": [1] 1
クラス定義は既存クラスをその一部として含むことができる(クラスの継承)。含まれたクラスは上位クラス、含むクラスは下位(拡張)クラスという。上位クラスとしては、基本データ型でもよい。下位クラスは上位クラスのスロットをそのスロットの一部として持つ。同じ名前のスロットがあってもエラーにはならないが、同じものとされてしまう。
> setClass("coord", representation(x="numeric", y="numeric"), prototype = list(x=0, y=0)) [1] "coord" > ( c1 <- new("coord", x=1, y=1) ) An object of class "coord" Slot "x": [1] 1 Slot "y": [1] 1 # "coord" を継承するクラス "coord2" > setClass("coord2", representation(z="numeric", y="numeric"), prototype = list(z=0), contains="coord") [1] "coord2" > ( c2 <- new("coord2", z=1, c1) ) An object of class "coord2" # 三つのスロットを持つ Slot "z": [1] 1 Slot "y": [1] 1 Slot "x": [1] 1 # 下位クラスに上位クラスとおなじ名前のスロットがあると、同じ物とされる > setClass("coord3", representation(x="numeric"), prototype = list(x=0), contains="coord") [1] "coord3" > new("coord3", x=2, c1) An object of class "coord3" Slot "x": [1] 2 Slot "y": [1] 1
> setClass("foo", representation(x="numeric"), contains="matrix") [1] "foo" > new("foo", x=1, matrix(1:4,2,2)) An object of class "foo" [,1] [,2] [1,] 1 3 [2,] 2 4 Slot "x": [1] 1
クラス定義は改変できないように封印(seal)できる。
> setClass("3coord", representation(x="numeric",y="numeric",z="numeric")) [1] "3coord" # 定義を書き換えるのは自由 > setClass("3coord", representation(x="numeric",y="numeric",w="numeric")) [1] "3coord" > sealClass("3coord", where=.GlobalEnv) # 定義を封印 ## 書き換えようとするとエラーになる(封印を解除するのは? クラスをいったん消すのかな?) > setClass("3coord", representation(x="numeric",y="numeric",z="numeric")) 以下にエラー setClass("3coord", representation(x = "numeric", y = "numeric", : "3coord" has a sealed class definition and cannot be redefined # 定義済みクラスの一覧 > ls() # ls ではオブジェクトのみが表示される [1] "e" "e1" "e3" "e4" "e5" [6] "e55" "last.warning" > getClasses(.GlobalEnv) # .GlobalEnv 中のクラス一覧 [1] "3coord" "coord" "coord2" "matWithId" "numWithId"
S4 クラスに対する操作関数は、総称的(generic)関数の指定と、それに対するメソッド(method)関数の定義からなる。総称的関数は引数オブジェクトのクラスの組合せから、最適のメソッド関数を選択割り当て(method dispatch)る。callGeneric 関数の使用法に注意。
> setClass("track", representation(x="numeric", y="numeric")) [1] "track" > isGeneric("Arith") # (組み込みの)算術演算総称的関数(群) Arith [1] TRUE # クラスと数値の算術演算メソッドの定義 > setMethod("Arith", c("track", "numeric"), function(e1, e2) {e1@y <- callGeneric(e1@y , e2); e1 ) [1] "Arith" # 数値とクラスの算術演算のメソッド定義 > setMethod("Arith", c("numeric", "track"), function(e1, e2) {e2@y <- callGeneric(e1, e2@y); e2} ) [1] "Arith" # 算術演算全てにメソッドが同時に定義されている > t1 <- new("track", x=1:4, y=sort(rnorm(4))) > t1 - 100 # 第一のメソッド(引き算)適用 An object of class "track" Slot "x": [1] 1 2 3 4 Slot "y": [1] -101.36265 -101.00339 -100.81776 -100.71723 > 100 - t1 # 第二のメソッド(引き算)適用 An object of class "track" Slot "x": [1] 1 2 3 4 Slot "y": [1] 101.36265 101.00339 100.81776 100.71723 > 1/t1 # 第二のメソッド(割算)適用 An object of class "track" Slot "x": [1] 1 2 3 4 Slot "y": [1] -0.7338617 -0.9966217 -1.2228547 -1.3942441 # クラス同士の算術演算のメソッドの定義 setMethod("Arith", c("track", "track"), function(e1, e2) { e1@x <- callGeneric(e1@x , e2@x); e1@y <- callGeneric(e1@y , e2@y); e1 }) > t1 <- new("track", x=1:4, y=rnorm(4)) > t2 <- new("track", x=rep(2,4), y=runif(4)) > t1 + t2 An object of class "track" Slot "x": [1] 3 4 5 6 Slot "y": [1] 0.9274807 1.2821877 -0.3399180 1.7301085
> plotData <- function(x, y, ...) plot(x, y, ...) > setGeneric("plotData") # "plotData" は総称的関数と宣言 [1] "plotData" # そのメソッドの定義(第二引数は無いと宣言) > setMethod("plotData", signature(x = "track", y = "missing"), function(x, y, ...) plot(slot(x, "x"), slot(x, "y"), ...)) [1] "plotData" > plotData(t1) > removeGeneric("plotData") # 総称的関数で無くする [1] TRUE
# 行列一つをスロットに持つクラス "foo" の定義 > setClass("foo", representation(m = "matrix")) [1] "foo" > m1 <- matrix(1:12, 3, 4) > f1 = new("foo", m = m1) > f2 = new("foo", m = t(m1)) # 二つのクラス "foo" 引数を持つ総称的関数のメソッドを定義 > setMethod("%*%", c("foo", "foo"), function(x, y) callGeneric(x@m, y@m)) [1] "%*%" > stopifnot(identical(f1 %*% f2, m1 %*% t(m1))) # 検査 > removeMethods("%*%") # メソッド定義を取り除く [1] TRUE
> setMethod("[", "track", function(x, i, j, ..., drop) { x@x <- x@x[i] x@y <- x@y[i] x }) [1] "[" > plot(t1[1:15])
> setClass("numWithId", representation(id = "character"), contains = "numeric") [1] "numWithId" > e <- new("numWithId", id = "abc", runif(3)) > foo <- function (x) x@id # "唯の" 関数 > foo(e) [1] "abc" > foo2 <- function (x) x@.Data[3] # "唯の" 関数その2 > foo2(e) [1] 0.2612681
S4 クラスとメソッドには該当するドキュメント(もし作者がそれを提供していれば)を表示する ? 演算子を用いた組み込み機能がある。また、そうしたドキュメントを作成するための補助機能がある。
# クラス "genericFunction" に対するドキュメントを得る > class ? genericFunction # initialize 関数に対するメソッドのドキュメントを得る > methods ? initialize # 関数呼び出し myFun(x, sqrt(wt)) を評価する際、この呼び出しに対して使われるであろう # メソッドに関する何らかのドキュメントを得る。呼び出し自体と同様に、一つのメソッドが # 選択され、そのメソッドに対するドキュメントが得られる > ?myFun(x, sqrt(wt)) # 第一引数がクラス maybeNumber、第二引数が logical であるメソッドに対するドキュメント # 指定されたクラスに対応する一つのメソッドが選択され、そのドキュメントが得られる > method ? myFun("maybeNumber", "logical") # 特定の総称関数に対して定義されたメソッド用のドキュメントの骨格をもつ初期的で一般的な # ファイル 'myFun-methods.Rd' を作り出す > promptMethods("myFun") # このファイルを編集した上、参照するには次のようにする > methods ? myFun
以下は library(help=methods) で得られる R (2.1.1) の基本パッケージ methods 中のオブジェクトの 一覧である。
.BasicFunsList 組み込み・特殊関数のリスト Classes クラス定義解説 Documentation クラスとメソッドのオンラインドキュメントの利用と作成 GenericFunctions 総称的関数操作用ツール LinearMethodsList-class クラス "LinearMethodsList" MethodDefinition-class メソッド定義を表現するクラス MethodWithNext-class クラス MethodWithNext Methods メソッドの一般情報 MethodsList-class クラス Class MethodsList、総称的関数用のメソッドの表現 ObjectsWithPackage-class 関連パッケージ名を持つ、ブジェクト名ベクトル SClassExtension-class 継承(拡張)関係を表すクラス as オブジェクトがあるクラスに属するように強制 callNextMethod 継承メソッドを呼び出す character-class 基本データ型に対応するクラス classRepresentation-class クラスオブジェクト environment-class クラス "environment" fixPre1.8 バージョン 1.8 以前の R からセーブされたオブジェクトをフィックス genericFunction-class 総称的関数オブジェクト getClass クラス定義を得る getMethod メソッド定義を得る、検査する getPackageName 与えられたパッケージに伴う名前 hasArg 呼出し中の引数を見る initialize-methods あるクラスからの新しいオブジェクトを初期化するメソッド is あるクラスからのオブジェクトか? isSealedMethod 封印されたメソッド・クラスに対する検査 language-class 未評価の言語オブジェクトを表現するクラス makeClassRepresentation クラス定義を生成する new あるクラスからオブジェクトを生成する promptClass 形式的クラスのドキュメントに対するシェルを生成 promptMethods 形式的メソッドのドキュメントに対するシェルを生成 representation クラス定義に対するプロトタイプや表現を構築 setClass クラス定義を作成 setClassUnion 他のクラスの合併として定義されたクラス setGeneric 新しい総称的関数を定義 setMethod メソッドを作成、保管 setOldClass 古いスタイルに対する名前を指定 show オブジェクトを示す showMethods 指定された関数に対する全てのメソッドを示す signature-class メソッド定義に対する "signature" クラス slot 形式的クラスからのオブジェクト中のスロット structure-class 基本構造に対するクラス traceable-class トレースを制御するために内部的に使われるクラス validObject オブジェクトの正統性を検査