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(lm)
<environment: namespace:stats>
と言う風にします。package:stats
の環境にあることが分かります。
package:stats
の環境を取得することもできます。
as.environment("package:stats")
<environment: package:stats>
attr(,"name")
[1] "package:stats"
attr(,"path")
[1] "C:/Program Files/R/R-4.2.2patched/library/stats"
2.3 ロードしたパッケージのオブジェクトが検索される理由
パッケージの環境にある関数(e.g. lm
)が呼び出せるのは、サーチパスにpackage:stats
の環境が載っているから、もしくは.GlobalEnv
の親などの先祖にpackage:stats
が含まれているからです。
2.3.1 サーチパス
パッケージを呼び出すと、サーチパスにパッケージの環境が追加されます。readr
パッケージがインストールされているのであれば、
library(readr)
search()
[1] ".GlobalEnv" "package:readr" "package:rmarkdown"
[4] "tools:vscode" "package:stats" "package:graphics"
[7] "package:grDevices" "package:utils" "package:datasets"
[10] "package:methods" "Autoloads" "package:base"
とpackage:readr
を確認することができます。
アンロードすると、消えます。
detach("package:readr", unload=TRUE)
search()
[1] ".GlobalEnv" "package:rmarkdown" "tools:vscode"
[4] "package:stats" "package:graphics" "package:grDevices"
[7] "package:utils" "package:datasets" "package:methods"
[10] "Autoloads" "package:base"
なおpackage:stats
はパッケージの呼び出し側が参照する環境(package
environment)で、パッケージ内部のコードが参照する環境(namespace
environment)とは別になっています。
2.3.2
.GlobalEnv
の先祖
.GlobalEnv
からparent.env
を用いて親を辿っていくと、package:stats
を経由してbase
に辿り着きます。
<- .GlobalEnv
e while(!identical(e, emptyenv())){
print(environmentName(e))
<- parent.env(e)
e }
[1] "R_GlobalEnv"
[1] "package:rmarkdown"
[1] "tools:vscode"
[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
がそれになります。
parent.env(as.environment("package:base"))
<environment: R_EmptyEnv>
2.3.3 サーチパスにないパッケージを参照する(pkg::name)
library
関数でパッケージをロードせずにサーチパスに載っていなくても、例えばreadr
パッケージがインストールされていれば、readr::read_csv
と言うようにパッケージ内の関数にアクセスできます。::
はbaseパッケージで定義された演算子で、必要ならば::
の左側のパッケージをロードして環境をつくった上で、::
の右側の関数を探してくれるようです。
3 環境をつくってみる
回帰分析をして観測値を予測とともにプロットする作業においては意識する必要はないのですが、環境がどういうモノか確認していきましょう。 明示的につくる必要は多くないと思いますが、つくることもできます。
<- new.env() myenv
環境の中に変数を定義したり、環境の中の変数を更新できます。
$a <- 1
myenvwith(myenv, { a <- 2 }) # 第1引数の環境を指定すると、現在の環境をその環境に切り替えることができる
$a myenv
[1] 2
同じ名前のオブジェクトでも、異なる環境にあれば共存できます。
3.1 子環境をつくる
子環境をつくることもできます。
with(myenv, {
<- new.env()
cenv })
子環境の中では、親環境の変数をシームレスに参照することができます。
with(myenv$cenv, a)
[1] 2
3.2 作った環境をサーチパスに載せる
往々にしてbad practiceになるので避けるべきですが、
attach(myenv)
parent.env(myenv)
<environment: 0x000000001441e308>
でサーチパスに載りますし、detach
で外せます。
3.3 環境とリストの違い
ほとんどリストのように扱っていますが、リストとは異なり現在の環境の変数の値は破壊的に更新されます。リストで同様に処理を行なっても、変数a
の元のリストの値は更新されません。
<- list()
lst $a <- 1
lstwith(lst, { a <- 2 })
$a lst
[1] 1
リストを更新したい場合は、lst <- with(lst, { a <- 2 })
と、更新されたオブジェクトで明示的に上書きする必要があります。
また、環境は比較演算子を用いることができません。identical
関数を用います。
identical(myenv, .GlobalEnv)
[1] FALSE
4 関数呼び出し時の環境
関数呼び出しの度に実行時環境(executable environment)が自動的に作成され、それに切り替えられます。これによって、ローカルスコープが実現します。
<- function() print(environment()) fun
上の現在の環境を表示する関数を、何度か呼び出してみましょう。
fun()
<environment: 0x000000001ceb1cc0>
fun()
<environment: 0x000000001cf13fa0>
16進数の番号が変わっていくところから、関数呼び出しの度に新たな環境がつくられていることが分かります。
4.1 この子は誰の子?
現在の環境はenvironment
関数、環境の親はpanrent.env
関数で取得することができます。
<- function(){
fun1 <- function(t, d) print(sprintf("%s in depth %d:", t, d))
showMsg showMsg("parent", 1)
print(parent.env(environment()))
showMsg("current", 1)
print(environment())
<- function(){
fun2 showMsg("parent", 2)
print(parent.env(environment()))
showMsg("current", 2)
print(environment())
}fun2()
}fun1()
[1] "parent in depth 1:"
<environment: 0x000000001441e308>
[1] "current in depth 1:"
<environment: 0x000000001d27df50>
[1] "parent in depth 2:"
<environment: 0x000000001d27df50>
[1] "current in depth 2:"
<environment: 0x000000001d2877c0>
fun2の親環境がfun1の環境であることが分かります。なお、関数オブジェクトがある環境が親になるので、再帰するとずっと親環境は同じになります。
4.2 呼び出し時ではなく定義時に親は定まる
環境の親子関係を見たときのコードを再帰に書き替えてみます。
<- function(depth=0){
fun <- function(t, d) print(sprintf("%s in depth %d:", t, d))
showMsg 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: 0x000000001441e308>
[1] "current in depth 1:"
<environment: 0x000000001d55efe0>
[1] "parent in depth 2:"
<environment: 0x000000001441e308>
[1] "current in depth 2:"
<environment: 0x000000001d568670>
深さ1
も2
で親環境が同じになりましたね。
関数の親環境は、関数オブジェクトが定義されている環境になります。fun2
はfun1
の中で定義されていましたが、fun
を再帰しても定義されている場所は変わりません。
4.3 保存先ではなく定義したときの環境が親
環境の親子関係を見たときのコードのfun1
内の代入演算子を<-
から<<-
に書き替えます。これはsuper
assignmentと言って、親環境の変数に代入するものです。親環境に該当する変数がない場合は、.GlobalEnv
に作成されます。
<- NULL
fun2 <- function(){
fun1 <<- function(t, d) print(sprintf("%s in depth %d:", t, d))
showMsg showMsg("parent", 1)
print(parent.env(environment()))
showMsg("current", 1)
print(environment())
<<- function(){
fun2 showMsg("parent", 2)
print(parent.env(environment()))
showMsg("current", 2)
print(environment())
}fun2()
}fun1()
[1] "parent in depth 1:"
<environment: 0x000000001441e308>
[1] "current in depth 1:"
<environment: 0x000000001da5d160>
[1] "parent in depth 2:"
<environment: 0x000000001da5d160>
[1] "current in depth 2:"
<environment: 0x000000001da64988>
fun2()
[1] "parent in depth 2:"
<environment: 0x000000001da5d160>
[1] "current in depth 2:"
<environment: 0x000000001dac0520>
関数オブジェクトfun1
とfun2
は同じ環境に作られるわけですが、定義した環境が親環境になるため、fun2
の親環境がfun1
の現在の環境になっています。
4.4 呼び出し元が消えても、呼び出し元の環境は残る
トリッキーなのですが、削除した関数の環境にある変数x
を更新することができます。
<- function(){
fun1 <- 7
x <<- function(){
fun2 print(parent.env(environment()))
<<- x + 1
x
x
}
x
}if(exists("x")) rm(x) # 変数xは消しておく
fun1() # 関数fun2を表示,自分の環境のxを表示
[1] 7
fun2() # 関数fun1の環境にあるxを更新・表示
<environment: 0x000000001de459b0>
[1] 8
rm(fun1) # 関数fun1を消す
fun2() # 動く
<environment: 0x000000001de459b0>
[1] 9
5 まとめ
関数内でグローバル変数は参照できるが、代入演算だけでは更新はできないと言う基本的な挙動を理解するだけでだいたい間に合うわけですが、ローカル変数以外を参照する再帰関数を書き出す頃に、変数スコープの詳細な振る舞いを把握する必要が出てきます。変数を間接参照するget
などの関数は、環境を指定する引数envir
を持てることに気づけば、これだけならばlexical
scopingとだけ理解してもよいのですが、変数を間接参照するget
などの関数は環境を指定する引数envir
を持てるわけで、環境を把握しておけばより柔軟なコードを書けるようになれます。