Advanced R: S3
はじめに
Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。
この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。
S3
ここでは、S3の主要コンポーネント(クラス、ジェネリック、メソッド)の概要、継承、メソッドディスパッチの詳細について説明されている。
S3オブジェクトは、少なくても1つのクラス属性を持つ基本型のこと。たとえば、factor
は基本型はinteger
で、クラスがfactor
でlevels
属性を持つ。
library(sloop) f <- factor(c("a", "b", "c")) typeof(f) [1] "integer" attributes(f) $levels [1] "a" "b" "c" $class [1] "factor"
unclass()
でクラス属性を削除できる。
unclass(f) [1] 1 2 3 attr(,"levels") [1] "a" "b" "c"
例えば、table()
はどうなっているのか。基本形はinteger
で、dim
属性、dimnames
属性、table
クラスを持っている。
x <- table(rpois(100, 5)) typeof(x) [1] "integer" attributes(x) $dim [1] 12 $dimnames $dimnames[[1]] [1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" $class [1] "table"
S3オブジェクトは、generic function
に渡されると、基本型とは異なる動作を行う。ジェネリック関数かどうかはsloop::ftype()
で確認できる。
ftype(print) [1] "S3" "generic" ftype(str) [1] "S3" "generic" ftype(plot) [1] "S3" "generic" ftype(unclass) [1] "primitive"
ジェネリック関数は、クラスに応じて異なる実装を使用するインタフェースを定義する。base Rの多くの関数はジェネリック関数。
print(f) [1] a b c Levels: a b c print(unclass(f)) [1] 1 2 3 attr(,"levels") [1] "a" "b" "c"
例えば、POSIXlt
は基本型はdouble
で、クラスがPOSIXlt
である。特定のクラスの実装はメソッドと呼ばれ、ジェネリック関数は、クラスをもとにジェネリック関数がインターフェースとなり、メソッドディスパッチを行うことで、そのメソッドを見つける。
time <- strptime(c("2017-01-01", "2020-05-04 03:21"), "%Y-%m-%d") class(time) [1] "POSIXlt" "POSIXt" print(time) [1] "2017-01-01 JST" "2020-05-04 JST" print(unclass(time)) $sec [1] 0 0 $min [1] 0 0 $hour [1] 0 0 $mday [1] 1 4 $mon [1] 0 4 $year [1] 117 120 $wday [1] 0 1 $yday [1] 0 124 $isdst [1] 0 0 $zone [1] "JST" "JST" $gmtoff [1] NA NA
some_days
は基本形がdouble
でDate
クラスをもつ。Date
クラスのときは、mean.Date
メソッドが使われるが、unclass()
すると、クラスがなくなり基本形に戻るので、mean.default
メソッドが使われる。
set.seed(1014) some_days <- as.Date("2017-01-31") + sample(10, 5) mean(some_days) [1] "2017-02-06" mean(unclass(some_days)) [1] 17202.2
s3_dispatch()
で、メソッドディスパッチのプロセスを確認できる。プロセスであって実装されているメソッドが表示されているわけではない。また、S3メソッドのソースコードは、関数名を入力しても確認はできないので、s3_get_method()
を使う。
s3_dispatch(mean(some_days)) => mean.Date * mean.default s3_get_method(mean.Date) function (x, ...) .Date(mean(unclass(x), ...)) <bytecode: 0x7f848ca7d378> <environment: namespace:base>
一般的には.
の後にクラスが記述されるので、メソッドはその点を見れば判断できるが、下記のようにややこしいものもある。t.test
はt検定のための関数で、転置関数t()
のtest
クラスへのメソッドではない。t.data.frame
はt()
のdata.frame
クラスに対するメソッドである。as.data.frame.data.frame()
なんかもそう。as.data.frame()
のdata.frame
クラスへのメソッドを実装している。近年は、関数名に.
を使うとややこしいので、_
が使われている。
ftype(t.test) [1] "S3" "generic" ftype(t.data.frame) [1] "S3" "method"
クラス
オブジェクトにクラスのインスタンスを作成するには、単にclass
属性を設定する。structure()
やclass<-()
で設定できる。
x <- structure(list(), class = "my_class") class(x) [1] "my_class" x <- list() class(x) <- "my_class" class(x) [1] "my_class" inherits(x, "my_class") [1] TRUE inherits(x, "your_class") [1] FALSE
既存のオブジェクトのクラスを変更することが可能なので、注意が必要。
mod <- lm(log(mpg) ~ log(disp), data = mtcars) class(mod) [1] "lm" print(mod) Call: lm(formula = log(mpg) ~ log(disp), data = mtcars) Coefficients: (Intercept) log(disp) 5.381 -0.459 class(mod) <- "Date" print(mod) Error in as.POSIXlt.Date(x): (list) object cannot be coerced to type 'double'
独自のクラスを作成するとき、このような問題を避けるために、通常は3つの機能を提供することが推奨される。
- A low-level constructor:
new_myclass()
が正しく、かつ効率的な構造を持つ新しいオブジェクトなのか。 - validator:
validate_myclass()
オブジェクトが正しい値を有することへの保証すること。 - helper:他人があなたの
myclass()
オブジェクトを作成するための方法を提供すること。
constructor
S3はクラスの正式な定義を提供されない。つまり、同じ基本型と同じ型を持つ同じ属性を持つことを保証する仕組みはない。代わりに、コンストラクタを使用して一貫した構造を強制する。S3クラスDate
のコンストラクタを作る。Date
は、ただ1つのDate
クラスを持ち、基本形はdouble
。
new_Date <- function(x = double()) { stopifnot(is.double(x)) structure(x, class = "Date") } new_Date(c(-1, 0, 1)) [1] "1969-12-31" "1970-01-01" "1970-01-02"
S3クラスdifftime
のコンストラクタを作る。difftime
の基本形はdouble
ですが、先ほどとは異なり、units
を持つ。
new_difftime <- function(x = double(), units = "secs") { stopifnot(is.double(x)) units <- match.arg(units, c("secs", "mins", "hours", "days", "weeks")) structure(x, class = "difftime", units = units ) } new_difftime(c(1, 10, 3600), "secs") Time differences in secs [1] 1 10 3600 new_difftime(52, "weeks") Time difference of 52 weeks
validator
複雑なクラスでは、妥当性について複雑なチェックが必要。例えば、factor
で考えてみる。コンストラクタは型が正しいことだけをチェックし、不正な形式の要素を作成することを可能にしている。
new_factor <- function(x = integer(), levels = character()) { stopifnot(is.integer(x)) stopifnot(is.character(levels)) structure( x, levels = levels, class = "factor" ) } new_factor(1:3, "a") Error in as.character.factor(x): malformed factor new_factor(1:3, c("a", "b", "c")) [1] a b c Levels: a b c
コンストラクタで完結させるのではなく、不正な形式の要素に対するチェックは別の関数validate_factor
に分けるほうが望ましい。
validate_factor <- function(x) { values <- unclass(x) levels <- attr(x, "levels") if (!all(!is.na(values) & values > 0)) { stop( "All `x` values must be non-missing and greater than zero", call. = FALSE ) } if (length(levels) < max(values)) { stop( "There must be at least as many `levels` as possible values in `x`", call. = FALSE ) } x } validate_factor(new_factor(1:5, "a")) Error: There must be at least as many `levels` as possible values in `x` validate_factor(new_factor(0:1, "a")) Error: All `x` values must be non-missing and greater than zero
helper
自分が作成たクラスからオブジェクトを構築したい場合、ヘルパーメソッドも提供するべき。ヘルパーには下記の機能をもたせる。
myclass()
と同じ名前を付ける。- コンストラクタとバリデータがあればそれを呼び出して終了。
- エンドユーザー向けのエラーメッセージを作成。
- デフォルト値と便利な変換を備えた、ユーザーインターフェースを持つ。
例えば、new_difftime()
はdouble
ベクトルを使用できる場合、integer
ベクトルを使用できるという規則に違反している。なので、変換するヘルパー関数を作成する。
new_difftime(1:10) Error in new_difftime(1:10): is.double(x) is not TRUE difftime <- function(x = double(), units = "secs") { x <- as.double(x) new_difftime(x, units = units) } difftime(1:10) Time differences in secs [1] 1 2 3 4 5 6 7 8 9 10
例えば、character
ベクトルを使用してfactor
のレベルを指定すると便利。以下では、character
ベクトルを取り、レベルはユニークな数と推測。
factor <- function(x = character(), levels = unique(x)) { ind <- match(x, levels) validate_factor(new_factor(ind, levels)) } factor(c("a", "a", "b")) [1] a a b Levels: a b
メソッドディスパッチ
S3ジェネリック関数の仕事は、メソッドディスパッチを実行することである、クラス似たする特定の実装を見つけること。メソッドのディスパッチはUseMethod()
によって行われる。UseMethod()
は2つの引数を取る。ジェネリック関数の名前と、メソッドのディスパッチに使用する引数。
例えばmean
はUseMethod()
への呼び出しだけを含む。
mean function (x, ...) UseMethod("mean") <bytecode: 0x2ced990> <environment: namespace:base>
UseMethod()
は、ジェネリック関数が持つメソッドの一覧から、クラスに対する適切なメソッドを取得する。
x <- Sys.Date() paste0("print", ".", c(class(x), "default")) [1] "print.Date" "print.default" s3_dispatch(print(x)) => print.Date * print.default
これだけであれば簡潔ですが、実際は複雑です。
x <- matrix(1:10, nrow = 2) class(x) [1] "matrix" s3_dispatch(mean(x)) mean.matrix mean.integer mean.numeric => mean.default s3_dispatch(sum(Sys.time())) sum.POSIXct sum.POSIXt sum.default => Summary.POSIXct Summary.POSIXt Summary.default -> sum (internal)
メソッドは実際にいかに探されているのか。sloop::s3_dispatch()
は、メソッドディスパッチのプロセスを見るためことが可能。ジェネリック関数のクラスに関連付けられているすべてのメソッドを見つけたい場合はsloop::s3_methods_generic()
とsloop::s3_methods_class()
を使用する。
s3_methods_generic("mean") # A tibble: 6 x 4 generic class visible source <chr> <chr> <lgl> <chr> 1 mean Date TRUE base 2 mean default TRUE base 3 mean difftime TRUE base 4 mean POSIXct TRUE base 5 mean POSIXlt TRUE base 6 mean quosure FALSE registered S3method s3_methods_class("ordered") # A tibble: 4 x 4 generic class visible source <chr> <chr> <lgl> <chr> 1 as.data.frame ordered TRUE base 2 Ops ordered TRUE base 3 relevel ordered FALSE registered S3method 4 Summary ordered TRUE base
継承
S3クラスは、継承と呼ばれるメカニズムを通じて動作します。クラスは文字ベクトルであり、複数のコンポーネントでも成り立つ。
class(ordered("x")) [1] "ordered" "factor" class(Sys.time()) [1] "POSIXct" "POSIXt"
ベクトルの最初の要素でクラスのメソッドが見つからない場合、2番目のクラスのメソッドを探す。
s3_dispatch(print(ordered("x"))) print.ordered => print.factor * print.default s3_dispatch(print(Sys.time())) => print.POSIXct print.POSIXt * print.default
呼び出し方によっては、メソッドを変更できる。また、ordered
はfactor
のサブクラスであり、factor
はordered
のスーパークラスと呼ぶ。
s3_dispatch(ordered("x")[1]) [.ordered => [.factor [.default -> [ (internal) s3_dispatch(Sys.time()[1]) => [.POSIXct [.POSIXt [.default -> [ (internal)
S3と基本型
S3と基本型の関係について。クラスのないオブジェクトを使ってS3ジェネリック関数を呼ぶとどうなるのか。このような場合、3つの要素を持つ暗黙のクラスを発生させます。
class(matrix(1:5)) [1] "matrix" s3_class(matrix(1:5)) [1] "matrix" "integer" "numeric"
これらを使って、メソッドディスパッチを行う。
s3_dispatch(print(matrix(1:5))) print.matrix print.integer print.numeric => print.default
また、baseの関数は、C言語で実装されているものがいくつかある。これらは内部ジェネリックとよばれ、UseMethod()
は使われず、C言語を呼び出す。