2011年5月24日火曜日

関数型言語出身者が陥りやすいOOPの罠

いや、大した話じゃないんですけどね(笑)。そもそもプログラミングを「関数型言語」(例えばLispとか)から始めた人ってあんまいないでしょうし、言わば「何を当たり前の事を」と言うような話です。ただ、極私的なメモとして残しておきます。

さて、お題。ところで、ソフトバンクから「みんなのPython」って本が出ています。他の参考資料は良く知りませんが、手元にある本ではSQLiteと言う簡易データベースと繋げる例を紹介しているのはこの本だけです。

で、シンプルな例ではあるんですが、一方ここで紹介されてるコードと言うのは言わばバッチ処理で書かれてるんですね。関数でもない。単にファイルを起動してPythonインタプリタに読み込ませるだけの例になっている。要するに事実上、PythonインタプリタがUIフロントエンドになってるだけで、SQLを直接DBに打ち込むのと大して変わらないわけですよ。

今考えてる事はこういう事です。ちょっとしたGUIでのDBへのフロントエンドを作りたい。要するにSQLは(言語としてはメチャクチャですが・笑)定形処理を行うとしたら、自由度が高すぎるんです。決まりきった検索結果を返したいのなら関数で隠蔽するのが良い、ってのがパターンになる。

もっと言っちゃうと操作的にはGUIで充分なんです。つまり、ボタンをクリックして「決まりきった検索パターンで」結果を返してくれるに越した事がないんですね。

ただし。GUIが出てくるとこれはもうオブジェクト指向の範疇なんですね(笑)。好むと好まざるに関わらず。オブジェクト指向 = GUIの為の言語実装手段、って言っても良いくらいです。事実歴史的にはほぼそうです。GUIと言うインターフェースを人が考えついて以来、この二つは密接に関連してきたんです。

何故CLIによるプログラミングの学習でオブジェクト指向が理解しづらいか。これも当たり前ですよね(笑)。CLIである以上本質的にはOOPなんて要らないんですから。いたずらに「そう言うテクニックもあるんですよ」と言う余談めいた話にしかならない。当然「構造化されたスパゲティコードを書く方法」と揶揄される事になる。これは実はパラダイムが全く違うからなんですね。CLIでカッコ付けてOOPでコードなんて書く必要なんて無いんです。これはUIがシンプルであれば当然で、要するに"Keep It Simple, Stupid!!!"って言われるハメになるでしょう(笑)。

しかし、ステージが「GUIをやりたい」となると話は変わってくる。今急激にOOPを克服せなアカン事になってしまった(笑)。

まあこんな状態なんですけれども。

もちろんOOPの概要は知っています。理屈は分かる。継承、って現象も知ってる。ただ、CLIで数値計算用のスクリプトを書いてたりしてた以上OOPの出番なんてなかった(笑)。当たり前ですよね。使い捨て前提だし、そんな「ちょっと計算して確かめたい」ってだけのブツにOOPなんて導入したら、恐らく単純に関数使って書いたコードの1.5倍(当社比)は膨れ上がります(笑)。大盛イカ焼きそばならいいですがコードが1.5倍ってなあ困りますね。

特にPythonはself、self、selfなんで、「何でちょっと計算したいだけなのにselfまみれやねん」って成りかねない。いや、なる(笑)。

つまり、個人的には「テキストを写経した」事はあるけど、自ら率先してOOPを使ってコードを書こう、なんて考えた事もやった事もないんですよ。ただし、GUIのフロントエンドとやり取りする以上これはいきなり必須になってしまった。少なくとも必須と考えた方が良い。将来的な拡張とかも考えるとね。

ところで。いきなり話は変わりますが(笑)。PythonでSQLiteを操る場合、実はちょっとした制限があるんです。それは

SQL部分はトリプルクオート(例えば"""〜"""や'''〜''')で囲んだ方が良い

と言う部分です。まあ、制限っつーよりある種規約ですか。

実はPythonではトリプルクオートは通常、一種のDocStringの役目を果たします。コメント以上のコメントですね。要するに、何らかのモジュールを作成した場合、そのモジュールの「機能を」説明する為に付ける一種のコメントです。Emacsなんかでお馴染みでしょう。Emacsも基本、それらの情報をソース本体に記述されたDocStringから引っ張ってきます。DocString。いいのかしら用語これで(笑)。まあ、いいや、多分(笑)。

ちょっと簡単な例を上げます。例えば「みんなのPython」によると、DBから引っ張ってきたカーソルを使って、SQLを実行するには、Python + sqite3モジュールの組み合わせでこのように書くことが推奨されます。


もちろん「?」なんてのはSQLじゃあありませんよね。cursor.executeの引数はSQL文のテンプレートと後続のタプルとで成り立っていて、要するに?は後続のタプルが持ってる値を順番に埋め込んで行く為の「穴」の役割を果たしています。つまり、この引数が「評価」(笑・Lisp的解釈?)されて「完全なSQL文になってから」メソッドexcuteに渡される。とまあ、こう言う仕組みになってるんですよ。

これ便利でしょうか(笑)?最初見た時「うげえ」とか思ったんです。たしかに一見便利には見えます。しかし、実際はそのSQLのテンプレート「自体」を作るのが大変なんじゃなかろうか、と。つまり、この構造を成り立たせる為にはソースコード内の関数内に「SQL自体を」埋めこまなきゃならない。と言う事はデータベースを開いて、カーソル作って、何らかのSQLを実行して、データベースを閉じる、と。そこは抽象化出来ても限界があるんじゃなかろうか。要するに、このSQLの「雛形」を保持した関数自体は全く局所的な役割しか果たせず、考えうる(必要と思われる)SQLテンプレートを保持した関数を量産しないといけない。これはメチャクチャ醜いのでは、と恐怖したんですね。

そもそも関数を作るのは「汎化作用を狙って」です。一つの機能しか持たせられないのにどーしてfunctionなのか、と。

sqlite3モジュールのトリプルクオートには理由がある


さてこれは困った。何が困ったかと言うと、大体Lisp経験者はそもそもこんな「SQLを(基本的には)丸ごと埋め込む」ようなバカな作法は嫌いでしょ(笑)。絶対SQL自体をバラバラの要素に分解しちゃって、自在に組み立てられないか、って考える筈です。でしょ(笑)?コードを自動生成する・・・そうなんですよ。Lisp経験者なら恐らく絶対「マクロ」を使う局面なんです。そして汎化を狙う筈だ。

LispのマクロはLispでLispコードを自動生成する事です。このケースの場合は、Pythonを使ってSQLコードを自動生成出来ないか、って事ですよね。LispはLisp内で閉じてますし、PythonでPythonを書くのは難しいでしょうが、Python <-> SQLだったらまあ、理論的には可能でしょ。基本的なアイディアは同じです。要するにカッコではなく、文字列対象にして操作が可能なら、これは立派なSQLへの翻訳機です。

とっこっろっがねえ〜〜。ここで壁にぶち当たった。何故ならそもそもPythonではトリプルクオートで囲まれた文字列を操作する事が出来ない。これにビックリしたんですね。どう工夫しても結局フツーの文字列に落ち着いちゃう。

先にも書きましたけど、元々トリプルクオテーション自体はDocStringを狙ってるものです。つまり人間が手書きするのが前提なんですね。それを一種リファレンスとして扱う為の機能なんです。従ってPythonそのものでトリプルクオテーションを操作する事は考えられてません(っつーかその筈。ググったけどそんな方法は見つからなかった・笑)。

大変不自由なんですよ。そして何故かsqlite3モジュールはこいつを利用してSQL文を書く事を推奨している。っつー事は必然的にSQLの部分は手書きせんとアカンって事になる。うげえ、とか思ったんですね(笑)。何じゃこのシステムは、と。

んでまあ、あとで調べて分かったんですけど、これってどうやらセキュリティの為らしいんです。Pythonで操れるフツーの文字列でSQLを記述する方法を選んだ場合、要するに、クラッカーなんかが特定の悪意を持ったSQL文字列をPythonに喰わせて、データベースシステムを破壊しちゃう事も可能なんです。だから「敢えて」Pythonで操れないトリプルクオテーションをSQL埋め込みの雛形に選んだ。

言われてみればなるほどな、良く考えられてるな、とは思うんですが、当初は参りましたね(笑)。トリプルクオートでわざと囲んだSQLを別ファイルに用意してロードしてみても、ファイル開くとフツーの文字列になってたりして(笑)、

「てめえ、何じゃこりゃ、フザケてんのか!」

とか無意味に怒りまくってましたもの(笑)。

関数型言語出身者がハマる罠


例えば関数型言語、特にScheme経験すると、関数は何らかのデータをフィルタリングしてその結果を返すように書くべき、それが美しいプログラミングだ、と叩き込まれます。つまり、引数で入力 -> フィルタリングした結果を返す、って事ですよね。

要するに、通常の(Scheme的な)方法で考えると単に文字列を引数として与えて、内部でSELECTか何かと結合させて、その結果を返せば一丁上がり、ってなる筈なんですが、ところが出力をトリプルクオテーションの文字列として返す事が難しい。まあ、そんなワケでPythonでSQLを弄るのは最終段階にしておいて、取り敢えずじゃあ雛形のVIEWばっか作成すれば何とかなるのかしらん、と暫くRDBMSの基本機能の方に頼ってたわけです。

VIEWは必ずしも軽くない


何故そう思ったのか、と言うとPythonでフィルタリングを掛けられる状態まで、ある種SQLのVIEW上でデータを加工しておいたら、Python側のプログラム自体はシンプルで済むのではないか、と言う期待があった。

ところが、VIEWにVIEWをどんどん重ねたSQLを書いていくと無茶苦茶データ読み出しが遅くなっていく(笑)。最初、ポインタ参照程度で軽いのかしらん、とか思ってたんですが、さにあらず。一回の検索で5分とかかかる例とかも出てきて「ああ、こりゃやってられんわ」と言う結論になってしまった(笑)。

今回初めてRDBMSを触った、ってなくらいなんで、当然SQLも初めて。全くシステム的な理解がない。その中で、Twitterでやり取りしている間に「SQLの副問い合わせは重いですよ」と言う話が出てきた。

そうなんですよね。RDBMSのVIEWってのは基本的に副問い合わせを分離したモノに過ぎない。記述の簡便化なわけです。つまり、VIEWにVIEWを重ねたような入れ子構造にすると、メチャクチャ重くなって当たり前だったんですね(笑)。副問い合わせだらけですから(笑)。

つまり、部分的に必要なデータ、ってのを最終的にPythonから取り出す為に加工を始めた筈だったんですが、その「最終的に」加工する直前までの状態で、元ネタからのデータを「全部引っ張ってきたような」表を作りまくる、と。それが二重、三重にも重なっていくと、表の作成だけでメチャクチャ時間が取られて当たり前だったんです。規模的に一般にはどーなのか知りませんが、現時点対象の元ネタは6Gくらいのデータです。当然デカイんで途中経過で表の作成が何重にも重なれば大変な事になってしまいます。

漸くココに来て、そしてどーやらGUIの必要性が出てきたので

「しゃーねえ。色々合わせてOOPでやってみるか」

と腹をくくらざるを得なくなったわけです。

メソッドと関数は全然違う


んでしょーがないんで、書籍でPythonのオブジェクト指向の部分読みなおしてたりしてたんですが…。サッパリちんぷんかんぷんでありんす(爆)。

っつーか、いや、分かりやすいんですよ。分かりやすいんだけど、概論過ぎてピンと来ないんですよね(笑)。あまりにも簡単な例になり過ぎていて、って事があって、要するに実用的じゃない。

例えば、「Pythonチュートリアル」ではこんな例が上げられています。


これはPythonの作者、Guido van Rossum自らによる例です。なるほど、クラスがあって、継承して、メソッドがオーバーライドされてるのは分かる。分かるんですが……。

問題は、こんなprintで出力するだけの「クラス」なんて実際にはありえないでしょう(笑)。概念は分かっても実用性がない。要するに説明のレベルとしては基礎的過ぎるんです。

実際のPythonのOOPのコードはとにかくselfだらけ、です。あっちにselfアリ、こっちにselfアリ、です。そしてSchemeなんかの関数型言語出身者が一番困るのが、コードを読んでもどの変数がどの変数を参照してるのか、サッパリ分からない、って事です。

ただでさえ、メソッド内定義はそれこそオブジェクト名の嵐ですしね。こっちのself.spamは唐突に出てきて何か代入されてるるんだけど、あっちに書かれてるself.spamと同じモノなの?違うものなの?ああ、分からん!となる(笑)。

分かった範囲で結論から言うと、どうやら「メソッド」ってのは「関数」とはまるっきり違うモノらしい。正直言うと、どの本見ても、Python初心者への恐怖感を拭うためか、「メソッドは関数と同じようなモノ」と説明しています。しかし、これがScheme辺りからプログラミングをはじめた人間を混乱にたたき落とす(笑)。実は全く違う。表記法の簡便性としてはdefを使うのは妥当でしょうが、正直別の語を当ててほしいくらいです。

いや、別にPythonが悪いわけじゃないんですよ。ただ、Scheme辺りから始めちゃうと関数とレキシカルスコープはほぼ一体だって考えちゃうんですよね。しかしどうやらメソッドはレキシカルスコープを持たない。あるのはクラス範囲内限定の大域変数があるだけ、です。どうやらそうみたい。んでEmacs Lisp書いてる人は悩まないでしょう。Scheme上がりが混乱する、んですね(笑)。どうしても関数のスコープ基準で変数の参照を考えますから。

つまり、どうやらメソッド自体が変数を内包する変数だ、って考えた方が分かりやすい。関数だ、って捉えるから混乱する。変数だったら書き換えも生じる可能性もある。オーバーライドですよね(笑)。

継承してみる


そこまで噛み砕いてしまえば意外と簡単かも、って思いました。っつーか関数のフリした変数があるのならこれはもはや基本的には単なるバッチ処理です。Scheme上がりは如何に美しい関数を書くかに腐心する傾向があるんですが(笑)、もうこうなったら豪に入りては豪に従え、やけのヤンパチ、アジの開き直りでありんす(笑)。

実は本質的にはSQLを埋め込む関数の量が増大するのでは、と言う問題は全然解決してないんですが、継承の機構を使ってSQLを実行する雛形をスーパークラスにしちゃえば良いのでは、と考えました。

実は良く知らなかったんですが、DBのカーソルは基本的に、関数型言語で言う破壊的操作を伴うので、例えばcursor.exetute()でデータを取り出し変数x1に代入。その後、別のSQLを実行して変数x2に代入すると、変数x1の内容が変数x2と同じに書き換えられちゃうんですね。「んなバカな!」とか思ったんですが、どうやらそう言う仕様らしい(笑)。しかもカーソル自体はどーやらPythonの機能と言うよりはRDBMS側が提供している機能らしいんで、こりゃまたどーしよーもねえ。

ただ、@kaorin_linux氏の話によると、これはクラスを作って隠蔽してしまえば意図通りの動作になる模様で、そう言う意味でもDBにアクセスする一連の作業をクラス化する事には意味があると思えます。いや、本当はもっと単刀直入な手もある、って話ですが、いずれにせよ、GUI部分とやり取りするのを視野に入れると、どの道オブジェクトとして纏めておいた方がいいでしょう。

んで、初めて自発的にOOPでのコードを書いてみました。書いてみると実はなんて事なかった(笑)。以下がそれです。


参考にしたページもあったんですが、データを読み出すだけ、と言う意図もあり、全然簡単に出来ましたね。

クソ、くだらん(笑)。一体何を悩んでたんだ、俺は(爆)。

最初、初期化で何するか、ってのも悩んだんですけど、良く考えてみれば初期化に必要なのはDBに接続する事、です。わざわざ手動でメソッド呼び出して接続したくない。だからシンプルに__init__ではデータベースの接続を行ってカーソルを作成しています。

ポイントはSQL文を外部からどうやって呼び出すか。ここで先ほど見た通り、関数型言語な人ってのは関数の引数をアテにする。性質的にはさっきも書きましたが、オーバーライド自体が「破壊的変更」にしか見えないんで好ましい、とは思わないんですよ。まあ、他の人は知りませんが、僕はそうですね。

ただ、さっきも言った通り「豪に入りては豪に従え」なんで、意図的に「オーバーライドされる為だけが目的の」SQLtempと言うメソッドを作っています。ここがトリプルクオートのSQL文が入る場所です。従って中身は消されるのが前提です。もちろん空の文字列を喰わせておく、って手もありますが、ここではpassを使って、明示的に「何もしない」事にしました。

あとはお決まりですよね。SQL文を実行してデータを読み出し、そいつを返り値にする。もうちょっと言うと、どう言った穴開きのSQL文が来るか分からない。そこでshowAllRowsは第二引数に可変長引数を取っています。別の手もあるんですが、一応、ここの可変長引数は纏めてタプルになるので、最初に見たとおり、トリプルクオートのSQL文にそのまま渡すには大変都合が良いです。そのまま穴を埋めてくれる。んで、想定としては、ここの可変長引数はGUIのマウスのクリックから来るシグナルから取り出します。

で、sqlite3モジュールのfetchall()はデータベースのデータをタプルのリストとして返してくるので、そいつを返り値にしときゃ、まあ良いのではないでしょうか(笑)。

そうすれば、データベースのGUIフロントエンドで、例えば何かのデータが表示されてる。キーを表示している一部分をクリックすると、そこに関連した詳細なデータが出てくる、と言うカタチを目論む。その「詳細なデータ形式」と言うのはSQLとしてテンプレを用意しておけば良い、ってのが基本です。つまり、ここは上で作ったスーパークラスを継承した何かがあれば良いわけです。機能的にはもちろん、


  1. データベースに接続する

  2. SQLを実行する

  3. データを読み出す

なんですが、この1番と3番は既にスーパークラスで作られています。2の半分も実装済みです。繰り返しますが、要は「適切なSQLのテンプレートさえ」あれば良い。

従って書くべきコードは基本的に以下の通りで、形式的にはPythonですが、事実上殆ど単なるSQLです。


雛形作っておけばSQLで実行してみて、テストして、上手く行った後、ここにそれを貼りつければいい(笑)。簡単ですね(笑)。コピペでコードを仕上げる、ってのが如何にもOOP的です(笑)。Smalltalk的と言うか(笑)。

まあ、てなワケで悩んでた時間も長かったんですが、やってみると思ったよりOOPで書くのはツラくはない、ってのが曲がりなりにも分かりました。もっともC++/Javaな人たちから見るとあまりにも当たり前過ぎてつまんない話でしたね(笑)。

すんません(笑)。

0 件のコメント:

コメントを投稿