9
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

[翻訳]R6 vignette: R6クラス入門

この文書は,Winston Chang によるRパッケージ R6 (version 2.2.0) のビネット "Introduction to R6 classes" の日本語訳です.

License: MIT

関連文書


R6パッケージは,R標準の参照クラスに類似した種類のクラスを提供します.ただしR6はより効率的であり,S4クラスやmethodsパッケージに依存しません.

R6クラス

R6クラスはR標準の参照クラスに似ていますが,より軽量であり,S4クラスの使用に伴う問題を回避しています(Rの参照クラスはS4に基づいています).速度とメモリ負荷に関する詳しい情報については,「性能」のビネットを参照してください.

Rの多くのオブジェクトとは異なり,R6クラスのインスタンス(オブジェクト)は参照セマンティクスです.また,R6クラスは以下の機能をサポートしています.

  • パブリックメソッドとプライベートメソッド
  • 活性束縛(active bindings)
  • パッケージをまたいで動作する継承(スーパークラス)

なぜパッケージ名がR6なのでしょうか?Rの参照クラスが導入された際に,Rの既存のクラスシステム名であるS3とS4に従って,新しいクラスシステムを冗談でR5と呼ぶユーザがいました.参照クラスが実際にR5と名付けられることはありませんでしたが,このパッケージとクラスの名前はそこからインスピレーションを得ています.

また,R5はSimon UrbanekがS4の文法と性能に関する問題を解決するために始めた,別のオブジェクトシステムのコードネームでもありました.しかしR5のブランチは少し開発が行われた後に中止され,リリースされることはありませんでした.

基本

単純なR6クラスを作成する方法を示します.public引数はリストであり,リストの要素にできるのは関数かフィールド(関数以外)です.関数はメソッドとして使われます.

library(R6)

Person <- R6Class("Person",
  public = list(
    name = NULL,
    hair = NULL,
    initialize = function(name = NA, hair = NA) {
      self$name <- name
      self$hair <- hair
      self$greet()
    },
    set_hair = function(val) {
      self$hair <- val
    },
    greet = function() {
      cat(paste0("Hello, my name is ", self$name, ".\n"))
    }
  )
)

このクラスのオブジェクトをインスタンス化するには$new()を使います.

ann <- Person$new("Ann", "black")
#> Hello, my name is Ann.
ann
#> <Person>
#>   Public:
#>     clone: function (deep = FALSE) 
#>     greet: function () 
#>     hair: black
#>     initialize: function (name = NA, hair = NA) 
#>     name: Ann
#>     set_hair: function (val)

$new()メソッドはオブジェクトを作成し,initialize()メソッドが存在していれば呼び出します.

クラスのメソッド内において,selfはそのクラスのオブジェクト自身のことを指します.オブジェクトのパブリックメンバ(ここまで見てきたものはすべてそうです)にはself$xでアクセスし,代入はself$x <- yで行います.後で見るように,移植不可(non-portable)クラスではselfを省略できますが,デフォルトではメンバにアクセスするのにselfが必要なことに注意してください.

オブジェクトがインスタンス化されたら,値やメソッドには$でアクセスできます.

ann$hair
#> [1] "black"
ann$greet()
#> Hello, my name is Ann.
ann$set_hair("red")
ann$hair
#> [1] "red"

実装についてのノート:外側から見ると,基本的にはR6オブジェクトというのはパブリックメンバを含む環境です.これはまた,パブリック環境として知られているものです.R6オブジェクトのメソッドはこれとは別にエンクロージング環境を持ちます.おおまかに言うと,エンクロージング環境とはその中でメソッドが実行される環境のことです.エンクロージング環境ではselfが束縛されている値が見つかりますが,これはパブリック環境への単なる参照です.

プライベートメンバ

前の例ではすべてのメンバがパブリックでしたが,プライベートメンバを加えることもできます.

Queue <- R6Class("Queue",
  public = list(
    initialize = function(...) {
      for (item in list(...)) {
        self$add(item)
      }
    },
    add = function(x) {
      private$queue <- c(private$queue, list(x))
      invisible(self)
    },
    remove = function() {
      if (private$length() == 0) return(NULL)
      # private$queue を使って明示的にアクセスできる
      head <- private$queue[[1]]
      private$queue <- private$queue[-1]
      head
    }
  ),
  private = list(
    queue = list(),
    length = function() base::length(private$queue)
  )
)

q <- Queue$new(5, 6, "foo")

パブリックメンバにはselfを使ってself$add()のようにアクセスしますが,プライベートメンバにはprivateを使ってprivate$queueのようにアクセスします.

パブリックメンバには通常通りアクセスできます.

# 項目の追加と削除
q$add("something")
q$add("another thing")
q$add(17)
q$remove()
#> [1] 5
q$remove()
#> [1] 6

しかし,プライベートメンバには直接アクセスすることはできません.

q$queue
#> NULL
q$length()
#> Error: attempt to apply non-function

可能であればメソッドがselfを(不可視的に)返すようにするのは有用なデザインパターンです.というのもメソッドの連鎖ができるようになるからです.たとえば,add()メソッドはselfを返すので,連鎖させることができます.

q$add(10)$add(11)$add(12)

一方,remove()は削除された値を返すので,連鎖させることはできません.

q$remove()
#> [1] "foo"
q$remove()
#> [1] "something"
q$remove()
#> [1] "another thing"
q$remove()
#> [1] 17

活性束縛(active binding)

活性束縛は見かけはフィールドのようですが,アクセスされる度に関数を呼び出します.活性束縛は常にパブリックです.

Numbers <- R6Class("Numbers",
  public = list(
    x = 100
  ),
  active = list(
    x2 = function(value) {
      if (missing(value)) return(self$x * 2)
      else self$x <- value/2
    },
    rand = function() rnorm(1)
  )
)

n <- Numbers$new()
n$x
#> [1] 100

活性束縛に値を読むかのようにしてアクセスすると,対応する関数がvalue引数を欠損として呼び出されます.

n$x2
#> [1] 200

値を代入するかのようにしてアクセスすると,代入値がvalue引数として使われます.

n$x2 <- 1000
n$x
#> [1] 500

活性束縛した関数が引数を取らない場合,活性束縛を<-と一緒に使うことはできません.

n$rand
#> [1] 0.2648
n$rand
#> [1] 2.171
n$rand <- 3
#> Error: unused argument (quote(3))

実装についてのノート:活性束縛はパブリック環境に束縛されます.活性束縛の関数のエンクロージング環境もパブリック環境です.

継承

ひとつのR6クラスは別のR6クラスを継承することができます.言い換えると,スーパークラスとサブクラスを作ることができます.

サブクラスは追加のメソッドを持つことができ,スーパークラスのメソッドをオーバーライドするメソッドを持つこともできます.以下の履歴を保持するキューの例では,show()メソッドを追加してremove()メソッドをオーバーライドします.

# このキューは非効率なので注意.単に継承を例示するためのものです.
HistoryQueue <- R6Class("HistoryQueue",
  inherit = Queue,
  public = list(
    show = function() {
      cat("Next item is at index", private$head_idx + 1, "\n")
      for (i in seq_along(private$queue)) {
        cat(i, ": ", private$queue[[i]], "\n", sep = "")
      }
    },
    remove = function() {
      if (private$length() - private$head_idx == 0) return(NULL)
      private$head_idx <<- private$head_idx + 1
      private$queue[[private$head_idx]]
    }
  ),
  private = list(
    head_idx = 0
  )
)

hq <- HistoryQueue$new(5, 6, "foo")
hq$show()
#> Next item is at index 1 
#> 1: 5
#> 2: 6
#> 3: foo
hq$remove()
#> [1] 5
hq$show()
#> Next item is at index 2 
#> 1: 5
#> 2: 6
#> 3: foo
hq$remove()
#> [1] 6

スーパークラスのメソッドはsuper$xx()で呼び出せます.以下の例のCountingQueueは,これまでにキューに追加されたことのあるオブジェクトの個数を保持します.これはadd()メソッドをオーバーライドして,カウンタをインクリメントしてからスーパークラスのadd()メソッドをsuper$add(x)で呼び出すことによって実現されています.

CountingQueue <- R6Class("CountingQueue",
  inherit = Queue,
  public = list(
    add = function(x) {
      private$total <<- private$total + 1
      super$add(x)
    },
    get_total = function() private$total
  ),
  private = list(
    total = 0
  )
)

cq <- CountingQueue$new("x", "y")
cq$get_total()
#> [1] 2
cq$add("z")
cq$remove()
#> [1] "x"
cq$remove()
#> [1] "y"
cq$get_total()
#> [1] 3

参照オブジェクトを含むフィールド

R6クラスが参照セマンティクスを持つフィールド(たとえば他のR6オブジェクトや環境)を含む場合,それらのフィールドはinitialize()メソッドで初期化するべきです.もしクラス定義の中で直接それらのフィールドが参照オブジェクトに設定されると,そのオブジェクトがR6オブジェクトのすべてのインスタンスで共有されてしまいます.

SimpleClass <- R6Class("SimpleClass",
  public = list(x = NULL)
)

SharedField <- R6Class("SharedField",
  public = list(
    e = SimpleClass$new()
  )
)

s1 <- SharedField$new()
s1$e$x <- 1

s2 <- SharedField$new()
s2$e$x <- 2
# s2$e$x を変更すると s1$e$x の値も変わってしまう
s1$e$x
#> [1] 2

これを回避するには,フィールドをinitialize()メソッドで初期化するようにします.

NonSharedField <- R6Class("NonSharedField",
  public = list(
    e = NULL,
    initialize = function() self$e <- SimpleClass$new()
  )
)

n1 <- NonSharedField$new()
n1$e$x <- 1

n2 <- NonSharedField$new()
n2$e$x <- 2
# n2$e$x は n1$e$x には影響を与えない
n1$e$x
#> [1] 1

移植可能(portable)クラスと移植不可(non-potable)クラス

R6のバージョン1.0.1はデフォルトでは移植不可クラスを作成するようになっていましたが,後続のバージョンではデフォルトで移植可能クラスを作成するようになっています.重要な相違点が2つあります.

  • 移植可能クラスはパッケージをまたいだ継承をサポートしています.移植不可クラスではこれがうまくできません.
  • 移植可能クラスでは,メンバにアクセスするのに必ずselfprivateが必要であり,self$xprivate$yのようにしてアクセスします.移植不可クラスでは,こういったメンバには単にxyでアクセスでき,代入は<<-演算子で行います.

前者を実装するには後者が必要になります.

self<<-の使用

参照クラスではフィールドにselfなしでアクセスでき,フィールドへの代入は<<-を使って行います.以下はその例です.

RC <- setRefClass("RC",
  fields = list(x = 'ANY'),
  methods = list(
    getx = function() x,
    setx = function(value) x <<- value
  )
)

rc <- RC$new()
rc$setx(10)
rc$getx()
#> [1] 10

移植不可のR6クラスも同様です.

NP <- R6Class("NP",
  portable = FALSE,
  public = list(
    x = NA,
    getx = function() x,
    setx = function(value) x <<- value
  )
)

np <- NP$new()
np$setx(10)
np$getx()
#> [1] 10

しかし,移植可能なR6クラス(これがデフォルトですが)ではselfprivateを使わなければなりません.<<-はうまく動作しません(もちろん,selfを使わない限りはですが).

P <- R6Class("P",
  portable = TRUE,  # これがデフォルト
  public = list(
    x = NA,
    getx = function() self$x,
    setx = function(value) self$x <- value
  )
)

p <- P$new()
p$setx(10)
p$getx()
#> [1] 10

さらなる情報については「移植性」のビネットを見てください.

その他のトピック

既存のクラスへのメンバ追加

クラスが既に作成された後にメンバを追加するのが有用なことがあります.クラスのジェネレータオブジェクトの$set()メソッドを使えばこれが可能です.

Simple <- R6Class("Simple",
  public = list(
    x = 1,
    getx = function() self$x
  )
)

Simple$set("public", "getx2", function() self$x*2)

# 既存のメンバを置き換えるには overwrite=TRUE を使う
Simple$set("public", "x", 10, overwrite = TRUE)

s <- Simple$new()
s$x
#> [1] 10
s$getx2()
#> [1] 20

新たなメンバを持つのは$set()を呼び出した後に作成されたインスタンスだけです.

クラス作成時にクラスの変更を禁止するにはlock_class=TRUEが使えます.クラスのロックとアンロックは以下のようにすることでも可能です.

# ロックされたクラスの作成
Simple <- R6Class("Simple",
  public = list(
    x = 1,
    getx = function() self$x
  ),
  lock_class = TRUE
)

# これはエラーになる
# Simple$set("public", "y", 2)

# クラスのアンロック
Simple$unlock()

# 今度はきちんと動く
Simple$set("public", "y", 2)

# 再びクラスをロックする
Simple$lock()

オブジェクトのクローン

R6オブジェクトは,オブジェクトのコピーを作るためのclone()という名前のメソッドをデフォルトで持っています.

Simple <- R6Class("Simple",
  public = list(
    x = 1,
    getx = function() self$x
  )
)

s <- Simple$new()

# クローンを作成
s1 <- s$clone()

# クローンの方を変更
s1$x <- 2
s1$getx()
#> [1] 2

# 元のオブジェクトにはクローンの変更による影響はない
s$getx()
#> [1] 1

clone()メソッドが追加されるのがいやであれは,クラス作成時にcloneable=FALSEにできます.clone()メソッドを持つR6オブジェクトがロードされているとclone()関数がメモリを44.1 kB消費しますが,さらにオブジェクトを追加した場合,オブジェクト1つあたりのclone()メソッドが消費するメモリはわずかなものです(112バイト).

ディープクローン

参照セマンティクスのオブジェクト(環境,R6オブジェクト,参照クラスオブジェクト)を持つフィールドがある場合,コピーされたオブジェクトは,元のオブジェクトのフィールドと同一のオブジェクトへの参照を持つことになります.これは望ましいこともありますが,そうでない場合が多いです.

例として,他のR6オブジェクトsを含むようなオブジェクトc1を作成し,これをクローンします.元のオブジェクトとクローンのsフィールドは両方が同一のオブジェクトを参照しており,一方のフィールドを変更するともう一方
にも変更が反映されます.

Simple <- R6Class("Simple", public = list(x = 1))

Cloneable <- R6Class("Cloneable",
  public = list(
    s = NULL,
    initialize = function() self$s <- Simple$new()
  )
)

c1 <- Cloneable$new()
c2 <- c1$clone()

# c1の`s`フィールドを変更
c1$s$x <- 2

# c2の`s`は同じオブジェクトなので変更が反映される
c2$s$x
#> [1] 2

クローンがsコピーを受け取れるようにするには,deep=TRUEオプションが使えます.

c3 <- c1$clone(deep = TRUE)

# Change c1's `s` field
c1$s$x <- 3

# c2's `s` is different
c3$s$x
#> [1] 2

clone(deep=TRUE)のデフォルトのふるまいは,R6オブジェクトのフィールドをコピーするようになっています.ですが,環境や参照クラスオブジェクト,その他のデータ構造で参照セマンティクスのオブジェクトを含むもののフィールドはコピーしません(たとえばR6オブジェクトを含むリスト).

R6オブジェクトがこういった種類のオブジェクトを含んでおり,それらをディープクローンしたい場合には,自分でそのための関数をプライベートメソッドのdeep_clone()で提供しなければなりません.以下は,環境からなる2つのフィールドabを持ち,さらにその2つのフィールドが値xを持つようなR6オブジェクトの例です.このオブジェクトには,通常の(参照でない)値を持つフィールドvと,プライベートなdeep_clone()メソッドもあります.

deep_clone()メソッドは各フィールドに対して一度ずつ呼び出されます.フィールドの名前と値がメソッドに渡され,その戻り値がクローンで使われます.

CloneEnv <- R6Class("CloneEnv",
  public = list(
    a = NULL,
    b = NULL,
    v = 1,
    initialize = function() {
      self$a <- new.env(parent = emptyenv())
      self$b <- new.env(parent = emptyenv())
      self$a$x <- 1
      self$b$x <- 1
    }
  ),
  private = list(
    deep_clone = function(name, value) {
      # x$clone(deep=TRUE) が呼び出されると,deep_clone() が
      # 各フィールドに対して一度ずつ,name と value を引数にして実行される
      if (name == "a") {
        # `a` は環境なので,こうやってコピーするのがてっとり早い
        list2env(as.list.environment(value, all.names = TRUE),
                 parent = emptyenv())
      } else {
        # 他のフィールドに対しては単に値を返すようにする
        value
      }
    }
  )
)

c1 <- CloneEnv$new()
c2 <- c1$clone(deep = TRUE)

c1$clone(deep=TRUE)が呼び出されると,deep_clone()メソッドがc1の各フィールドに対して呼び出され,フィールドの名前と値が引数として渡されます.この例のバージョンでは,環境aはコピーされるが環境bはコピーされず,vもコピーされないようになっています(といってもvは参照オブジェクトではないので関係ありませんが).

# c1$aを変更してもc2$aは別のオブジェクトなので影響はない
c1$a$x <- 2
c2$a$x
#> [1] 1

# c1$bを変更するとc2$bも同じオブジェクトなので影響がある
c1$b$x <- 3
c2$b$x
#> [1] 3

# c1$vを変更しても参照オブジェクトではないのでc2$vに影響はない
c1$v <- 4
c2$v
#> [1] 1

この例のdeep_clone()メソッドでは処理内容を決めるために各フィールドの名前をチェックしていますが,inherits(value, "R6")is.environment()などを使って,値の方をチェックすることもできるでしょう.

R6オブジェクトの表示

R6オブジェクトにはデフォルトのprint()メソッドがあり,オブジェクトのすべてのメンバの一覧を表示するようになっています.クラスでprint()メソッドを定義してやれば,デフォルトのメソッドをオーバーライドできます.

PrettyCountingQueue <- R6Class("PrettyCountingQueue",
  inherit = CountingQueue,
  public = list(
    print = function(...) {
      cat("<PrettyCountingQueue> of ", self$get_total(), " elements\n", sep = "")
      invisible(self)
    }
  )
)
pq <- PrettyCountingQueue$new(1, 2, "foobar")
pq
#> <PrettyCountingQueue> of 3 elements

ファイナライザ

オブジェクトがガベージコレクトされた際に関数を実行するのが有用な場合があります.たとえば,ファイルやデータベースへの接続が閉じるように保証したいことがあるかもしれません.こういった用途のためにfinalize()メソッドを定義することができます.finalize()メソッドはオブジェクトがガベージコレクトされた時に引数なしで呼び出されます.

A <- R6Class("A", public = list(
  finalize = function() {
    print("Finalizer has been called!")
  }
))

# オブジェクトのインスタンス化
obj <- A$new()

# オブジェクトに対して存在している唯一の参照を削除し,
# 強制的にガベージコレクションを実行する
# (通常,ガベージコレクションは自動的に時々実行される)
rm(obj); gc()
#> [1] "Finalizer has been called!"
#>          used (Mb) gc trigger (Mb) max used (Mb)
#> Ncells 405575 21.7     750400 40.1   592000 31.7
#> Vcells 628036  4.8    1308461 10.0   913159  7.0

ファイナライザはreg.finalizer()関数を用いて実装されており,Rセッションが終了した時にもファイナライザが呼び出されるよう,onexit=TRUEに設定されています.これはデータベース接続などの場合に有用です.

まとめ

R6クラスは他のオブジェクト指向プログラミング言語で一般的な機能を提供します.Rに組み込みの参照クラスと似ていますが,より単純・軽量・高速で,パッケージをまたいだ継承が可能です.


訳注:翻訳の際,コードは以下の環境で実行した.

devtools::session_info()
#> Session info --------------------------------------------------------------
#>  setting  value                       
#>  version  R version 3.3.2 (2016-10-31)
#>  system   x86_64, mingw32             
#>  ui       RTerm                       
#>  language (EN)                        
#>  collate  Japanese_Japan.932          
#>  tz       Asia/Tokyo                  
#>  date     2017-01-15
#> Packages ------------------------------------------------------------------
#>  package   * version date       source        
#>  backports   1.0.4   2016-10-24 CRAN (R 3.3.2)
#>  codetools   0.2-15  2016-10-05 CRAN (R 3.3.2)
#>  devtools    1.12.0  2016-06-24 CRAN (R 3.3.2)
#>  digest      0.6.10  2016-08-02 CRAN (R 3.3.2)
#>  evaluate    0.10    2016-10-11 CRAN (R 3.3.2)
#>  htmltools   0.3.5   2016-03-21 CRAN (R 3.3.1)
#>  knitr       1.15.1  2016-11-22 CRAN (R 3.3.2)
#>  magrittr    1.5     2014-11-22 CRAN (R 3.3.1)
#>  memoise     1.0.0   2016-01-29 CRAN (R 3.3.1)
#>  pryr      * 0.1.2   2015-06-20 CRAN (R 3.3.2)
#>  R6        * 2.2.0   2016-10-05 CRAN (R 3.3.2)
#>  Rcpp        0.12.8  2016-11-17 CRAN (R 3.3.2)
#>  rmarkdown   1.2     2016-11-21 CRAN (R 3.3.2)
#>  rprojroot   1.1     2016-10-29 CRAN (R 3.3.2)
#>  stringi     1.1.2   2016-10-01 CRAN (R 3.3.2)
#>  stringr     1.1.0   2016-08-19 CRAN (R 3.3.2)
#>  withr       1.0.2   2016-06-20 CRAN (R 3.3.1)
#>  yaml        2.1.14  2016-11-12 CRAN (R 3.3.2)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
9
Help us understand the problem. What are the problem?