Python Hack : 噛めば噛むほどおいしくなるクロージャの話
2009.11.06
最近 JavaScript を書く機会が増えているが、それに従って自分のコーディングスタイルが少しづつだが変化してきているのが分かる。もともと「コードの読みやすさ」や「実行効率」にとことんこだわるタイプだが、(JavaやC++になくて)JavaScriptやRubyにあるクロージャや無名関数が私のコーディングスタイルにとてもマッチしているからだと思う。
簡単な例を紹介しよう。Pythonで書かれた config.py というモジュール。config.yamlという設定ファイルを読み込んで Dictionary として返す config.get() という関数。普通に実装すると、以下のような感じになる。
import yaml
_config = None
def get():
global _config
if not _config:
data = open('config.yaml').read().decode('utf8')
_config = yaml.load(data)
return _config
config.yaml という設定ファイルがプログラムの実行中には変化しない点を利用して、一度読み込んだデータをグローバル変数 _config にキャッシュしておき、次からはそれを返す、というものだ。
よくあるコーディングスタイルだが、私はどうも好きになれない。まずは、_configというグローバル関数が気にいらない。モジュールの外からアクセスできないとは言え(Pythonの場合、"_"で始まる変数や関数はプライベート扱いになる)、このモジュール内の他のプログラムからは見えており、後々モジュールが大きくなって複数のプログラマーが関わって来た時に「同じモジュール内だし、get()関数を呼ぶより_configに直接アクセスした方が早いじゃん」という誘惑に負けてしまう人がいないとは限らない。
もう一つは、「if not _config:」という条件分岐を毎回毎回実行しなければならないこと。一つ一つは小さな話でも、何百回も何千回も呼ぶ必要がある場合、このオーバーヘッドも馬鹿にならない(人間だったらすぐに気がついてチェックを辞めるが、プログラムの場合は素直に何度でもチェックしてしまうから)。
クロージャと無名関数を使うと、こんな風に実装することができる。
import yaml
def _get_from_disk():
data = open('config.yaml').read().decode('utf8')
config = yaml.load(data) # クロージャ内に隠蔽・保持されるローカル変数
global get
get = lambda : config # 二回目からはconfigを返す無名関数を呼ぶ様に変更
return get()get = _get_from_disk # 初回のみローダーを実行
わずかな違いだが、私にはこのスタイルの方がずっとすっきりくる。キャッシュした値を覚えている config はローカル変数なのでモジュール外だけでなくモジュール内の他の部分からも完璧に隠蔽されているし、二回目からは "lambda: config" (JavaScriptだと "function() { return config; }" という無名関数に相当)を実行するだけなので効率が良い。
小さな話と言えば小さな話だが、こんな風に「メンテナンスのしやすさ」や「実行効率」にこだわりつつ作ったプログラムとそうでなプログラムでは、出来上がった時に大きな違いが出る。
唯一の難点は、「クロージャがなんであるか」が直感的に理解しにくいために、初心者にとっては、決して「読みやすいプログラム」にはなっていないこと(C++やJavaにしか触れていない人たちには上のサンプルは理解しがたい)。あまりクロージャを駆使すると、一部の人しか理解できない黒魔術になってしまうので、そこは気をつけなければいけない。
恥ずかしながら、こういうコードの書き方があるのを初めて知りました。
シングルトンのクラスがこんなにスマートに書けて感動……。
目から鱗です。
class Singleton(object):
def __new__(cls, *args, **kwargs):
instance = object.__new__(cls)
cls.__new__ = classmethod(lambda cls, *args, **kwargs: instance)
return cls.__new__(cls, *args, **kwargs)
Posted by: log | 2009.11.06 at 17:52
Pythonはよく知りませんが上のような書き方ができるためにはローカル変数が,メソッドの終了後も維持されている必要があるわけで,少なくともRubyやJavaScriptでは難しいのではないかと思います。
RailsではActiveSupport::Memoizableで,上記の機能を実装しており,個人的にはそちらの方が汎用的でスマートに感じます。
Posted by: Andy | 2009.11.06 at 21:57
>ローカル変数が,メソッドの終了後も維持されている必要があるわけで
それこそががまさにクロージャと呼ばれるもので、Pythonだけでなく、RubyやJavaScriptもちゃんとサポートしています。ぜひとも試してください。
Posted by: Satoshi | 2009.11.06 at 22:35
なるほど。まさにクロージャがなんだかよく分かってなかった人間のようです。
こうやって対比させてみると、なるほどなぁと思えます。
Posted by: yone | 2009.11.07 at 04:06
こういうクロージャの使い方はしたことがなかったのですが,大丈夫なのですね。感心しました。
ただ,上にあげたActiveSupport::Memoizableだったら,同じことを
def get
data = open('config.yaml').read().decode('utf8')
yaml.load(data)
end
memoize :get
だけで書けてしまうわけで,やはりそちらの方がスマートに見えます。ちなみにMemoizableの実装ではクロージャは使っていません(初回のオーバヘッドはちょっと大きそうです)。
Posted by: Andy | 2009.11.07 at 09:19
フレームワーク前提なら django などでも memoize デコレータでシンプルにできるよ。
pure Ruby|Python と特定のフレームワークの話は区別すべきだね。
Posted by: こむそう | 2009.11.07 at 12:43
これだと
from config import get
とした時に、毎回'config.yaml'と読み込むことになってしまいませんか?
Posted by: doloop | 2009.11.07 at 22:10
memoizeを独自に実装してもせいぜい10数行のプログラムです。特定フレームワークだからどうのということではなく,satoshiさんの例のように,こういうのが出てくるたびにクロージャを使ったコードを書くのよりも,memoizeのような仕組みを実装する方がいいのではないかと思いました。
Posted by: Andy | 2009.11.08 at 20:16
こんにちは
いつも楽しんで拝見しています。クロージャの件ですが、確かにクロージャは分かりずらい反面、非常にやくにたつと思います。ただ「一度しか呼び込まなくて良い設定ファイル」をキャッシュするためにわざわざクロージャを使う必要があるのか少し理解に苦しんでいます。読み込んだ後、変化しないのであれば定数に保持すればいいだけですし、もしget関数を利用したいのであればクラスのインスタンス変数に保持すれば良いのではないのでしょうか?PythonはあまりなじみがないのでRubyでの例を載せておきます。
http://gist.github.com/229861
もし私が何か重要な点を見過ごしていれば、ご指摘いただければありがたいです。
Posted by: Makoto | 2009.11.09 at 02:44