Rの名前空間とスコーピング
プログラミング言語としてのRの良いところの一つは、名前空間の扱い方がシンプルで柔軟で一貫性があるところです。ぽちぽちと統計解析に使うには大雑把な把握で無問題だと思いますが、自作パッケージなどを作るようになる頃に重要になってくる知識なので、実例で理解していきましょう。
1 名前空間
プログラミングでは多数の様々なオブジェクトや宣言の識別子、つまり名前を扱うことになるのですが、ヒトに分かりやすい名前を無計画に命名していくとすぐに重複することになります。これはバグの温床になるため、名前が通用する範囲(スコープ)を限定することにより識別子の重複を避ける仕組みである名前空間が、ほぼ全てのプログラミング言語で用意されています。
2 環境を知ろう
Rの場合は、環境(environment)を名前空間に対応させる仕組みを用いて、スコープを管理しています。
- 環境はvectorやlistといったオブジェクトを登録するメモリー空間の変数で、Rのコードが実行されるときに1つ紐付け(現在の環境; currrent environment)
- 代入演算子
<-で定義されたオブジェクトは現在の環境に登録され、変数を参照するときは現在の環境に登録されているオブジェクトが優先的に参照されます - 環境には親があり、現在の環境に参照した名前の変数がないときは、親の環境、親の親の環境・・・と言う風に先祖を辿って検索します
- 先祖の環境にも参照した名前の変数がないときは、サーチパスにある環境も検索します
Rでは環境を理解することが、変数スコープを理解することになります。
2.1 グローバル領域もひとつの環境
グローバル領域もひとつの環境で、変数.GlobalEnvやglobalenv()関数で参照できます。
2.2 パッケージはそれぞれ別の環境にある
標準で使える関数lmやconfintはls()をしてもリストされません。これは現在の環境(e.g. .GlobalEnv)とは異なる環境にあるからです。調べるためには、
<environment: namespace:stats>
と言う風にします。package:statsの環境にあることが分かります。
package:statsの環境を取得することもできます。
<environment: package:stats>
attr(,"name")
[1] "package:stats"
attr(,"path")
[1] "/usr/lib/R/library/stats"
2.3 ロードしたパッケージのオブジェクトが検索される理由
パッケージの環境にある関数(e.g. lm)が呼び出せるのは、サーチパスにpackage:statsの環境が載っているから、もしくは.GlobalEnvの親などの先祖にpackage:statsが含まれているからです。
2.3.1 サーチパス
パッケージを呼び出すと、サーチパスにパッケージの環境が追加されます。readrパッケージがインストールされているのであれば、
[1] ".GlobalEnv" "package:readr" "package:rmarkdown"
[4] "package:stats" "package:graphics" "package:grDevices"
[7] "package:utils" "package:datasets" "package:methods"
[10] "Autoloads" "package:base"
とpackage:readrを確認することができます。
アンロードすると、消えます。
[1] ".GlobalEnv" "package:rmarkdown" "package:stats"
[4] "package:graphics" "package:grDevices" "package:utils"
[7] "package:datasets" "package:methods" "Autoloads"
[10] "package:base"
なおpackage:statsはパッケージの呼び出し側が参照する環境(package
environment)で、パッケージ内部のコードが参照する環境(namespace
environment)とは別になっています。
2.3.2
.GlobalEnvの先祖
.GlobalEnvからparent.envを用いて親を辿っていくと、package:statsを経由してbaseに辿り着きます。
[1] "R_GlobalEnv"
[1] "package:rmarkdown"
[1] "package:stats"
[1] "package:graphics"
[1] "package:grDevices"
[1] "package:utils"
[1] "package:datasets"
[1] "package:methods"
[1] "Autoloads"
[1] "base"
またbaseにも親があって、R_EmptyEnvがそれになります。
<environment: R_EmptyEnv>
2.3.3 サーチパスにないパッケージを参照する(pkg::name)
library関数でパッケージをロードせずにサーチパスに載っていなくても、例えばreadrパッケージがインストールされていれば、readr::read_csvと言うようにパッケージ内の関数にアクセスできます。::はbaseパッケージで定義された演算子で、必要ならば::の左側のパッケージをロードして環境をつくった上で、::の右側の関数を探してくれるようです。
3 環境をつくってみる
回帰分析をして観測値を予測とともにプロットする作業においては意識する必要はないのですが、環境がどういうモノか確認していきましょう。 明示的につくる必要は多くないと思いますが、つくることもできます。
環境の中に変数を定義したり、環境の中の変数を更新できます。
[1] 2
同じ名前のオブジェクトでも、異なる環境にあれば共存できます。
3.1 子環境をつくる
子環境をつくることもできます。
子環境の中では、親環境の変数をシームレスに参照することができます。
[1] 2
3.2 作った環境をサーチパスに載せる
往々にしてbad practiceになるので避けるべきですが、
<environment: 0x5b48fafd1860>
でサーチパスに載りますし、detachで外せます。
3.3 環境とリストの違い
ほとんどリストのように扱っていますが、リストとは異なり現在の環境の変数の値は破壊的に更新されます。リストで同様に処理を行なっても、変数aの元のリストの値は更新されません。
[1] 1
リストを更新したい場合は、lst <- with(lst, { a <- 2 })と、更新されたオブジェクトで明示的に上書きする必要があります。
また、環境は比較演算子を用いることができません。identical関数を用います。
[1] FALSE
4 関数呼び出し時の環境
関数呼び出しの度に実行時環境(executable environment)が自動的に作成され、それに切り替えられます。これによって、ローカルスコープが実現します。
上の現在の環境を表示する関数を、何度か呼び出してみましょう。
<environment: 0x5b48fcaf2078>
<environment: 0x5b48fcb75968>
16進数の番号が変わっていくところから、関数呼び出しの度に新たな環境がつくられていることが分かります。
4.1 この子は誰の子?
現在の環境はenvironment関数、環境の親はpanrent.env関数で取得することができます。
fun1 <- function(){
showMsg <- function(t, d) print(sprintf("%s in depth %d:", t, d))
showMsg("parent", 1)
print(parent.env(environment()))
showMsg("current", 1)
print(environment())
fun2 <- function(){
showMsg("parent", 2)
print(parent.env(environment()))
showMsg("current", 2)
print(environment())
}
fun2()
}
fun1()[1] "parent in depth 1:"
<environment: 0x5b48fafd1860>
[1] "current in depth 1:"
<environment: 0x5b48fc7d7e48>
[1] "parent in depth 2:"
<environment: 0x5b48fc7d7e48>
[1] "current in depth 2:"
<environment: 0x5b48fc7e1208>
fun2の親環境がfun1の環境であることが分かります。なお、関数オブジェクトがある環境が親になるので、再帰するとずっと親環境は同じになります。
4.2 呼び出し時ではなく定義時に親は定まる
環境の親子関係を見たときのコードを再帰に書き替えてみます。
fun <- function(depth=0){
showMsg <- function(t, d) print(sprintf("%s in depth %d:", t, d))
showMsg("parent", depth)
print(parent.env(environment()))
showMsg("current", depth)
print(environment())
if(depth<2) fun(depth + 1)
}
fun(1)[1] "parent in depth 1:"
<environment: 0x5b48fafd1860>
[1] "current in depth 1:"
<environment: 0x5b48fca2bc50>
[1] "parent in depth 2:"
<environment: 0x5b48fafd1860>
[1] "current in depth 2:"
<environment: 0x5b48fca39358>
深さ1も2で親環境が同じになりましたね。
関数の親環境は、関数オブジェクトが定義されている環境になります。fun2はfun1の中で定義されていましたが、funを再帰しても定義されている場所は変わりません。
4.3 保存先ではなく定義したときの環境が親
環境の親子関係を見たときのコードのfun1内の代入演算子を<-から<<-に書き替えます。これはsuper
assignmentと言って、親環境の変数に代入するものです。親環境に該当する変数がない場合は、.GlobalEnvに作成されます。
fun2 <- NULL
fun1 <- function(){
showMsg <<- function(t, d) print(sprintf("%s in depth %d:", t, d))
showMsg("parent", 1)
print(parent.env(environment()))
showMsg("current", 1)
print(environment())
fun2 <<- function(){
showMsg("parent", 2)
print(parent.env(environment()))
showMsg("current", 2)
print(environment())
}
fun2()
}
fun1()[1] "parent in depth 1:"
<environment: 0x5b48fafd1860>
[1] "current in depth 1:"
<environment: 0x5b48f9698bb8>
[1] "parent in depth 2:"
<environment: 0x5b48f9698bb8>
[1] "current in depth 2:"
<environment: 0x5b48f9655e30>
[1] "parent in depth 2:"
<environment: 0x5b48f9698bb8>
[1] "current in depth 2:"
<environment: 0x5b48f9532318>
関数オブジェクトfun1とfun2は同じ環境に作られるわけですが、定義した環境が親環境になるため、fun2の親環境がfun1の現在の環境になっています。
4.4 呼び出し元が消えても、呼び出し元の環境は残る
トリッキーなのですが、削除した関数の環境にある変数xを更新することができます。
fun1 <- function(){
x <- 7
fun2 <<- function(){
print(parent.env(environment()))
x <<- x + 1
x
}
x
}
if(exists("x")) rm(x) # 変数xは消しておく
fun1() # 関数fun2を表示,自分の環境のxを表示[1] 7
<environment: 0x5b48fa47dbe8>
[1] 8
<environment: 0x5b48fa47dbe8>
[1] 9
5 まとめ
関数内でグローバル変数は参照できるが、代入演算だけでは更新はできないと言う基本的な挙動を理解するだけでだいたい間に合うわけですが、ローカル変数以外を参照する再帰関数を書き出す頃に、変数スコープの詳細な振る舞いを把握する必要が出てきます。変数を間接参照するgetなどの関数は、環境を指定する引数envirを持てることに気づけば、これだけならばlexical
scopingとだけ理解してもよいのですが、変数を間接参照するgetなどの関数は環境を指定する引数envirを持てるわけで、環境を把握しておけばより柔軟なコードを書けるようになれます。
