Rのこと。

記事は引っ越し作業中。2023年中までに引っ越しを完了させてブログは削除予定

Advanced R: S3

はじめに

Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。

この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。

S3

ここでは、S3の主要コンポーネント(クラス、ジェネリック、メソッド)の概要、継承、メソッドディスパッチの詳細について説明されている。

S3オブジェクトは、少なくても1つのクラス属性を持つ基本型のこと。たとえば、factorは基本型はintegerで、クラスがfactorlevels属性を持つ。

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は基本形がdoubleDateクラスをもつ。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.framet()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つの引数を取る。ジェネリック関数の名前と、メソッドのディスパッチに使用する引数。

例えばmeanUseMethod()への呼び出しだけを含む。

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

呼び出し方によっては、メソッドを変更できる。また、orderedfactorのサブクラスであり、factororderedスーパークラスと呼ぶ。

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言語を呼び出す。