<- function(inp_df, colnum){
colhead head(inp_df[colnum])
}
Rはご存知のとおりパッケージが充実していて、様々な機能をすぐにインストールして使うことができるという利点があります。このパッケージは、関数を汎用的に作っておいてそれを様々な用途に使い回すという観点からも、とてもよくできた仕組みになっています。実は、普通に定義した関数にはない仕組みがパッケージには備わっています。
データ型による挙動の違い
簡単な例として、data frameの中から指定した列番号だけ最初の6行を表示するcolhead()
という関数を作ってみましょう。
これでiris
データの2列目と3列目の最初を表示してみます。
colhead(iris, 2:3)
Sepal.Width Petal.Length
1 3.5 1.4
2 3.0 1.4
3 3.2 1.3
4 3.1 1.5
5 3.6 1.4
6 3.9 1.7
想定どおり表示されました。しかしここで、分析の都合上data.table
パッケージを使いたくなったとします。iris
データをdata.table
型に変換して別の変数に保存します。
library(data.table)
<- as.data.table(iris) iris_dt
これを先ほどの関数で見てみようとすると・・・
colhead(iris_dt, 2:3)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<num> <num> <num> <num> <fctr>
1: 4.9 3.0 1.4 0.2 setosa
2: 4.7 3.2 1.3 0.2 setosa
想定と異なる結果になりました。なぜでしょうか?
data.table
パッケージを使い慣れている方ならばお分かりのことと思いますが、data.table
の一部を抜き出す際の引数は、必ず「行、列」の順番に指定するように設計されています。それを踏まえて見ると、違いが生じたのはcolhead()
関数の次の箇所です。
inp_df[column]
これはdata frameの場合はcolumn
に指定した列を抜き出す動作になりますが、data.table
の場合は指定した行を抜き出す動作になります。そのため、同じ関数なのに違う結果となってしまったのです。
関数が呼び出される順番
ここで、data.table
の動作が優先された理由を少し詳しく見てみましょう。関数の呼び出しには環境(environment)という仕組みが大きく関わっています。先ほど定義したcolhead()
関数のように、コマンドラインで直接作成したものは.GlobalEnv
という環境の中に置かれます。
environment(colhead)
<environment: R_GlobalEnv>
この環境には親(parent)があります。いま、.GlobalEnv
の親はdata.table
パッケージ環境になっています。
environmentName(parent.env(.GlobalEnv))
[1] "package:data.table"
そのまた親もあります。これらをたどっていく道をSearch Pathと言って、search()
関数で確認することができます。
search()
[1] ".GlobalEnv" "package:data.table" "package:stats"
[4] "package:graphics" "package:grDevices" "package:utils"
[7] "package:datasets" "package:methods" "Autoloads"
[10] "package:base"
通常は、最後にlibrary
で呼び出されたパッケージが.GlobalEnv
の親になり、その先はそれよりも前のパッケージ呼び出しをさかのぼる順に並んでいます。base
パッケージはSearch Pathの最後にあります。そのためbase
パッケージにあるdata frame用の関数よりも、data.table
パッケージの関数の方が優先して呼び出されることになります。
パッケージの名前空間
data.table
型の変数がそのための動作をするのは当然のことなのですが、変数の型によって動作が変わってしまうのは、汎用的な関数を作りたい場合に困ります。もともとdata.table
はdata frameを拡張した型ですので、data frameを対象に作られた処理でもうまく動作することが理想です。(data.table
パッケージのヘルプにも動作するはずだと書かれています)
実は関数をパッケージ化するとこれを実現できます。パッケージの関数はnamespaceという特別な環境に配置され、その直近の親環境はそのパッケージで必要としている他のパッケージとbase
パッケージのnamespaceです。そのまた親が.GlobalEnv
になり、その先は普通に定義した関数と同じように呼び出し先が検索されます。
例えばstats
パッケージのvar()
関数がある環境を見てみましょう。
environment(stats::var)
<environment: namespace:stats>
namespace:stats
というstats
パッケージ用の特別な環境にあることが分かります。この親環境はimports:stats
で、そのまた親はnamespace:base
です。
parent.env(environment(stats::var))
<environment: 0x00000269c5f1f418>
attr(,"name")
[1] "imports:stats"
parent.env(parent.env(environment(stats::var)))
<environment: namespace:base>
パッケージを開発したことがある方はご存知だと思いますが、パッケージ内の関数で別のパッケージを利用しているときは、その外部パッケージが何かを宣言しておく必要があります。そこで宣言しておいたものは上の例だとimports:stats
に入っていて、優先的に呼び出されます。例外的にbase
パッケージは宣言しなくてよくて、上の例のようにbase
パッケージのnamespaceは常にimportsの親になります。パッケージではこのように環境がセットされるため、パッケージ開発時に意図した動作が保証されることになります。最初のcolhead()
関数をもしパッケージにすれば、data.table
型の変数が渡されたとしても、data.table
パッケージ環境より前にnamespace:base
環境があるためdata frameとしての動作が保たれるでしょう。
パッケージはインストールして使おう
このようにパッケージには、汎用的な関数を共有するためのうまい仕組みが備わっています。Rのパッケージはソースが公開されているものも多いですが、ソースコードをコピーして関数を直接作ってしまうと、この名前空間の仕組みが活かされないため思わぬエラーが生じたりすることもあります。パッケージは正しくインストールして使うように心がけましょう。