Python初心者のインケンがマイクロフレームワークのbottleでMVCしちゃうサンプル

2013-12-26

はじめに

Pythonまったく使ったことないから、Pythonでなんか作ってみようと思ったのがきっかけです。

かといって、「Hello World」出すだけなんてしょーもないことしても意味が無いので、ちゃんとフレームワークを使ってMVCっぽく作ろうと思いました。

せっかくなので、自分がマンガを今何巻まで持っているかを管理するツールを作りたいと思います。いつも何巻まで持ってたっけかなーって忘れちゃうので。

CRUDの基本を抑えたサンプルです。

ちなみに、Pythonの構文やモジュールのインストール等は、検索すればすぐ出てくるのでググってください。

Pythonのフレームワーク

Pythonのフレームワークで有名なのは、フルスタックなDjango、ライトなFlask、じわじわ人気が出てきてるらしいPyramidなどがありますが、

個人的にフルスタックなフレームワークは好きでないのと、1ファイルでライトなフレームワークというのに惹かれたので、bottleを使います。

ただ、Python始めた当初にbottleのMVCサンプルをいろいろ探したのですが、大したものがなかったので今回作ることにしました。

サンプルソース

今回のソースはgitにあげてあります。
これを基に説明していきます。

https://github.com/inkenkun/bottle_mvc

完成形

今回のサンプルの完成形です。

自分が持ってるマンガ一覧と、新規登録ページ、編集削除ページです。

サンプルのために一覧には一応「次へ」や「前へ」のpagerもつけてます

pmanga1pmanga2pmanga3

Pythonのバージョン

macにも予め入ってる 2.7を使います。

ちなみにbottleはPython 2.5以上か 3.x. じゃないと動きません。

Pythonは2系と3系では書式等が結構違います。3系だと今回のサンプルはそのままでは動かないのであしからず。

必要モジュール

pipっていうnodejsのnpmみたいなのでさくっと入れます。

#MySQLコネクタ
sudo pip install MySQL-python

#テンプレートエンジン
sudo pip install jinja2

#HTTPサーバー
sudo pip install gunicorn

#プロセス管理
sudo pip install supervisor

bottleは今回はダウンロードしてきたものを特定のディレクトリに配置して使うので、ここでは入れません。

jinja2

PHPのsmartyライクなテンプレートエンジンです。

Pythonのテンプレートエンジンの中ではわりとデファクトな感じらしいです。

gunicorn

bottleだけでもHTTPサーバーとしての機能を持っていますが、gunicornを使うとnodeのclusterみたいなことができます。

プロダクトとして使うのであれば使ったほうがいい感じ

supervisor

gunicormの起動を管理します。

イメージ的には、gunicornでbottleを複数起動し、supervisorでそのプロセスを管理し、Nginxでgunicornにプロキシする感じ

とりあえず、gunicorn + supervisor + Nginx が今の流行りらしいです。

ディレクトリ、ファイル構成

さて、ディレクトリとファイル構成を見て行きましょう。

  • /
    • app/
      • controllers/
        • _init_.py
        • manga.py
      • models/
        • _init_.py
        • db.py
        • manga.py
        • pager.py
      • _init_.py
    • config/
      • db.cnf
    • libs/
      • bottle.py … bottle本体
    • sql/
      • create_table.sql
    • stat/
      • css/
        • style.css
    • views/
      • inc/
        • header.html
        • footer.html
      • edit.html
      • index.html
      • new.html
    • index.py … dispatcher

bottleの基本構成

基本的にbottleは、本体とそれを動かすdispatcherさえあれば動きます。ネットに載ってるサンプルもほとんどそれです。

でもそれだと業務的には使えないので、もうちょっとMVCっぽく上記の構成にしてみました。

SQLは前もってMySQLに流しとく用です。

_init_.pyって?

pythonは基本的には他のファイルをそのままではインポートできません。

インポートしたいディレクトリに、この_init_.pyを置いておくと、インポートできるようになります。

中身は空でも大丈夫ですし、インポートしたいファイルを予めこれに記載してもOKです。

libsのディレクトリには_init_.pyないんだけど、、って思った方もいるかもしれませんが、ここは別途呼び出しています。

View (テンプレート)

viewsフォルダの中身ですね。

jinja2というテンプレートエンジンを使っています。

特にトリッキーなことはしてないです。smartyとか使ってる人であればすんなり入れるのではないでしょうか。

こんな感じで、共通部分を呼んだり
{% include "inc/header" %}

ループ回したり

{% for i in result.mangas %}
<tr>
    <td>{{i.num}}</td>
    <td>{{i.name}}</td>
    <td><a href="/edit/{{i.id}}"><button class="pure-button pure-button-small">編集</button></a></td>
</tr>
{% endfor %}

で、特に普通ですね。

dispatcher

rootディレクトリ直下のindex.pyです。

中身短いので、全部載せます。

index.py
# -*- coding: utf-8 -*-

import sys
sys.path.append('libs')

from bottle import route, static_file, default_app
from app.controllers import *

# ==========================================
#   静的なパスを追加</code></div>
# ==========================================
@route('/stat/<filepath:path>')
def server_static(filepath):
    return static_file(filepath, root='./stat/')

## bottle単品で動かしたい場合
#from bottle import run
#run(host='localhost', port=3000)

## gunicornを使う場合
app = default_app()

ネットのサンプルだと、すべてのルーティングがこのファイルに書いてあるのですが、今回はコントローラーに記載しています。

1行目のutf-8の記述は、utf-8で書く場合必ず必要です。

4行目の sys.path.append(‘libs’) でbottle本体があるディレクトリを呼んでるんですね。なので_init_.pyがいりません。

7行目で app/controllers の中身を全部インポートしています。この書き方ができるのは app/controllers/_init_.pyで以下のように全部ファイルを読み込んでいるからです。

app/controllers/_init_.py
import os, glob
__all__ = [os.path.basename(f)[:-3] for f in glob.glob(os.path.dirname(__file__) + "/*.py")]

controllersの_init_.pyでは上記のことをやってるのにmodelでやってないのは、modelファイルはcontrollerで個別にインポートしているからです。

今回gunicornで動かす用の書き方になっていますが、gunicornを使わないでbottleだけで動かしたい場合は、21行目をコメントして、17,18行目をコメントアウトしてください。

bottleだけで動かしたい場合は、index.pyのディレクトリに移動して、「python index.py」で動かせます。テストするにはよいです。

Controller

app/controllersの中身です。

コントローラーでは、ルーティングに沿ってモデルを実行し、テンプレートに渡しています。

app/controllers/manga.py
#一覧ページ
@route('/')
@route('/<page:int>')
def index(page=1):
    result = model.load(page)
    return template('index', result = result)

こんな感じで複数のルーティングを一つのメソッドに対して割り当てたりもできます。

上記は、「http://localhost/」 ってアクセスした場合と、「http://localhost/2」等の次のページへ移動した時の共通処理になっています。

Model

app/modelsの中身です。

まず、DBのコネクタ部分

app/models/db.py
import MySQLdb
import ConfigParser

config = ConfigParser.ConfigParser()
config.read('config/db.cnf')

dbhandle = MySQLdb.connect(
    host = config.get('db', 'host'),
    port = config.getint("db","port"),
    user = config.get('db', 'user'),
    passwd = config.get('db', 'password'),
    db = config.get('db', 'database'),
    use_unicode=1
)
con = dbhandle.cursor(MySQLdb.cursors.DictCursor)

4,5行目で confファイルを読んでます

13行目「use_unicode=1」ですが、これないとDBから取ってきた値がUnicodeになっていなくて、テンプレートに渡した際にエラります。

Python2系はマルチバイト処理が結構厄介です。

15行目はDBの結果を連想配列(ディクショナリ)で扱えるようにする設定です。

PHPでは当たり前のようにやってくれる処理も、Pythonではいろいろめんどくさいですね。

次に、Pagenation

「次のページ」「前のページ」のあれです。

今回のサンプルでは特に必要ないと思ったのですが、使用頻度が高そうなので入れときました

これはFlaskのsnippetsから丸パクリです。

最後に、Manga Model

一番のメインな部分です。

と言ってもDBから値を取ってきて、returnしてるだけです。

こんな感じで、プレースホルダーを使えます。

sql = "select * from manga order by kana"
sql += ' limit %s, %s'
db.con.execute(sql, (start, define_page))

更新系はちゃんと最後「db.dbhandle.commit()」トランザクションをコミットしてあげる必要があります。

それと、pythonでは、クラス内のメソッドは、「def load(self, page):」こんな感じで第一引数に必ず「self」を指定する必要があります。

呼び出す場合は、selfを無視してこんな風に。

model = app.models.manga.Manga()
result = model.load(page)

ソースはざっとこんな感じです。

Pythonのインデント書式のおかげでスッキリとしたソースになりますね。

Gunicornで複数Workerを立ち上げよう

bottleだけで動かしてると、トラフィックとか増えた際になんか心もとないです。

Nodejsのclusterのように、何個か同時に立ちあげたいですよね。

そこでGunicorn。RubyのUnicornの派生です。

Gunicornはコマンドラインから実行するには以下の様になります。

gunicorn -b 127.0.0.1:3000 -w 1 index:app

-b は立ち上げるIPとポート番号 -w はworker数、indexはdispatcherのファイル名、appはdispatcher内のアプリ名になります。

ちなみに -D を指定すると、バックグラウンドのデーモンとして起動しますが、supervisor でデーモン管理する場合はつけません。

あと、自分のローカルのマシンでテストする際には上のコマンド叩いてテストすることもありますが、今回はsupervisorの設定ファイルに書きます。

SupervisorでGunicornを管理する

gunicornのコマンドを毎回ポチポチ叩くわけに行かないので、gunicornを管理してくれるsupervisorを入れます。

Nodejsでもおなじみのsupervisorです。

supervisorの設定系や起動スクリプトはこのへんを参考に。

「/etc/supervisord.conf」にgunicornの設定をしていきます。

どこか適当なところに(一番下にでも)以下の記載をします。

/etc/supervisord.conf
[program:gunicorn]
command=/usr/bin/gunicorn -b 127.0.0.1:3000 -w 2 index:app
directory=/var/manga
user=root
autostart=True
autorestart=True
redirect_stderr=True

これで、supervisorを起動すると、127.0.0.1:3000 でアプリが起動するので、Nginxでそのアドレスに対してproxyしてあげれば完成です。

まとめ

bottleは1ファイルのマイクロフレームワークですが、ちゃんとMVCになりました。

あとは状況によって、DAO分けたり、Validatorつけたりすると、実務でも十分使えるのではないでしょうか。

bottleは初めてPythonを触ってみるのにはちょうどいいフレームワークだと思います。

Flaskとも書式が似てるので、bottleからFlaskもすんなり移行できそうです。