Jの衝動書き日記

さらりーまんSEの日記でございます。

Pythonで簡単にWebサービス構築

 お仕事で、Pythonのtornadoについて学んだのでそのメモを公開する。tornadoはシングルスレッドで動作する軽量なWebサービス構築用のフレームワーク。テスト用のMockサービスなどちょっとしたものならばJavaでゴリゴリ書くよりは手っ取り早く作れる。

Python環境構築

 pyenv環境を構築するのがオススメ。pyenvを使えば異なるバージョンのPythonを切り替えて使えるようになる。また、プラグインのvirtualenvを使えば同じバージョンで異なる環境(インストールされているPythonライブラリを変えるなど)を作れる。

   pyenvの管理配下に複数のpythonをインストールし、ライブラリもそれぞれそこに置かれる。

 構築方法は以下に丸投げを参照。pyenv および virtualenv の使い方 - Qoosky

    Windows? 知りません。まあ、Windows上で頑張るよりはvagrant入れてLinux環境作った方が簡単ですよ。

 

tornadoのインストール

 pythonのライブラリインストールはpipを使えば楽。

    pip install tornado

    パッケージの確認はpip list

 

Webサービス構築手順

使用するクラス

  • from tornado.ioloop import IOLoop
  • from tornado.web import RequestHandler, Application

手順

1.HTTP Requestに応答するためのHandlerを作成する。

 RequestHandlerを継承したクラスを作成し、getやpostメソッドを実装する。

class MainHandler(RequestHandler):
    def get(self):
        self.write("Hello, world")

 RequestHandler.write()で応答結果を返す。javaservletのような感じ。

 RequestHandler.render()を使用すればテンプレート化されたhtmlを返せる(jspのような感じ)

 

2.Applicationを作成する。

 引数には、urlとそれに対応するHandlerをペアにしたtupleの配列を渡す。

 定義: class tornado.web.Application(handlers=None, default_host='', transforms=None, **settings)

   Application([
        (r"/hello", MainHandler),
      ], debug=True)


   r"/hello"はraw stringというpythonの記法で制御文字が単なる文字列として扱われる
   **settingsは可変長引数の宣言でkeyword=値の形で渡される debug=Trueなど

 

3.待ち受けポート番号を設定する

app.listen(8888)

 

4.接続受付を行う。

 Ctrl-Cしない限りは応答が返らない

IOLoop.current().start()

 

ソース全体

from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, Application

class MainHandler(RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return Application([
        (r"/hello", MainHandler),
        ], debug=True)

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    IOLoop.current().start()

 

URLパターンマッチング

  REST的にWebサービスを構築するならば、/user/{id}などでGETした時のidを取得したい。これは以下の手順で行えばよい。

1.Applicationを作成するときにurlを正規表現で作成する

   Application([
    (r"/user/(?P<id>[0-9]+)", UrlMatchSampleHandler),
    ...
    ])

 ?P<id>はpython拡張正規表現で、一致する箇所を<>で指定した名称で取り出せる。

 

2.対応するget/postメソッドに引数を設定する

def get(self, id):

 これで、/user/1111でGETした場合、idには1111が渡される。なお、URLパターンに一致しないアクセスは404応答となる。

 

クエリの取得

  /user?id=xxxなどのGETクエリを取得したい場合はget_query_argumentを使用する。なお、指定したパラメータが存在しない場合は例外がスローされるため注意。defaultを指定すればエラーとはならない。

id = self.get_query_argument("id", default=9999)

 

フォームの取得

 POSTで送信されたformデータを取得したい場合はget_body_argumentを使用する。

name = self.get_body_argument("name")

 

Jsonの扱い

 POSTで送信されるデータがjsonの場合は、 json_decodeで変換すればjsonデータとして扱える。なお、Webサービスjson中心となる場合はTornado-JSONを使用した方が楽。

json = json_decode(self.request.body)

 

非同期処理

 tornadoはシングルスレッドで動作するため処理に時間がかかるようなサービスがあると後続のリクエストはその処理が完了するまで待たされることになる。 それを避けるためには対象のサービスを非同期処理に対応させる必要がある。

1.非同期処理とするメソッドに@tornado.gen.coroutineを付ける

2.対象のメソッドにおいて非同期処理の前処理を書く

3.非同期処理を行う箇所にyieldを宣言する

4.yield以降にそれを受け取った処理を書く

  @gen.coroutine
  def get(self):
      http_client = AsyncHTTPClient()
      response = yield http_client.fetch("http://localhost:8888/heavy")
      self.write("respose is {} ".format(response.body))

 

 なお、yield宣言した処理内ではスレッドをブロックするような処理を書かないこと。ブロックするような処理がある場合は非同期処理とはならないので注意。例えばtime.sleepはブロック処理である。

yieldについて

 関数内にyieldがある場合、その関数は返り値としてgeneratorを返すようになる。generatorから値を取り出すにはfor in か__next()__(python3の場合)で取り出す。

 generatorにアクセスするたびにyield宣言されている箇所まで処理を進めていき、yieldのところで値を返す。

 def hoge():
   処理1
   yield "one"
   処理2
   yield "two"
   処理3
   yield "three"
 
 gen = hoge()
 gen.__next()__ → 処理1が実行されてoneが返る
 gen.__next()__ → 処理2が実行されてtwoが返る
 gen.__next()__ → 処理3が実行されてthreeが返る
 gen.__next()__ → 例外スロー

※for in gen の場合は例外はスローされない

 

非提供のもの

  • セッション

   tornado本体では未提供だが拡張版が存在する(torndsession 1.1.4 : Python Package Index)。セッション情報の保存はmemcashedかRedis上で行っている模様。

  • バリデーション

   formバリデーションはtornado本体では未提供。wtforms-tornadoというものがある。jsonに関してはtornado-jsonを使えばjsonschemaによるチェックを行える。

 

参考

 何だかんだ言って公式のサイトがわかりやすく一番参考になる。英語だけど。

 User’s guide — Tornado 4.4.1 documentation

 

 上記のサンプルコードは以下にあります。

github.com