ライブラリの読み込み
library(methods)S4はS3と同じくR言語のオブジェクト指向プログラミングのためのクラスです。S3がS言語のver.3を意味するのと同様に、S4はS言語のver.4(1998年)で実装されたため、S4と呼ばれています。
S4はS3よりはオブジェクト指向っぽいクラスで、クラスの定義においてクラスの要素(Slots)のデータ型を固定することができます。ただし、メソッド(Rでは関数)はクラスの定義とは別に定義しないといけないのが他のオブジェクト指向言語(RubyやPython)とは異なります。
RでS4が最も用いられているのはBioconductorのパッケージ群です。Bioconductorではパッケージの開発時にS4を使うよう定められているため、Bioconductorで用いられるクラスほぼS4です。したがって、Bioconductorのパッケージ群を利用する生物学者、生物情報学者の方はS4の知識を持っているほうがよいでしょう。
Bioconductorのように非常に大規模なパッケージ群を作成する場合、S4を用いることでより統一感のあるクラス群を比較的簡単に実装することができます。
一方でS4は使いかたが複雑で、S3より学ぶのが難しいです。規模が小さいプロジェクトやパッケージではS3クラスで十分である場合もあります。
以下では、S4について書かれたテキスト(Genolini 2008)とAppsilonのS4についてのポスト、Advanced RのS4の章(Wickham 2019)を参考にS4について説明していきます。
とは言え、S4もS3と同じくオブジェクト指向プログラミングのためのクラスです。S3でのクラス定義の基本的な概念はS4にも通じます。
S4でも、S3と同じく以下の要素をクラスの定義として記載していくことになります。
S4はmethodsパッケージで設定されているクラスです。methodsパッケージはRを起動すると自動的に読み込まれるようになっていますが、並列演算を行う場合などには読み込まれない場合もあります。必要に応じてパッケージを読み込むようにするとよいでしょう。
ライブラリの読み込み
library(methods)S4では専用の関数でクラスを定義します。クラスの定義に用いる関数はsetClass関数です。
setClass関数はたくさんの引数を取れるのですが、最低限必要な引数はClassです。Classにはクラス名を文字列で指定します。
S4ではClass名には単語の頭を大文字にするアッパーキャメルケース(MyClassDefinitionなど)を用いるのが一般的です。
クラスの定義:setClass関数
setClass(Class = "MyDog")S4では、そのクラスの要素のことをSlotsと呼びます。Slotsには名前と値が設定できます。Slotsにはlistのような数値でのインデックスはなく、名前のインデックスだけが設定されており、呼び出しや書き込みにはSlotsの名前を用います。
S3とS4で大きく異なるのは、S3がその要素のデータ型を固定していないのに対し、S4ではslotsのデータ型が常に固定されている点です。このため、S3のように野放図に要素のデータ型を変えることはできず、S3より堅牢なクラスを作成することができます。
SlotsはsetClass関数のslots引数に指定します。slots引数には、名前付きの文字列ベクターで、そのSlotsの名前とデータ型を指定します。
データ型には、basicクラス(Rで設定されているクラス)やS4クラスなどを指定することができます。設定可能なクラスは以下の表の通りです。
| 設定する値 | データ型・クラス |
|---|---|
| “character” | 文字列 |
| “complex” | 複素数 |
| “double” | 数値(倍精度浮動小数点) |
| “expression” | 演算 |
| “integer” | 整数 |
| “list” | リスト |
| “logical” | 論理型 |
| “numeric” | 数値 |
| “single” | 数値(浮動小数点) |
| “raw” | バイトデータ |
| “vector” | ベクター(virtual) |
| “S4” | S4(virtual) |
| “NULL” | NULL(空オブジェクト) |
| “function” | 関数 |
| “externalptr” | Cのポインタ |
| “ANY” | どれでもよい(virtual) |
| “VIRTUAL” | VIRTUALクラス |
| “missing” | 値が無い |
| “namedList” | 名前付きリスト |
このsetClassで設定したクラスのオブジェクト(インスタンス)は、new関数で作成することができます。ですので、S4でコンストラクタに当たるのはこのnew関数です。しかし、new関数でオブジェクトを作成することは推奨されておらず、別途ヘルパー関数を作成してオブジェクトを作成する方がよいとされています。
上記のMyDogクラスのオブジェクトをnew関数で作成すると、Slotsに何も登録されていないオブジェクトが作成されます。
コンストラクタ:new関数
Pochi <- new("MyDog")
PochiAn object of class "MyDog"
Slot "name":
character(0)
Slot "trick":
character(0)
Slot "age":
numeric(0)
new関数内でSlotsの要素を引数として指定すると、Slotsに指定した値が登録されます。
new関数:Slotsを引数で指定する
Shiro <- new("MyDog", name="Shiro", trick = "hand", age = 15)
ShiroAn object of class "MyDog"
Slot "name":
[1] "Shiro"
Slot "trick":
[1] "hand"
Slot "age":
[1] 15
設定したSlotsは@を用いて呼び出すことができます。ただし、S4ではクラスのユーザーが@でSlotsを呼び出すことは推奨されておらず、アクセサ(ゲッターとセッター)を準備すべきであるとされています。
Slotsを呼び出す
Pochi@namecharacter(0)
Pochi@trickcharacter(0)
Pochi@agenumeric(0)
Slotsへの値の設定も@を用いて行うことができます。このSlotsへの値の設定では、値の型がクラスの設計時に指定したデータ型と一致する必要があり、データ型が一致しない場合には値を設定することはできません。
Slotsに値を設定する
Pochi@name <- "Pochi"
# ageにはnumericを指定しているため、文字列は設定できない
Pochi@age <- "10"Error:
! assignment of an object of class "character" is not valid for @'age' in an object of class "MyDog"; is(value, "numeric") is not TRUE
# 年齢として不自然な値は設定できる
Pochi@age <- 150上記のMyDogクラスの定義ではSlotsの要素の長さを指定していません。ですので、ageに2つの値を指定するようなこともできます。
Slotsの要素の数
Pochi@age <- c(15, 150)
Pochi@age[1] 15 150
setClass関数では、Slotsに指定するデフォルトの値をprototype引数で設定することができます。
デフォルト値を指定しておくと、new関数でオブジェクトを作成した際にSlotsにデフォルトとして指定した値が設定されます。
デフォルト値が指定されたオブジェクト
Kuro <- new("MyDog")
KuroAn object of class "MyDog"
Slot "name":
[1] "Kuro"
Slot "trick":
[1] "sit"
Slot "age":
[1] 5
上の例では初期値に具体的な値を指定しましたが、多くの犬の名前がKuroで、お座りが得意で、5歳、というわけではないので、クラスの設計時に初期値を設定する意味はありません。初期値は設定しないか、もしくは以下のようにNAを指定しておくとよいとされています。
NAの指定時にはNAのデータ型がslotsに指定した型と同じである必要があるため、単にNAを用いるのではなく、NA_character_やNA_real_、NA_integer_などのデータ型付のNAを指定します。
クラスの情報を取得するための関数はいくつかあります。classNameはクラス名と定義されているenvironmentを返す関数、slotNamesはSlotsの名前を返す関数、getSlotsはSlotsのデータ型を名前付きベクターで返す関数、getClass関数はclassNameとgetSlotsを合わせた情報を表示する関数です。いまいち使いどころが難しい関数ですが、後ほど説明する継承でスーパークラスの情報を得るような場合に用いるとよいでしょう。
クラスの情報を返す関数
# クラス名を返す
className("MyDog")An object of class "className"
[1] "MyDog"
Slot "package":
[1] ".GlobalEnv"
# Slots名を返す
slotNames("MyDog")[1] "name" "trick" "age"
# Slotsの情報を返す
getSlots("MyDog") name trick age
"character" "character" "numeric"
# クラスとSlotsの情報を返す
getClass("MyDog")Class "MyDog" [in ".GlobalEnv"]
Slots:
Name: name trick age
Class: character character numeric
S4クラスはクラス自体の定義をremoveClass関数で削除することができます。
クラスの削除:removeClass
removeClass("MyDog")[1] TRUE
上記の通り、S4ではSlotsのデータ型を指定できます。その他のSlotsの値の制限、例えばSlotsに設定可能な値の長さや範囲などは、別途validity引数に指定します。このvalidity引数はS3で説明したバリデータに当たるものです。
validity引数は関数で指定します。この関数で指定した返り値がFALSEになった時には、オブジェクト作成時にエラーが出ます。
以下のようにnameの要素が2つだとvalidityの条件がFALSEになるため、エラーが出ます。
Error in `validObject()`:
! invalid class "MyDog" object: FALSE
2つ以上の条件をvalidityに指定することもできますが、単に条件だけを表記すると、返り値が最後の条件(以下の例ではlength(object@name) == 1)だけに従ってしまいます。
ですので、以下のようにnameの要素の長さが1であれば、object@age<25が成り立っていなくてもオブジェクトが作成できてしまいます。
An object of class "MyDog"
Slot "name":
[1] "Pochi"
Slot "trick":
[1] "sit" "hand"
Slot "age":
[1] 30
ですので、validityはstopやstopifnot関数のようなエラーを出す関数で指定したほうが良いでしょう。
バリデータを設定しておくと、validObject関数でオブジェクトに設定したSlotsに問題がないかを確認することができます。
validObject関数でオブジェクトの状態を確認する
Pochi <- new("MyDog", name = "Pochi", trick = c("sit", "hand"), age = 15)
# バリデータの条件に合致しているとTRUEが返ってくる
validObject(Pochi)[1] TRUE
バリデータはsetClass内だけではなく、setValidity関数で設定することもできます。
setValidity関数でバリデータを設定する
setValidity(
Class = "MyDog",
function(object){stopifnot(length(object@name) == length(object@age))}
)Class "MyDog" [in ".GlobalEnv"]
Slots:
Name: name trick age
Class: character character numeric
このsetValidityでバリデータを設定すると、setClassでクラスを定義した時のバリデータが上書きされます。エラーの原因となりますので、setValidityは使わず、バリデータはsetClass内できちんと定義したほうがよいでしょう。
同じ名前のクラスをsetClassで作成し直してみましょう。MyDogクラスを作り直し、slotsの名前が変わると、元のMyDogクラスで作成したオブジェクトはエラーを吐き出すようになります。クラスを途中で作り直すとエラーの原因となりますので、できればクラスはそのクラスを用いるスクリプトとは別ファイルで定義し、source関数で読み込んで使いましょう。また、スクリプト内ではクラスの設定を変えない方がよいでしょう。
クラスに適切にバリデータが設定できたら、次にヘルパーを作成していきます。ヘルパーはクラスのオブジェクトを作成するときの関数です。S4では、setMethod関数を用いてクラスオブジェクトを引数とする関数を作成していきます。
まずはsetMethod関数でクラス作成時に実行される関数(initializer)を作成します。initializerはその関数の名前であるinitializeで呼び出すのではなく、new関数が呼び出されたときに実行される関数です。クラスオブジェクトを作成したときに表示される文字列やSlotsに設定するattributes(ベクターの名前や行列・データフレームのdim、列名など)の設定を定義しておくと良いでしょう。
initializerを設定するには、setMethod関数の関数名を指定するf引数にf="initialize"を指定します。setMethod関数ではsignature引数にクラス名、definition引数に関数の内容を記載します。definition引数には関数を指定します。
また,initializer内でオブジェクトの構造を変更するような場合には,definition内でvalidObject関数を呼び出し,変更後のオブジェクトが適切な構造を持つか評価することもできます.
オブジェクト作成時に実行される関数:initialize
# setMethod関数でinitializeを定義する
setMethod(
f = "initialize",
signature = "MyDog",
definition = function(.Object, name, trick, age){
cat("--- MyDog: Initialized --- \n")
.Object@name <- name
.Object@trick <- trick
.Object@age <- age
cat("name: ", .Object@name, "\n")
cat("trick: ", .Object@trick, "\n")
cat("age: ", .Object@age, "\n")
# validObject(.Object) # バリデータを持ち込む場合(オブジェクトが変更されないなら不要)
return(.Object)
}
)
# newでオブジェクトを作成すると、initializeで定義した関数が実行される
Pochi <- new("MyDog", "Pochi", "sit", 10)--- MyDog: Initialized ---
name: Pochi
trick: sit
age: 10
S4ではクラスオブジェクトをnew関数で作成できますが、このnew関数はクラス名と結びついておらず、クラスの作成するにはちょっと使いにくい関数です。
S3の章で説明した通り、Rでは関数名と作成するオブジェクトのクラスが一致する場合が多いです。例えば、factorを作成するときにはfactor関数、data.frameを作成するときにはdata.frame関数を用いています。
S4でも同じく、クラス名と同じ名前の関数でオブジェクトを作成するほうが良いでしょう。このオブジェクト作成のための関数の名前には、はじめの単語以外の頭を大文字にするロウワーキャメルケース(myClassDefinitionなど)を用います。
ユーザー用のコンストラクタを設定する
# 通常のfunctionで作成できる
myDog <- function(name, trick, age){
new("MyDog", name, trick, age)
}
Pochi <- myDog("Pochi", "sit", 10)--- MyDog: Initialized ---
name: Pochi
trick: sit
age: 10
また、同様のコンストラクタはsetClass関数の返り値を変数に代入することでも設定できます。
An object of class "MyCat"
Slot "name":
character(0)
S4では、RubyやPythonのようなメソッドはなく、関数だけが設定できます。このS4クラスの関数を作成するための関数がsetMethod関数です。
一方、上記のようにS4を引数に用いる場合にもfunctionで関数を作成することもできます。
setMethodでもfunctionでも関数は作成できますが、setMethod関数では引数のデータ型などを指定でき、より堅牢な関数を作成できます。その代わり、ジェネリック関数としての指定が必要となるなど、関数の設計が複雑になります。
しっかりとしたS4クラス用の関数が必要な場合にはsetMethod、その他の場合にはfunctionを用いるとよいでしょう。
上記のinitializerですでに説明しましたが、S4でジェネリック関数を設定する場合には、setMethod関数を用います。まずは既存の関数であるshow関数のジェネリックを設定していきます。
まずshow関数の引数を調べます。args関数を用いると関数の引数名を調べることができます。show関数はobjectを引数に取っています。
print関数の引数名を調べる
args("show")function (object)
NULL
次に、setMethod関数でクラスに対応したジェネリック関数を作成します。f引数は関数名を文字列で指定します。show関数のジェネリックを作成する場合には、f="show"を指定します。
signature引数には引数のクラスを指定します。このsignatureにはクラス名の文字列、名前付きベクターを取ることができます。また、やや複雑なのですが、同名の関数があり、signature引数にsignature関数を取ることもできます。
signature関数はジェネリック関数の引数=クラス名という引数を取る関数で、ジェネリック関数の引数に特定のクラスを指定するために用います。ベクターでsignatureを設定する場合も同様の名前付きベクターとして設定します。以下の例ではshow関数の引数objectにMyDogクラスが指定された場合のshow関数を設定するため、signature関数の引数はobject="MyDog"としています。
このsignatureの指定時に、ジェネリック関数の引数名が元の関数と異なるとエラーが出ます。show関数であれば元の引数名であるobjectを用いる必要があります。
関数の定義はdefinitionに関数として記載します。initializerと同様に、関数内でクラスの要素を変更する場合には、validObject関数を呼びだして引数をチェックすることもできます。
既存の関数にジェネリック関数を設定する
setMethod(
f = "show",
signature(object = "MyDog"), # signature = c(object = "MyDog")でも可
definition = function(object){
cat("--- MyDog: print --- \n")
cat("The name of my dog is ", object@name, ".\n", sep = "")
cat("He is good at ", object@trick, "ing. \n", sep = "")
cat("His age is ", object@age, ".\n", sep = "")
}
)上記のようにsetMethod関数で定義したジェネリック関数を用いると、定義の通りに関数の返り値が得られます。
print.MyDog関数
show(Pochi)--- MyDog: print ---
The name of my dog is Pochi.
He is good at siting.
His age is 10.
S4では、print関数ではなくshow関数を設定し、オブジェクトを表示するのが一般的です。show関数はmethodsパッケージの関数で、S4の表示にはshowDefaultという関数が用いられています。
show関数で呼び出し
Pochi--- MyDog: print ---
The name of my dog is Pochi.
He is good at siting.
His age is 10.
# Slotsは通常通り呼び出せる
Pochi@age[1] 10
S4ではこのshow関数のジェネリックを作成し、オブジェクトの表示に用いるのが一般的です。単にオブジェクト名を宣言した場合には、Rの通常のオブジェクトではprint関数が呼ばれますが、S4ではshow関数が呼ばれます。
S4ではprintではなく、show関数のジェネリックを設定しておくのが有益でしょう。
S3のジェネリック関数とは異なり、S4ではsetMethods関数はshow.MyDog関数を作成・呼び出しているわけではありません。S4のジェネリック関数はmethods関数では表示されず、showMethods関数を用いて調べる必要があります。
S4のジェネリック関数を表示:showMethods
showMethods("show")Function: show (package methods)
object="ANY"
object="classGeneratorFunction"
object="classRepresentation"
object="envRefClass"
object="externalRefMethod"
object="genericFunction"
object="genericFunctionWithTrace"
object="MethodDefinition"
object="MethodDefinitionWithTrace"
object="MethodSelectionReport"
object="MethodWithNext"
object="MethodWithNextWithTrace"
object="MyDog"
object="namedList"
object="ObjectsWithPackage"
object="oldClass"
object="refClassRepresentation"
object="refMethodDef"
object="refObjectGenerator"
object="signature"
object="sourceEnvironment"
object="traceable"
返り値を見ると、show関数として、一番上に引数objectが"ANY"の場合、途中に"MyDog"の場合が設定されていることがわかります。この"ANY"は最も親のスーパークラスのようなもので、show関数を設定していないS4クラスのオブジェクトを引数にした場合には、オブジェクトのクラスが"ANY"であるとして関数が呼び出されます。
この"ANY"については継承を説明する際にもう一度解説します。
上記のsetMethod関数ではshow関数という、すでに存在するmethodsの関数のジェネリック関数を作成しました。
同様に、S4に対して新しい名前のジェネリック関数を設定したい場合もあります。しかし、setMethod関数でいきなり新しい名前の関数を設定しようとするとエラーが出ます。
新しい名前のジェネリック関数をsetMethodで作成
Error in `setMethod()`:
! no existing definition for function 'ageDays'
S4では,新しい名前の関数をsetMethod関数でいきなり定義することはできません。S4クラスのオブジェクトに対応する関数を新しい名前で作成する場合、まずはsetGeneric関数でジェネリック関数名を設定する必要があります。
このsetGeneric関数は関数名と関数の内容を引数に取りますが、関数の内容としては必ずstandardGeneric関数を呼び出す関数を指定することになっています。S3におけるUseMethodのようなものだと思っておくとよいかと思います。
このsetGeneric関数で宣言した関数名はRのmethods関数で探索できるようになります.
ジェネリック関数名を定める:setGeneric関数
setGeneric("ageDays", function(x, ...) standardGeneric("ageDays"))[1] "ageDays"
setGeneric関数で宣言した後、setGeneric関数で同じ関数名をもう一度宣言するとジェネリック関数の設定が更新されてしまいます。更新されると前に宣言したジェネリック関数の設定は破棄されます。
S4ではクラスやメソッドを簡単に破壊できる設計になっています。簡単にジェネリック関数を再設定できると困るため、lockBindingを用いて関数名を環境(グローバル環境)にロックしてしまう方がよいとされている場合もあります。
lockBinding関数でsetGenericを再宣言できなくする
lockBinding("ageDays",.GlobalEnv)実際にS4を用いるのはパッケージを作成する時で、パッケージのクラスや関数の定義はパッケージの環境(environment)で展開されています。
パッケージの環境では関数の定義内のようにローカル変数として変数や関数が設定されています。関数内で定義した変数を関数外(グローバル環境)から変更できないように、パッケージ内でのクラスの定義もグローバル環境からは通常は変更できません。
ですので、「パッケージを作成せずにグローバル環境でS4を定義する」といった特殊な場合を除けば、クラスを破壊的に変更してしまうことはあまりないでしょう。
setGeneric関数で指定した関数名を用いて、setMethod関数でクラスの関数(クラスメソッド)を作成します。以下の例のageDate関数はMyDogオブジェクトのageに365をかけて文字列を返すだけの関数です。
MyDogクラスのオブジェクトを引数に取ると、関数の定義の通り実行されていることがわかります。
ジェネリック関数を実行する
ageDays(Pochi)[1] "3650 days"
S4のsetGenelic・setMethodで作成した関数は通常のRの関数とは少し取り扱いが異なります。methodsパッケージではS4の関数専用の情報取得用の関数が設定されています。
showMethods関数はそのクラスに定義された関数の情報を、getMethod関数は特定の関数の定義について返す関数です。existsMethodはその関数があるかどうかを論理型で返す関数です。
S4の関数の情報を取得する
# クラスに紐づいたS4の関数の一覧を返す
showMethods(class = "MyDog")Function: ageDays (package .GlobalEnv)
x="MyDog"
Function "asJSON":
<not an S4 generic function>
Function: initialize (package methods)
.Object="MyDog"
Function: show (package methods)
object="MyDog"
# あるクラス(signature)の関数(f)の定義を返す
getMethod(f = "ageDays", signature = "MyDog")Method Definition:
function (x, ...)
{
.local <- function (x)
{
paste(365 * x@age, "days")
}
.local(x, ...)
}
Signatures:
x
target "MyDog"
defined "MyDog"
# 関数が存在するかどうかを論理型で返す
existsMethod(f = "ageDays", signature = "MyDog")[1] TRUE
ジェネリック関数が設定できるようになったので、Slotsの値を取得するための関数(ゲッター)とSlotsに値を設定するための関数(セッター)を設計していきます。上で説明した通り、S4では@を用いればSlotsの値を取得したり設定したりできるのですが、クラスの利用者には@からSlotsにアクセスできないようにした方がよいとされています。
ゲッターとセッターはRの通常の関数(function)で定義することもできるのですが、引数の型を固定するためにS4のジェネリック関数として設定したほうがよいでしょう。S4のジェネリック関数として設定するため、setGenelicとsetMethod関数を用いて関数を定義していきます。
ゲッターとしては、getXXX関数(XXXはSlotsの名前)をまずは定義します。Rでは四角カッコ([ ])を用いて要素を取り出す、つまり[ ]をゲッターとして利用する習慣がありますので、必要に応じて四角カッコをゲッターとして設定してもよいでしょう。
getXXX関数の定義は上のジェネリック関数の定義の手順と同じです。まずはsetGenelic関数でジェネリック関数名を定めます。
setGeneric関数で関数名を指定する
setGeneric("getName", function(x, ...) standardGeneric("getName"))[1] "getName"
次に、setMethod関数でSlotsの要素を返す関数を定義します。ゲッターではSlotsの値を変更することはないため、バリデータを設定する必要はないでしょう。
四角カッコ([ ])はRでは[という関数で定義されています。ですので、[をジェネリック関数として設定すれば四角カッコでSlotsを呼び出すことができるゲッターを作成することができます。
`[`を関数として用いる
v <- 1:3
# ベクターvの2番目の要素を返す
`[`(v, 2)[1] 2
[はbaseの関数なのでsetGenelic関数での設定は必要ありません。
[の引数はx、i、j、dropの4つです。ココではxとiを設定します。[の定義では、x[i]という形、つまりxがオブジェクトでiがインデックスになっています。
xにMyDogクラスのオブジェクト、iに文字列のインデックスを指定するように関数を作成します。
`[`にジェネリック関数の設定を加える:引数が文字列
上記のように関数を定義すると、四角カッコの中に文字列でSlots名を指定することで、そのSlotsの要素を取り出せるようになります。しかし、iに数値が入ることは想定していないため、数値が入るとエラーが出ます。
[]でスロットを取り出す(文字列)
Pochi["name"][1] "Pochi"
Pochi["age"][1] 10
Pochi["birthday"]Error in `.local()`:
! "name", "trick", "age"のいずれかを指定してください
# iは文字列を指定しているので、数値だとエラーが出る
Pochi[1]Error in `Pochi[1]`:
! object of type 'S4' is not subsettable
iに数値が入った場合のジェネリック関数は別途setMethod関数で定義できます。
同じ関数を2回定義しているのでやや不思議な感じはしますが、数値のインデックスでもSlotsの要素が取り出せます。
数値・文字列の両方のインデックスで値を取り出す
Pochi[1][1] "Pochi"
Pochi[2:3][1] "sit" "10"
# 文字列でも取り出せる
Pochi["trick"][1] "sit"
このように、signatureに引数のクラスを複数指定して定義すると、引数のクラスに応じて異なる関数を呼び出し、応答させることができます。この同名の関数が引数の型によって応答を変える仕組みのことをmultiple dispatch(多重ディスパッチ)と呼びます。Rの標準のジェネリック関数もこの多重ディスパッチの仲間のようなものです。
上記の例では数値のインデックスでSlotsにアクセスする方法を定義していますが、S4では意図があってSlotsへの数値でのインデックス指定ができないようにされています。
数値は名前とリンクしていないので、番号を間違えるとエラーの素になります。文字列でインデックスを指定するほうが間違いは少なくなります。
signatureのデータ型と同名の関数の働き(multiple dispatch)について説明するために数値でのゲッターを作成しましたが、通常はこのような数値インデックスでアクセスするようなゲッターは設定しないほうがよいでしょう。
値をSlotsに代入し変更する、セッターも同様にsetGeneric、setMethod関数を用いて作成します。まずはsetGenericでジェネリック関数の名前を付けます。以下ではsetNameという関数名を設定しています。setName関数の引数はxと、関数内で別の関数を用いる場合にその関数の任意の引数を用いることができるよう、...を指定しておきます。
# 関数名はsetName、引数はxと...として指定
setGeneric("setName", function(x, ...) standardGeneric("setName"))[1] "setName"
次に、上記で設定したsetName関数の定義をsetMethod関数で記載しておきます。setName関数はシンプルで、nameのSlotsに第2引数であるyを代入して返すものです。
Slotsを変更するので、setClass関数で定義したバリデータの条件を満たさない値を代入できると困ります。バリデータの条件を満たすかどうかチェックするため、関数内でvalidObject関数を呼び出し、バリデータを持ち込みます。
関数の返り値がないと何も返ってこないため、最後にオブジェクトであるxを返しています。
nameのセッターを定義する
setMethod(
f = "setName",
signature = c(x = "MyDog"),
definition = function(x, y){
x@name <- y
validObject(x)
return(x)
}
)setName関数を用いると、名前が"Pochi"から"Kuro"に変更できています。
セッターを用いてみる
setName(Pochi, "Kuro")--- MyDog: print ---
The name of my dog is Kuro.
He is good at siting.
His age is 10.
Rでは、セッターとして値を代入する関数(例えば、v[1] <- 2など)を用いることがあります。このような関数は関数名<-という名前の関数として設定されています。ベクターのインデックスへの代入では、[<-という名前の関数を用いています。
セッターにこのような代入の関数を設定する場合、上記の通り関数名<-という関数を設定します。上記のsetName関数を代入可能とする場合には、setGenelic関数にsetName<-という関数名を指定します。オブジェクトを指定するxと、valueという代入する数値を指定する2つの引数を設定します。
<-を含む関数名を設定する
# 第2引数はvalueとする必要がある
setGeneric("setName<-", function(x, value) standardGeneric("setName<-"))[1] "setName<-"
次に、関数の定義を指定するのですが、この関数の定義にはsetMethod関数ではなく、setReplaceMethod関数を用います。関数名は<-のない名前とします。関数の定義は上記のsetNameと同じものとなります。
setReplaceMethodで関数を定義する
setReplaceMethod(
f = "setName",
signature = c(x = "MyDog", value = "character"),
definition = function(x, value){
x@name <- value
validObject(x)
return(x)
}
)上記のように関数を定義すると、<-でSlotsの値を変更することができるようになります。
<-で値を変更する
setName(Pochi) <- "Shiro"
Pochi--- MyDog: print ---
The name of my dog is Shiro.
He is good at siting.
His age is 10.
S4では定義や利用の方法がS3とは異なりますが、特に大きく異なる特徴は継承(inheritance)の仕組みです。
S3の継承はattributeに設定した「クラス名のベクター」を用いたジェネリック関数利用の仕組みです。
S3クラスにおける継承
[1] "seq_vec" "vec"
ジェネリック関数はこのクラス名のベクターの左側から、関数名.クラス名という名前を持つ関数を探し、マッチングした関数を実行するという形で機能しています。
S3では継承はクラス名のベクターとして設定されていますので、継承したクラス間でクラスの要素やバリデータが異なっていても問題ありません。とは言っても、クラスの要素やバリデータが異なるとジェネリック関数をうまく利用できないため、サブクラス(子クラス)にもスーパークラス(親クラス)と同じような要素やバリデータを設定する必要があります。
S4での継承はS3のような単なるクラス名のベクターではなく、「スーパークラスの設計をサブクラスに持ちこむ」ためのシステムです。普通のオブジェクト指向プログラミング言語における継承に近いものになっています。
スーパークラスを継承したサブクラスでは、スーパークラスと同じSlotsの要素、バリデータを持つことになります。サブクラスではスーパークラスから継承を行うことで、スーパークラスだけでは実装が難しい機能やデータを追加的に実装しつつ、スーパークラスの要素をうまく利用することができます。
S4でのサブクラスの定義では、通常のS4クラスの定義と同じくsetClass関数を用います。setClass関数のcontains引数にスーパークラス名をベクターで設定することでサブクラスを定義することができます。
ただし、このサブクラスをnew関数で作成しようとすると、エラーが出ます。
new関数でオブジェクトを作成できない
Error in `.local()`:
! unused argument (birthday = 16926)
エラーが出るのは、すでに設定していたMyDogクラスのinitializerにbirthdayという引数が無いためです。new関数ではBdMyDogクラスのinitializerがないため、MyDogクラスのinitializerを呼び出してしまいます。
BdMyDogクラスのinitializerを設定すると、new関数でサブクラスを作成することができます。initializerの設計時に、definition引数に...を設定しておいてもよいでしょう。
サブクラス用のinitializerを作成する
setMethod(
f = "initialize",
signature = "BdMyDog",
definition = function(.Object, name, trick, age, birthday){
cat("--- BdMyDog: Initialized --- \n")
.Object@name <- name
.Object@trick <- trick
.Object@age <- age
.Object@birthday <- birthday
cat("name: ", .Object@name, "\n")
cat("trick: ", .Object@trick, "\n")
cat("age: ", .Object@age, "\n")
cat("birthday: ", as.Date(.Object@birthday) |> as.character(), "\n")
return(.Object)
}
)
new("BdMyDog", name = "Pochi", trick = "sit", age = 10, birthday = as.Date("2016/5/5"))--- BdMyDog: Initialized ---
name: Pochi
trick: sit
age: 10
birthday: 2016-05-05
--- MyDog: print ---
The name of my dog is Pochi.
He is good at siting.
His age is 10.
S3ではクラスはベクターの要素でしかないため、継承は1次元的、つまりサブクラスからスーパークラスまでは分岐がありません。S4では、1つのサブクラスが2つのスーパークラスから継承する、多重継承の仕組みを持ちます。この多重継承はサブクラスに複数の機能を持ち込むことができる便利な仕組みであると同時に、サブクラスの構造やジェネリック関数の適用を複雑にする仕組みでもあります。
多重継承のサブクラスを作成するのはそれほど難しくありません。まずはスーパークラスとするクラスを一つ作成します。
スーパークラスをもう一つ作成する
setClass(
"PoliceDogLicense",
slots = c(licensed_date = "Date", expiration_date = "Date", valid_for = "numeric")
)
setMethod(
f = "initialize",
signature = "PoliceDogLicense",
definition = function(.Object, licensed_date, valid_for){
library(lubridate)
cat("--- PoliceDogLicense: Initialized --- \n")
.Object@licensed_date <- ymd(licensed_date)
.Object@expiration_date <-
(ymd(licensed_date) + years(valid_for)) |>
suppressWarnings()
.Object@valid_for <- valid_for
cat("licensed on: ", .Object@licensed_date |> ymd() |> as.character(), "\n")
cat("effective until: ", .Object@expiration_date |> ymd() |> as.character(), "\n")
if(.Object@valid_for == 1){
cat("valid for: ", .Object@valid_for, "year","\n")
} else {
cat("valid for: ", .Object@valid_for, "years","\n")
}
return(.Object)
}
)スーパークラス:PoliceDogLicense
--- PoliceDogLicense: Initialized ---
licensed on: 2025-12-10
effective until: 2026-12-10
valid for: 1 year
次に、BdMyDogクラスとPoliceDogLicenseクラスを継承した、PoliceDogクラスを作成してみます。2つのスーパークラスを承継する場合には、contains引数にベクターでスーパークラス名を指定します。
このクラスにもinitializerを定義します。
多重継承:initializerを定義する
setMethod(
f = "initialize",
signature = "PoliceDog",
definition = function(.Object, name, trick, age, birthday, licensed_date, valid_for){
library(lubridate)
cat("--- PoliceDog: Initialized --- \n")
.Object@name <- name
.Object@trick <- trick
.Object@age <- age
.Object@birthday <- birthday
.Object@licensed_date <- ymd(licensed_date)
.Object@expiration_date <-
(ymd(licensed_date) + years(valid_for)) |>
suppressWarnings()
.Object@valid_for <- valid_for
cat("name: ", .Object@name, "\n")
cat("trick: ", .Object@trick, "\n")
cat("age: ", .Object@age, "\n")
cat("birthday: ", as.Date(.Object@birthday) |> as.character(), "\n")
cat("licensed on: ", .Object@licensed_date |> ymd() |> as.character(), "\n")
cat("effective until: ", .Object@expiration_date |> ymd() |> as.character(), "\n")
if(.Object@valid_for == 1){
cat("valid for: ", .Object@valid_for, "year","\n")
} else {
cat("valid for: ", .Object@valid_for, "years","\n")
}
return(.Object)
}
)initializerを設定すれば、多重継承したクラス(PoliceDog)のオブジェクトを作成することができます。
多重継承:オブジェクトを作成する
--- PoliceDog: Initialized ---
name: Pochi
trick: sit
age: 10
birthday: 2016-05-06
licensed on: 2025-05-06
effective until: 2026-05-06
valid for: 1 year
PoliceDogはMyDogクラス、BdMyDogクラス、PoliceDogLicenseクラスを継承したクラスになります。ですので、Slotsには3つのクラスで設定したもの(name、trick、age、birthday、licensed_date、expiration_date、valid_for)がすべて含まれます。
PoliceDogLicenseクラスの構造
str(temp)Formal class 'PoliceDog' [package ".GlobalEnv"] with 7 slots
..@ birthday : Date[1:1], format: "2016-05-06"
..@ name : chr "Pochi"
..@ trick : chr "sit"
..@ age : num 10
..@ licensed_date : Date[1:1], format: "2025-05-06"
..@ expiration_date: Date[1:1], format: "2026-05-06"
..@ valid_for : num 1
また、スーパークラスで設定したジェネリック関数であるgetNameを適用することができます。
スーパークラスで設定したジェネリック関数を利用する
getName(temp)[1] "Pochi"
46章で説明したigraph(Csardi and Nepusz 2006)とクラスの継承をadjacency matrixで評価してくれるclassesToAMを用いると、クラスの継承をグラフで示すことができます。
classesToAM関数で継承のグラフを作成する
library(igraph)
# adjacency matrixで継承を示す
classesToAM(c("MyDog", "BdMyDog", "PoliceDogLicense", "PoliceDog")) MyDg BdMD PlDL PlcD
MyDog 0 0 0 0
BdMyDog 1 0 0 0
PoliceDogLicense 0 0 0 0
PoliceDog 0 1 1 0
# igraphの関数に渡すと、継承をグラフに表示できる
classesToAM(c("MyDog", "BdMyDog", "PoliceDogLicense", "PoliceDog")) |>
graph_from_adjacency_matrix() |>
plot(vertex.size=50)ここまでは特に問題はありません。問題が生じるのは、同名のジェネリック関数が多重継承したクラスの両方に存在する場合です。
例えばgetName関数のジェネリック関数をPoliceDogLicenseに設定した場合、PoliceDogクラスのオブジェクトにはMyDogクラスのgetNameか、PoliceDogLicenseクラスのgetNameのどちらが適用されるのか、という問題が起こります。
S4では、このような場合にはPoliceDogLicenseクラスのgetNameが適用されます。これは、MyDogクラスからの継承がMyDog->BdMyDog->PoliceDogという形で「2つ前」のスーパークラスであるのに対し、PoliceDogLicenseクラスはPoliceDogLicense->PoliceDogと「1つ前」のスーパークラスであるためです。この「2つ前」や「1つ前」を「継承の距離」、とすると、S4では継承の距離が近いスーパークラスのジェネリック関数を利用するようになっています。
距離が同じ場合には、クラス名のアルファベット順にジェネリック関数が選ばれます。ですので、BdMyDogとPoliceDogLicenseクラスの同名のジェネリック関数がある場合、PoliceDogにはBdMyDogのジェネリック関数が適用されます。
このジェネリック関数の問題では同名のジェネリック関数を設定しなければ問題はありません。しかし、ジェネリック関数の引数のデータ型にS4クラスを用いる場合には、なおややこしいことが起こります(詳しくはAdvanced Rのmultiple dispatchに関する記載を参考にして下さい)。
ですので、多重継承する場合にはクラスの設計、クラスの関数名、引数に使うクラスの性質、実際に適用される巻数に気を配る必要があります。
S4には、isとasという関数があります。isは引数のクラスを確認するための関数、asは引数のクラスを変更するための関数です。is.numericやas.numericなどと同じような関数だと考えるとわかりやすいでしょう。
まずはis関数から説明します。is関数は第一引数にオブジェクト、第二引数にクラス名を指定し、オブジェクトがそのクラスに属しているかを評価する関数です。オブジェクトがそのクラスに属していればTRUEを、そうでなければFALSEを返します。
クラスの継承を行っている場合には、オブジェクトがサブクラスであればスーパークラスに対してもTRUEが返ってくるのに対し、オブジェクトがスーパークラスでの場合にはサブクラスに対してはFALSEが返ってきます。
is関数
# PochiはMyDogクラスのオブジェクトなのでTRUE
is(Pochi, "MyDog")[1] TRUE
# tempはPoliceDogクラスのオブジェクトで、MyDogのサブクラスなのでTRUE
is(temp, "MyDog")[1] TRUE
# PochiはMyDogクラスのオブジェクトで、PoliceDogのスーパークラスなのでFALSE
is(Pochi, "PoliceDog")[1] FALSE
is(temp, "PoliceDog")[1] TRUE
次にas関数について説明します。as関数はオブジェクトのクラスを変更する関数です。as関数は第一引数にオブジェクト、第二引数にクラス名を取り、オブジェクトを第二引数で指定したクラスに変換します。この変換はスーパークラス→サブクラス、サブクラス→スーパークラスの両方で行うことができます。
as関数
An object of class "ClassB"
Slot "bar":
character(0)
Slot "foo":
character(0)
as(tempB, "ClassA") # ClassBのオブジェクトをClassAのオブジェクトにするAn object of class "ClassA"
Slot "foo":
character(0)
An object of class "ClassB"
Slot "bar":
character(0)
Slot "foo":
character(0)
しかし、as関数でデータ構造の違うクラスに変換することはできません。
as関数で全く異なるクラスに変換しようとする
as(Pochi, "character")Error in `as.character.default()`:
! no method for coercing this S4 class to a vector
このような場合、データの変換の方法をあらかじめsetAs関数で指定しておくと変換することができます。
変換のルールを定める:setAs関数
[1] "Shiro"
上記のシンプルなClassA、ClassBの場合にはas関数でクラスをサブクラス・スーパークラスの相互に変換することができました。一方で、すでに作成したMyDogクラスのオブジェクトであるPochiをサブクラスであるBdMyDogクラスに変換しようとすると、こちらはエラーが出ます。
initializerをあらかじめ設定していると、変換時にinitializerの演算が行われるため、変換時に実行されるinitializerでエラーが出てしまうためです。このような場合にはinitializerの設計を見直すか、あらかじめsetAs関数で変換の方法を指定したほうがよいでしょう。
initializerがあるためエラーが出るケース
as(Pochi, "BdMyDog")--- BdMyDog: Initialized ---
Error in `.local()`:
! argument "name" is missing, with no default
また、このsetAs関数によく似た名前のsetIs関数というものもあるのですが、setIs関数はis関数の方法を指定するのではなく、スーパークラスを設定するための関数です。用途が違う関数ですので間違って用いることがないようにしましょう。
setIs関数はsetClass関数のcontains引数に当たるもの、つまりスーパークラスをsetClass関数外からそのクラスに設定するための関数です。
setIs関数は現在のクラスのスーパークラスを設定するため、そのクラスのオブジェクトにis関数を適用してスーパークラスへの所属を評価すると、TRUEが返ってくるようになります。確かにis関数の方法を指定していると言えなくはなく、setIsと言っていい関数ではあります。
しかし、setIs関数でスーパークラスを設定した場合、そのクラスがスーパークラスのSlotsを持たないとエラーが出ます。
引用するパッケージで定義されているクラスにスーパークラスを特別に設定したい、というような場合以外にはなかなか使い道がない上に、スーパークラスのSlotsをすべて持っていないと設定ができないため、利用するときには気を使う必要があります。
Error in `setIs()`:
! class "ClassB" cannot extend class "ClassA": class "ClassB" is missing slot from class "ClassA" (foo), and no coerce method was supplied
Warning in .removePreviousCoerce(class1, class2, where, prevIs): methods
currently exist for coercing from "ClassB" to "ClassA"; they will be replaced.
getClass("ClassB")Class "ClassB" [in ".GlobalEnv"]
Slots:
Name: foo bar
Class: character character
Extends: "ClassA"
setClass関数のSlotsにデータ型・クラスを指定する場合や、setMethods関数のsignature引数にデータ型・クラスを登録する場合に、一つの型やクラスではない値を取りたい、という場合もあります。
例えば、以下のクラスのSlotsであるvalue_or_characterには数値か文字列を設定できるようにしたいとします。シンプルに数値も文字列も設定したいのであれば、すべてのクラスのスーパークラスである"ANY"を設定してしまうのもよいでしょう。
Slotsの型にANYを指定する
An object of class "ClassC"
Slot "value_or_character":
[1] 1
new("ClassC", value_or_character = "Hello World")An object of class "ClassC"
Slot "value_or_character":
[1] "Hello World"
確かにこの方法だとvalue_or_characterは数値でも文字列でも設定できるのですが、その他のデータ型(リストやデータフレーム)も設定できてしまいます。
An object of class "ClassC"
Slot "value_or_character":
$x
[1] "a"
$y
[1] "b"
そこで、Slotsのデータ型を数値と文字列だけに限定する際に用いるのが、Class Unionです。Class Unionは『複合クラス』のようなもので、『数値と文字列』のようなクラスを別途作成できるようにするものです。
Class UnionはsetClassUnion関数で作成することができます。setClassUnion関数の引数は複合クラス名を示すnameと、複合クラスに含まれるデータ型・クラスを指定するmemberの2つです。
Class Unionを設定する
setClassUnion(
name = "ValorChar",
members = c("numeric", "character")
)SlotsにClass Unionを指定する
An object of class "ClassD"
Slot "value_or_character":
[1] 1
new("ClassD", value_or_character = "Hello World")An object of class "ClassD"
Slot "value_or_character":
[1] "Hello World"
Error in `validObject()`:
! invalid class "ClassD" object: invalid object for slot "value_or_character" in class "ClassD": got class "list", should be or extend class "ValorChar"
このClass Unionもなかなか使いどころが難しいですが、知っておくと利用できる場合もあるでしょう。例えば、上記の四角カッコ([])に文字列と数値で引数を指定したい場合に、setMethods関数のsignatureに上記の"ValorChar"というClass Unionを用いれば、文字列も数値もインデックスに取ることができる関数を作成することができます。
最後にVIRTUALクラスについて説明します。すでに basicクラス に示していた通り、basicクラスのいくつかはVIRTUALクラスとして設定されています。
setClass関数でSlotsを設定しない時と、contains = "VIRTUAL"を指定した場合には、そのクラスはVIRTUALクラスとなります。
VIRTUALクラスを作成する
Virtual Class "ClassE" [in ".GlobalEnv"]
No Slots, prototype of class "NULL"
getClass("ClassF")Virtual Class "ClassF" [in ".GlobalEnv"]
Slots:
Name: foo
Class: character
VIRTUALクラスのオブジェクト(インスタンス)を作成しようとすると、エラーが出ます。VIRTUALクラスのオブジェクトを作成することはできません。
VIRTUALクラスではオブジェクトを作成できない
new("ClassE")Error in `new()`:
! trying to generate an object from a virtual class ("ClassE")
new("ClassF")Error in `new()`:
! trying to generate an object from a virtual class ("ClassF")
これだけではVIRTUALクラスの意味がよくわかりませんが、以下のようなAppsilonのS4についてのポストに示されている例を考えると比較的わかりやすいです。
まずはAnimalというクラスをVIRTUALクラスとして作成します。Animalには名前と年齢、脚の数をSlotsに設定します。
AnimalクラスはVIRTUALクラスなので、オブジェクトは作成できません。
VIRTUALクラスのオブジェクトは作成できない
new("Animal")Error in `new()`:
! trying to generate an object from a virtual class ("Animal")
しかし、このAnimalクラスを継承したクラスでは、Animalクラスと同じSlotsを持つオブジェクトを作成することができます。
VIRTUALクラスを継承する
An object of class "Human"
Slot "name":
[1] "Mitsuo"
Slot "age":
[1] 67
Slot "legs":
[1] 2
new("Octopus", name = "Takoyaki", age = 10, legs = 8)An object of class "Octopus"
Slot "name":
[1] "Takoyaki"
Slot "age":
[1] 10
Slot "legs":
[1] 8
このAnimalクラスに関数を設定しておけば、HumanクラスでもOctopusクラスでも、その関数を利用することができます。
したがって、VIRTUALクラスは同じ構造をした様々なクラスを作成する際にスーパークラスとして利用することができるクラスです。VIRTUALクラスでバリデータや関数をきちんと作成しておくことで、同じ構造を持つサブクラスを作成した際にもバリデータや関数を利用できます。