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 パッケージ中の関数のヘルプドキュメントとその参考実行例コードを研究することのように思われます。




目次



R の S3 クラスとメソッド

S3クラスは本質的にオブジェクトのクラス属性(class atribute)文字列

S3クラスは本質的にオブジェクトのクラス属性(class atribute)文字列に過ぎない。それが該当クラスに相応しい構造を持つかどうかは基本的に作成コード・操作の責任となる。

例: S3 クラス、総称的関数、メソッド関数群

> 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
(...以下略...)

S3メソッドの仕組み

総称的関数はクラス属性文字列(だけ?)を頼りに、適当なメソッド関数を割り当てる。例えば plot 関数は引数がクラス属性 "ts" を持つことを確認すると、それようのメソッド関数 plot.ts を実際には起動する(メソッドの選択適用(method dispatch))。

例:総称的関数 plot の(S3)メソッド関数一覧

> 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クラスの「いい加減さ」

S3 クラス属性は「勝手に」与えることができる。

例: S3 クラス属性は「勝手に」与えることができる

> y <- runif(10)
> class(y) <- "lm"      # 騙す(線形回帰オブジェクトにする)
> class(y)              # 騙された 
[1] "lm"
> print(y)              # 無意味な出力(しかしエラーにならない)
Call:
NULL
No coefficients

S3クラスの整合性チェック機能

S3クラスにも多少の整合性チェック機能がある。無理にクラスを変更しようとすると、つじつま合わせの強制変換(それ用の関数が存在すれば)を試みる。

例: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 でした)、
行列にクラスを設定するのは不正です

R の S4 クラス

S4 クラスの定義には、それが含むべきデータであるスロット(slot)とその型(type)が明示的に表現(representation)され、異なった型のデータを含まないようにチェック機能が働く。また、他のクラスをデータとして継承(inheritance)することができる。また、不用意に再定義されないようにクラス定義を封印(seal)することができる。クラスのオブジェクトには原型(prototype)を与えておくことができる。S4 クラス定義は適当なパッケージ(環境)に記録される。

新しいクラスは setClass 関数で定義する。名前付きスロットは representation 欄に、名前無しスロットはcontains 欄に書く。クラスの新しいオブジェクトはスロットの値を使って new 関数で生成する。クラス定義は getClass 関数で参照できる。

例:名前付きスロット id (文字列) と名前無しスロット(数値)を持つクラス定義

> 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"

例:クラス "numWithId" のオブジェクト(インスタンス)を作る

# 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

例: 基本データ型 matrix を拡張するクラス

> 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"

R の S4 メソッド

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 の定義

> 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])

例: S4 クラスを引数に取る普通(総称的でない)関数を定義することはもちろん可能

> 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

パッケージ methods 内のオブジェクト一覧

以下は 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              オブジェクトの正統性を検査

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2023-03-25 (土) 11:19:16