R in a Nutshellメモ (3) - オブジェクト指向プログラミング: S4

前回まで

知らないところだけをメモしていたら知らないことばかりで長くなったのでオブジェクト指向プログラミング(S4)の部分だけまとめ直してみました.

S4クラスによるオブジェクト指向プログラミング

Rのオブジェクト指向プログラミングシステムはSのそれを受け継いでいるが,version 3をもとにしたS3メソッドとversion 4をもとにしたS4メソッドの2種類が存在している.
以下ではS4メソッドとクラスを説明する.

クラス定義

クラスはsetClass()関数により作製できる.
例として一定の時間おきに採取されたデータを表すTimeSeriesクラスを作成する.
このクラスは採取されたデータ(数値)と,開始時刻,終了時刻を情報として含む.このような情報はスロットと呼ばれる領域に保持される.

> setClass("TimeSeries",
+          representation(
+              data="numeric",
+              start="POSIXct",
+              end="POSIXct"
+                         )
+          )
[1] "TimeSeries"

スロットのデフォルト値はprototypeにより指定する.

> setClass("TimeSeries",
+          representation(
+              data="numeric",
+              start="POSIXct",
+              end="POSIXct"
+                         ),
+          prototype(
+                    data = 0,
+                    start = as.POSIXct("1970/1/1"),
+                    end = as.POSIXct("2000/1/1")
+                    )
+          )
[1] "TimeSeries"
> new("TimeSeries")
An object of class "TimeSeries"
Slot "data":
[1] 0

Slot "start":
[1] "1970-01-01 JST"

Slot "end":
[1] "2000-01-01 JST"

オブジェクトの作製

作成したクラスのオブジェクトはnew()関数により作製できる.

> my.ts <- new("TimeSeries",
+              data = 1:5,
+              start= as.POSIXct("2011/1/1 1:11:11"),
+              end  = as.POSIXct("2011/1/9 0:00:00")
+              )
> my.ts
An object of class "TimeSeries"
Slot "data":
[1] 1 2 3 4 5

Slot "start":
[1] "2011-01-01 01:11:11 JST"

Slot "end":
[1] "2011-01-09 JST"

スロットへのアクセスは@演算子を通じて行う.

> my.ts@data
[1] 1 2 3 4 5

オブジェクトの検証

クラス定義の際に指定した型以外の型を持つデータをスロットに代入することはできない.
しかし,型以外の構造を検査したい場合もあるだろう.
例えば例のTimeSeriesクラスならばstartはendより前にあるべきだし,startとendはそれぞれ長さ1のベクトルでなければならない.
あるクラスのオブジェクトが満たすべき要件をチェックする検証メソッドはsetValidity()関数によって指定でき,その後validObject()関数によりオブジェクトの検証ができる.

> setValidity("TimeSeries",
+             function(object){
+               object@start <= object@end &&
+               length(object@start) == 1 &&
+               length(object@end) == 1
+             }
+             )
Class "TimeSeries" [in ".GlobalEnv"]

Slots:
                              
Name:     data   start     end
Class: numeric POSIXct POSIXct
> validObject(my.ts)
[1] TRUE

また,この後作製するオブジェクトについてはオブジェクト作成時に自動的に検証される.

> my.ts.invalid <- new("TimeSeries",
+                      data=1:5,
+                      start= as.POSIXct("2011/1/11 1:11:11"),
+                      end  = as.POSIXct("2011/1/9 0:00:00")
+                      )
 以下にエラー validObject(.Object) : 
   不正なクラス "TimeSeries" オブジェクト:  FALSE
> my.ts.invalid
 エラー:  オブジェクト 'my.ts.invalid' がありません 

generic function

Rではgeneric function(総称的関数)という仕組みによりポリモーフィズムが実現されている.
ある関数をgeneric functionとして宣言しておけば,その関数に与えたオブジェクトのクラスに応じて異なる関数を適用することができる.
例えば,TimeSeriesクラスのオブジェクトからデータのサンプリング間隔を取り出す関数periodを定義する場合を例にとる.
まず,関数periodをオブジェクトのperiodスロットにアクセスする関数として定義する.
setGeneric()関数を適用すると,その関数は総称的関数となる.

> period <- function(object) object@period
> setGeneric("period")
[1] "period"

次に,サンプリング間隔を計算する関数としてperiod.TimeSeriesを定義する.

period.TimeSeries <- function(object){
  if(length(object@data) > 1){
    (object@end - object@start) /
      (length(object@data) - 1)
  } else {
    Inf
  }
}

この関数を総称的関数periodのTimeSeriesクラスに対するメソッドとして定義するには,setMethod()関数を使用する.

> setMethod(period,
+           signature=c("TimeSeries"),
+           definition=period.TimeSeries)
[1] "period"
attr(,"package")
[1] ".GlobalEnv"

これでTimeSeriesクラスのオブジェクトに対してperiod関数を使用した場合にperiod.TimeSeries関数が適用されるようになる.

> period(my.ts)
Time difference of 1.987642 days

setMethod("summary", sigunature="TimeSeries", definition=云々)のようにすればTimeSeriesクラスに対するsummary関数が定義できる.
ただし,この方法で新しいメソッドを定義できるのは一部の組み込み関数にのみであり,例えばprint()関数に新しいメソッドを定義することはできない.
総称的関数のメソッドの一覧はshowMethods()で参照できる(S3クラスの場合はmethods()であることに注意).
また,メソッドを用意していないクラスを総称的関数の引数とした場合は最初に用意した関数が適用される.

> period(1:5)
 以下にエラー period(1:5) : 
   スロット "period"を、スロットを持たない基本クラス("integer")のオブジェクトから得ようとしました 
> period("123")
 以下にエラー period("123") : 
   スロット "period"を、スロットを持たない基本クラス("character")のオブジェクトから得ようとしました 
> showMethods(period)
Function: period (package .GlobalEnv)
object="ANY"
object="character"
    (inherited from: object="ANY")
object="integer"
    (inherited from: object="ANY")
object="TimeSeries"

(動作を見る限りでは,新しいクラスを引数とする度にデフォルトの関数をそのまま継承したそのクラス用のメソッドが用意されるらしい)

継承

クラスの継承はsetClassの引数containに既存のクラスを指定することで行う.
継承されたクラスは継承元のクラスのスロットを引き継ぐ.

> setClass("WeightHistory",
+          representation(
+                         height = "numeric",
+                         name = "character"
+                         ),
+          contains = "TimeSeries"
+          )
[1] "WeightHistory"
> jhon.doe <- new("WeightHistory",
+                 data = c(170, 169, 171, 168, 170, 169),
+                 start = as.POSIXct("2009/02/14 0:00:00"),
+                 end = as.POSIXct("2009/3/28 0:00:00"),
+                 height = 72,
+                 name = "Jhon Doe")
> jhon.doe
An object of class "WeightHistory"
Slot "height":
[1] 72

Slot "name":
[1] "Jhon Doe"

Slot "data":
[1] 170 169 171 168 170 169

Slot "start":
[1] "2009-02-14 JST"

Slot "end":
[1] "2009-03-28 JST"

また,オブジェクトの検証メソッドについても親クラスの定義を受け継ぐ.
試しにstartとendを入れ替えてみると,

> jhon.doe <- new("WeightHistory",
+                 data = c(170, 169, 171, 168, 170, 169),
+                 end = as.POSIXct("2009/02/14 0:00:00"),
+                 start = as.POSIXct("2009/3/28 0:00:00"),
+                 height = 72,
+                 name = "Jhon Doe")
 以下にエラー validObject(.Object) : 
   不正なクラス "WeightHistory" オブジェクト:  FALSE

多重継承

containsに複数のクラス名をベクトルとして渡せば複数のクラスを継承することができる.

> setClass("Person",
+          representation(
+                         height = "numeric",
+                         name = "character"
+                         )
+          )
[1] "Person"
> setClass("AltWeightHistory",
+          contain = c("TimeSeries", "Person")
+          )
[1] "AltWeightHistory"

仮想クラス

クラスCatを定義する.

> setClass("Cat",
+          representation(
+                         breed = "character",
+                         name = "character"
+                         )
+          )
[1] "Cat"

CatはクラスPersonと同じくnameスロットを備えている.そこで,例えばPersonとCatのnameスロットを対象としたメソッドをまとめて定義できたら便利だろう.
setClassUnion()を用いると引数にベクトルとして与えたクラスのスーパークラスであるような仮想クラスを定義できる.

> setClassUnion("NamedThing",
+               c("Person", "Cat")
+               )
[1] "NamedThing"
> showClass("NamedThing")
Virtual Class "NamedThing" [in ".GlobalEnv"]

No Slots, prototype of class "NULL"

Known Subclasses: 
Class "Person", directly
Class "Cat", directly
Class "AltWeightHistory", by class "Person", distance 2

クラスNamedThingに対してメソッドを定義すればサブクラスであるPersonやCatにもそのメソッドを適用できる.
同名のメソッドがすでにPersonやCatに存在していれば上書きされる.
なお,NamedThingのオブジェクトは作製できない.

> new("NamedThing")
 以下にエラー new("NamedThing") : 
   仮想クラス ("NamedThing") からオブジェクトを生成しようとしています