パッケージの関数と普通の関数の違い

Namespaceは大事なしくみ
R
公開

2023年9月23日

Rはご存知のとおりパッケージが充実していて、様々な機能をすぐにインストールして使うことができるという利点があります。このパッケージは、関数を汎用的に作っておいてそれを様々な用途に使い回すという観点からも、とてもよくできた仕組みになっています。実は、普通に定義した関数にはない仕組みがパッケージには備わっています。

データ型による挙動の違い

簡単な例として、data frameの中から指定した列番号だけ最初の6行を表示するcolhead()という関数を作ってみましょう。

colhead <- function(inp_df, colnum){
    head(inp_df[colnum])
}

これで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)

iris_dt <- as.data.table(iris)

これを先ほどの関数で見てみようとすると・・・

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のパッケージはソースが公開されているものも多いですが、ソースコードをコピーして関数を直接作ってしまうと、この名前空間の仕組みが活かされないため思わぬエラーが生じたりすることもあります。パッケージは正しくインストールして使うように心がけましょう。