data.frameを拡張するには 失敗編(S3でゆるふわ)
リモートにあるデータベースとシンクするdata.frame
をつくりたくて調べて分かったことのメモ。
(まだぜんぜん完成してないのでぼんやり書きます)
シンクするためには例えば、以下のような情報を保持する必要があります。
- スキーマ情報
- シンク先のURLまたはIP
- 最終更新した日付
- リビジョン番号
でも、挙動はdata.frame
と同じであってほしいです。
サブセットを作るのはsubset
とか使いたいし、ggplot
でプロットもしたいです。
そのためにいちいち自前でsubset
とかggplot
を実装しなければいけないとか言われると気が遠くなります。
ということで、外見上はdata.frame
として見えるようにする方法を調べました。
結論から言うと、同じことを考える人はいるもので、わりとあっさり方法は見つかりました。 というかぶっちゃけ今回書くことは↓のコピペみたいなもんです。
Extending Data Frame Class
EML/R/data.set.R at master · ropensci/EML
そもそもRのオブジェクトって?
ぜんぜん理解できていないので適当なこと書きますが、、
HadleyさんのAdvanced Rによると、Rには4種類のオブジェクトシステムがあります。
- 組み込み型
S3
→ 昔からあるやつ。クラスの定義はゆるふわ。S4
→ 新しいやつ。クラスの定義は厳密でめんどくさい。Reference class
→ オブジェクト指向言語っぽい感じ。
できればゆるふわですませたいものです。
data.frame
ってS3? S4?
Extending Data Frame Classでは、
data.frame
がS3かS4か調べています。
data.frame
のコンストラクタはS3のdata.frame()
と、S4のnew("data.frame")
があります。
data.frame()
でつくるとisS4
がFALSE
になって、一見S3っぽいのですが、
slotを持っていることからS4ではないか、という推測がされています。
> a <- data.frame(x=rnorm(10)) > isS4(a) [1] FALSE > slotNames(a) [1] ".Data" "names" "row.names" ".S3Class"
まあでもS3っぽく動いたりもするので、S3的なやり方とS4的なやり方、両方試してみます。
S3でやってみる
S3のクラス
S3のクラス定義、というのは特にありません。
structure()
でインスタンス生成時にclass
を設定するか、
class<-()
であとからクラスを設定するか、
いずれにせよクラス名はただの文字列です。
> a <- structure(list(), class="myclass1") > class(a) <- "myclass2"
これを毎回手でやると、めんどくさかったり、
クラス名を打ち間違えても特にエラーは出なかったりするので
実用上はコンストラクタを定義します。
> myclass <- function(...) {structure(list(...), class="myclass")} > a <- myclass(x=1) > class(a) [1] "myclass"
S3のメソッド
S3のメソッドは、method.myclass <- function(x) {...}
みたいな感じで宣言します。
例えば、myclass
のインスタンスA
について、中身を表示するためにprint(A)
を実行するとします。
print.myclass
が定義されていればそれが呼ばれ、なければprint.default
が使われます。
逆にいえば、myclass
のprint
結果を標準から変えたければprint.myclass
を定義します。
ちょっと脱線すると、baseパッケージの関数を上書きすることも可能です。
↓こんな感じ
> a <- data.frame(x=rnorm(10)) > class(a) [1] "data.frame" > a[1:3,] [1] -0.084118386 -0.003389443 0.535042087 > a[1,11] NULL > `[.data.frame` <- function(x, ...) 'Overwritten.' > a[1:3, ] [1] "Overwritten." > a[1,11] [1] "Overwritten." > rm(`[.data.frame`)
S3的にdata.frame
を拡張する
結論から言うと、
data.frame
を上書きすれば拡張はできるmydata.frame
みたいなのをdata.frame
と同じように挙動させるのは難しい
という感じで、やりたいことは満たせませんでした。
メタデータ的なものを追加するのはattr()
を使えばできます。
ここで設定した値はdata.frame
の影響を受けません。
> a <- data.frame(x=2) > a[1, ] [1] 2 > attr(a, "url") <- "https://notchained.hatenablog.com/" > attr(a, "url") [1] "https://notchained.hatenablog.com/" > a[1, ] [1] 2 > a$url NULL
問題は、クラスをmyclass
とかにしてしまうと、
当然ながらもうdata.frame
ではいられないことです。
> class(a) <- "myclass" > a[1, ] Error in a[1, ] : incorrect number of dimensions
まあ、data.frame
のメソッドを追加していけば、
やりたいことができるといえばできます。
たとえば、データをリモートからとってくるfetch.myclass
みたいなの関数を定義しようとしていたとすると、
fetch.data.frame <- function(x) {getURL(attr(x, "url"))}
みたいな感じのメソッドを定義してやれば、data.frame
でfetch
することはできます。
ただ、ここでやりたいことは「外見的にdata.frame
に見えるクラスをつくりたい」ということなので
「data.frame
を俺拡張する」というのは少し違いますよね。
副作用が怖そうだし。。
まとめ
ということで、S3でもいいところまでいきましたが、やっぱり限界がありました。
やっぱりそうですよね(ため息)。
長くなってしまったので、成功編(S4でふつうに)はまた次回!
追記:次回は http://notchained.hatenablog.com/entry/2014/03/02/013147 です。