軽量、シンプルな Web フレームワーク bobo を使ってみよう
@ymotongpoo (Python製テンプレートエンジンあれこれとJinja2 - YAMAGUCHI::weblog) さんから Python Web フレームワーク アドベントカレンダー2010 のバトンがまわってきました *1 *2 。今年のお題は「Web フレームワーク」です。普通だったら何を書こうか悩みますよね、でも、ちょっと WebFrameworks - Python Wiki のページを見てください。フルスタックだ、ベースフレームワークだ、テンプレート付きだと、ざっと見渡しても両手の指では数え切れないぐらい Python の Web フレームワークはあるのですね。かぶらないように、、、というか、選り取り見取り、参差錯落(しんしさくらく)、石を投げればフレームワークに当たる、、、えっと何の話でしたっけ。
bobo って何?
ということで bobo という WSGI アプリケーションを作成する軽量 Web フレームワークの紹介です。OSC Tokyo/Fall 2009 で Web開発者に贈る昨今のPython事情 *3 の発表を聴講して知りました。bobo は Zope の前身だったフレームワークの名前で、それがいま風に復活したようです。
bobo の機能は以下の2つです。
とにかく簡単で小さいフレームワークを目標に開発されています。テンプレート言語は持たず、DB とのインタフェースも持たず、その他の機能は WSGI のミドルウェアか、別のライブラリを使ってねと。何て潔く、清々しい、気持ちの良いフレームワークでしょうか。紹介記事の分量も少なくて済みます!
bobo ことはじめ
ことさらに簡単です。
import bobo @bobo.query('/') def hello(): return "Hello world!"
bobo (デバッグ)サーバを起動します。
$ bobo -f hello.py Serving ['bobo__main__'] on port 8080... client.example.com - - [14/Dec/2010 10:56:54] "GET / HTTP/1.1" 200 12
以下の URL にブラウザでアクセスすると "Hello world!" が表示されます。
http://localhost:8080/
URL のマッピングをデコレータで書けるのは簡潔で良いですね。
ちょっと余談になりますが、@bobo.query のリファレンス によると、以下のコードは同じです。
@bobo.query def example(): ... @bobo.query() def example(): ...
bobo.py を覗いてみると、
def query(route=None, method=('GET', 'POST', 'HEAD'), content_type=_default_content_type, check=None, order=None): return _handler(route, method=method, params='params', check=check, content_type=content_type, order_=order) ... 676 def _handler(route, func=None, **kw): 677 if func is None: 678 if route is None or isinstance(route, basestring): 679 return lambda f: _handler(route, f, **kw) 680 func = route 681 route = None 682 elif route is not None: 683 assert isinstance(route, basestring) 684 if route and not route.startswith('/'): 685 raise ValueError("Non-empty routes must start with '/'.", route) 686 687 return _Handler(route, func, **kw) ...
bobo.query デコレータは _handler() を返しているので、その部分を引用しました。本来、デコレータの扱いは、カッコの有無で振る舞いが大きく変わってしまいます。このカッコの有無の違いを意識させない実装になっています *5。この意識させるさせないの議論は賛否両論ありそうでコメントし辛いですが、ドキュメントにも明記されているので bobo はそういう仕様なんだということですね。
bobo にできること
ドキュメントを眺めながらざっくり見ていきます。
自動的なレスポンスオブジェクトの生成
bobo は値を返すときに レスポンスオブジェクト を自動的に生成します。実際には webob.Response を使用しています。例えば、@bobo.query デコレータに content_type を指定することで生成するレスポンスオブジェクトを変更することができます。
# -*- coding: utf-8 -*- import bobo @bobo.query("/", content_type="text/html; charset=sjis") def hello(): return unicode("Hello ワールド!", "utf-8") @bobo.query("/data", content_type="application/json") def get_data(): return dict(a=1, b=2)
ルート
Ruby on Rails のルーティングを模した URL をマッピングする構文を bobo では ルート と呼びます。
ちょっと汚いですが、GET パラメータとルート構文で受け取る文字列の型が違うっぽいような振る舞いになることを確認しました。よくはまるところですね。URL エンコードされたパラメータの扱いがどうなるのかエラーになってよく分かりませんでした(> <)
# -*- coding: utf-8 -*- import bobo @bobo.query("/route/:path?", "GET") def route_test(param=unicode("指定なし", "utf-8"), path="myapp"): s = unicode("GET パラメータは %s (%s 型) ですが<br />" "route 構文のパスは %s (%s 型)です。", "utf-8") s = s % (param, type(param).__name__, unicode(path, "utf-8"), type(path).__name__) return s
以下の URL にアクセスします。
http://localhost:8080/route/アプリ?param=test
ブラウザの出力です。
GET パラメータは test (unicode 型) ですが route 構文のパスは アプリ (str 型)です。
サブルート
ルートの仕組みを利用して、もう少し高度な機能も用意されています。subroute というデコレータがあり、クラス(呼び出し可能オブジェクト)をデコレートします。クラスデコレータは Python 2.6 から使用できます *6。subroute デコレータを使用すると、URL に対するレスポンスをあらかじめ対応付けるのではなく、リクエストされた時に URL のマッチング処理(階層構造の解析)を経て、レスポンスを返すことができます。ドキュメントによると「より巧みな抽象化、柔軟なリソースへのアクセスといった機能を提供する」ものだそうです。
クラスデコレータとか使ったことないな、先ずはサンプルコードです。
# -*- coding: utf-8 -*- import bobo USER_TABLE = { "1": dict(name="t2y", hobby=("programming", ("0", "2"))), } PROG_LANGUAGE_TABLE = { "0": "Python", "1": "PHP", "2": "Bash", "3": "C", "4": "PL/SQL", } HOBBY_TABLE = { "programming": PROG_LANGUAGE_TABLE, } @bobo.subroute("/user/:user_id", scan=True) class User(object): def __init__(self, request, user_id): self.user_id = user_id self.data = USER_TABLE[user_id] @bobo.query("/") @bobo.query("/index.html") def index(self): return """ id: %s name: %s See my <a href="hobby/">hobby</a>. """ % (self.user_id, self.data["name"]) @bobo.subroute def hobby(self, request): return Hobby(self.data["hobby"]) @bobo.scan_class class Hobby(object): def __init__(self, data): self.hobby = HOBBY_TABLE[data[0]] self.data = data[1] @bobo.query("/") def index(self): return "\n".join("<li><a href=%s>%s<a></li>" % ( key, self.hobby.get(key)) for key in self.hobby) @bobo.subroute("/:item_id") def subitem(self, request, item_id): return ProgrammingLanguage(self.data, item_id) @bobo.scan_class class ProgrammingLanguage(object): def __init__(self, data, item_id): self.data = data self.item_id = item_id @bobo.query("") def get(self): text = "I don't like %s programming" if self.item_id in self.data: text = text.replace("don't", "") return text % (PROG_LANGUAGE_TABLE.get(self.item_id))
凄いです、、、。よくこういうの考えるなーとつくづく感心します。
例えば、以下の URL へアクセスします。
http://localhost:8080/user/1/hobby/0
ブラウザの出力です。
I like Python programing
scan_class クラスデコレータはクラス内のルートを調べるために使用します。URL のマッチング処理は次の順番で行われ、適切な処理が呼び出されます。
- "/user/1" に対する "/user/:user_id" がマッチして User クラスが見つかる
- ".../hobby" は User クラス内の hobby() メソッドにマッチして Hobby クラスが呼び出される
- ".../0" は Hobby クラスの "/:item_id" にマッチして ProgrammingLanguage クラスが呼び出される
- 最終的に ProgrammingLanguage クラスの空のパスに対するメソッド get() が呼ばれます
URL の階層構造がパッと見では分からないというデメリットがありますが、URL の階層と機能を分離できるので、うまく設計するとメンテナンス性が高くなりそうな気がします。
ちなみに以下のように scan_class クラスデコレータと subroute クラスデコレータのキーワードに scan=True をセットするのは同じようです。
@bobo.scan_class class A(object): ... @bobo.subroute(scan=True) class A(object): ...
bobo 応用編
何かちょっとしたアプリを自作しようという構想はあったのですが、サブルートの仕組みがよくできてるなーと感心しているうちに時間がなくなってしまいました(> <)、bobo のサイトに アプリケーションサンプル があるのでそちらを参考にしてください。
まとめ
要するに bobo ドキュメント のチュートリアルをざっくり「紹介しました」って記事になってしまいました。これを機に bobo という Web フレームワークの存在を知って、いつか役に立てばいいなと思います。とっても簡単ですしね。次は blockdiag が素敵な感じの @tk0miya へバトンを渡します。
*1:gihyo.jp さんの 本日12月1日より,プログラマ有志による技術系Advent Calendarが各所ではじまる で他のコミュニティで行われているアドベントカレンダーが紹介されています。今、数えてみたら33個もあります
*2:Python Advent Calendar jp 2010
*3:こちらから発表資料がダウンロードできます: OSC2009 Tokyo/Fallで発表しました
*4:bobo は HTTP リクエスト/レスポンスオブジェクトを生成するために WebOb を使用(依存)するようです。
*5:Shibu's Diary: 引数のカッコが省略可能なデコレータの実装方法 がとても分かり易いです