Python
Flask
python3
sanic

pythonでサクッとwebサービス作るならFlaskよりsanicがいいよって話

イントロ

           ▄▄▄▄▄
        ▀▀▀██████▄▄▄       _______________
      ▄▄▄▄▄  █████████▄  /                 \
     ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |
   ▀▀█████▄▄ ▀██████▄██ | _________________/
   ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/
        ▀▀▀▄  ▀▀███ ▀       ▄▄
     ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
   ██▀▄▄▄██▀▄███▀ ▀▀████      ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███     ▌▄▄▀
▌    ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀   ▀▀████▒▒▒▒▄██▀
          ▀▀█████████▀
        ▄▄██▀██████▀█
      ▄██▀     ▀▀▀  █
     ▄█             ▐▌
 ▄▄▄▄█▌              ▀█▄▄▄▄▀▀▄
▌     ▐                ▀▀▄▄▄▀
 ▀▀▄▄▀

そもそもPythonでwebアプリをつくろうという選択肢が辛いですが、それでもPythonでやりたいんだ!という人向けに書きます。

あとsanicの日本語記事が少ないのでTOしていきたい。

sanicってなに

image.png

Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article.

On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.

https://github.com/channelcat/sanic

意訳をすると

  1. Flaskライクだよ!
  2. uvloop使って高速で動くwebサーバーだよ!
  3. Python3.5+だよ!

ということです。(多分)

Flaskじゃいけないの?

Flaskでもuvloopを使って高速化することはできます。
が、別途ライブラリ入れたりしなくて面倒ではあります。
(そもそもFlask自体多機能化はせずにシンプルに実装して必要に応じでライブラリを入れるという設計理念があったはず)
依存関係やらなにやら色々めんどうなことはできるだけ関わりたくないので標準ではいっているsanicを使えば非同期で高速な処理を行えつつ、Flaskライクで学習コストが低いのが魅力です。

性能がどれくらいすごいのかというのはすでに検証している方がいらっしゃるので画像を引用させていただきます。

image.png

PythonのWebアプリケーションフレームワーク Sanicを試す « Rest Term

細かい数値はリンク先を参照してください。
並列処理の高負荷だとsanicじゃなくてuvloopすげーとなりますが、結局はサクッと作ってさっとデプロイするとなるとSanicの方が良いのかなと思います。

メリットばっかり言ってもフェアじゃないのでデメリットとして以下の点があります(Flaskと比べて)

  1. 実績が少ない
  2. 日本語ドキュメントが無い
  3. 結局Flaskに戻る

1は歴史的に浅いので仕方がないの一言に終わります。

2に関しては公式ドキュメントを読めや!英語読めや!でも良いんですが、初学者が触るには日本語ドキュメントがあったほうが入り口が広がって良いことですし、母国語が日本語なので日本語で読んだほうが翻訳リソースを割かなくて嬉しい。

3に関してはコミュニティがFlaskに比べて小さいのであれもやろう、これもやろうとなったときに詰むのでなんだかんだでFlaskに戻ってしまうということがあります。webをPythonでやろうと思ってるpythonianなら自前で実装してしまうことが多いでしょうが、やはりそこがボトルネックになるのは辛いものがあります

誤解を生まないようにここで述べておきますが、Githubのstar数はFlaskの方が圧倒的に上ですが、(僕が勝手に思っているだけですが)アクティブなコミュニティ活性度の指標となるOpenIssueは俄然sanicの方が多いです。

Hello World

簡単なHello WorldはQiitaの記事を貼って終わりにしたかったんですが、一個もないので書きます。

install

pyenvで環境を離しておいた方がいいですよ。
そのあたりはこの記事の本質ではないので貼って終わりにします。
注意点は最初に書きましたがPythonのバージョンは3.5+にしてください。

Pyenvの使い方 - Qiita
pyenvのインストール、使い方、pythonのバージョン切り替えできない時の対処法 - Qiita

準備ができたらsanicのインストールです

pip install sanic

以上です。

表示させてみる

READMEのまんまをコピペします。名前はなんでも良いんですが、ここではmain.pyとでもしておきましょう

main.py
from sanic import Sanic
from sanic.response import json

app = Sanic()

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

できたら早速動かしてみましょう

python main.py

http://localhost:8000

スクリーンショット 2018-08-07 15.03.52.png

Ctr+cでサーバーが止まります

以上です。

各パーツの説明

Hello Worldだけだと寂しいので各パーツの説明をしていきます。

かといって全部説明するのは難しいので適時該当箇所の公式ドキュメントのURLを貼っておくので読んでください。

import

main.py
from sanic import Sanic
from sanic.response import json

Sanicをimportしています。
import fromがわからない人むけに説明すると、pythonでライブラリを使うときは import ライブラリ名となります。

じゃぁfromって何だよ。となるので from ライブラリ名 import 関数名 と返します。

上記の例だと from sanic import Sanic だとsanicライブラリのSanicを読み込んでいる。(それはそう)
二段目はsanicの中にあるresponseクラスを呼び出して、その中のjsonという関数を呼び出している。
具体的にいうとGithubでいうところの↓を呼び出している。
https://github.com/channelcat/sanic/blob/master/sanic/response.py

あれ?ここの挙動おかしいぞ?となったらソースコードを眺めてみよう。呼び出されている関数がどういう挙動をしているか理解できればその疑問も解決するはず。

インスタンス

main.py
app = Sanic()

インスタンスってなに?と言われると時間が無限にたりないので各自ググってください。
雑でもいいなら"importしたSanicをappに入れている"という理解で3割ぐらい合ってます

デコレーター

main.py
@app.route('/')

なかなか見慣れない@ですが、これらはデコレーターというものです。
デコレーターに関しては記事を貼って終わりにします。
Pythonのデコレータについて - Qiita
Pythonのデコレータを理解するための12Step - Qiita

Routing

雑で良いなら、括弧で囲まれた場所がRoutingを指しているというところだけ理解できればなんとかなります。
例えばさっきのmain.pyを以下のように書き換えてみます。

main.py
from sanic import Sanic
from sanic.response import json

app = Sanic()

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

@app.route('/api/')
async def apiindex(request):
    return json({'API': 'Index'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

先程のように python main.py を行い http://localhost:8000 にアクセスしてみましょう。

先ほどと同じく {"hello":"world"} が出たと思いますが、 次は http://localhost:8000/api にアクセスしてみましょう

スクリーンショット 2018-08-07 15.34.51.png

@app.route('/api/')の真下に書かれたjson({'API': 'Index'})が返ってきてきます。

このように@app.route()の括弧の部分をURIとして指定してあげればそれに応じたリクエスが返ってきます。

https://sanic.readthedocs.io/en/latest/sanic/routing.html

動的なRouting

数ページの静的なページだったらこれで問題ないのですが、動的なRoutingの必要が出たときにその分の@app.route()を作るのはしんどいので変数に入れてあげましょう。

main.py
from sanic import Sanic
from sanic.response import json
from sanic.response import text

app = Sanic()

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

@app.route('/api/')
async def apiindex(request):
    return json({'API': 'Index'})

@app.route('/api/<path>')
async def path_handler(request, path):
    return text('path = ' + path)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

しれっとfrom sanic.response import textが加わってますが後ほど説明します

大きな追加点は

main.py
@app.route('/api/<path>')
async def path_handler(request, path):
    return text('path = ' + path)

で、見たとおりなんですが('')内において、<>でくくる事によってその囲った部分が変数となります。

スクリーンショット 2018-08-07 15.51.23.png

https://sanic.readthedocs.io/en/latest/sanic/routing.html#request-parameters

POSTや型

型っぽいのを宣言できたり、POSTを受け取るときもこの部分を編集します。

integer
@app.route('/number/<integer_arg:int>')
async def integer_handler(request, integer_arg):
    return text('Integer - {}'.format(integer_arg))
Post
@app.post('/post')
async def post_handler(request):
    return text('POST request - {}'.format(request.json))

https://sanic.readthedocs.io/en/latest/sanic/routing.html#http-request-types

static file

これだけあれば大抵のことができそうですが、少し問題点が。

現状、URLと返すリソースが1対1となっています。

webサイトを作ろうとした時CSSやjsを書くと思うのですが、このままだとcssやjsまで1回ずつ書かないといけなくなってしまいます。

そこでstaticなファイルはフォルダでまとめて置いておく機能があるので利用しましょう。

以下のようなフォルダ構成を想定します

.
├── index.html
├── main.py
└── static
    ├── css
    └── js
        └── main.js

雑にapp.static('/static', './static')を貼るだけで返したhtmlに

<script type="text/javascript" src="/static/js/main.js"></script>

と記述すると読み込んでくれます。

https://sanic.readthedocs.io/en/latest/sanic/static_files.html
https://github.com/channelcat/sanic/blob/master/sanic/static.py

WebSocket

力が尽きたのでパスします

https://sanic.readthedocs.io/en/latest/sanic/routing.html#websocket-routes
https://github.com/channelcat/sanic/blob/master/sanic/websocket.py

Response

ようやくサーバーに問い合わせて返ってくる内容について触れます。
富士山で言うなら五合目あたりでしょうか。

main.pyのこの部分

main.py
return json({'hello': 'world'})

sonicでは以下の8つのfile responseをサポートしています

Plain Text
HTML
JSON
File
Streaming
Redirect
Raw

公式ドキュメントには

from sanic import response


@app.route('/text')
def handle_request(request):
    return response.text('Hello world!')

とありますが、個人的には

from sanic.response import text


@app.route('/text')
def handle_request(request):
    return text('Hello world!')

の方がキレイかなと思います。(やっている動作自体に変わりはないです)

https://github.com/channelcat/sanic/blob/master/sanic/response.py#L116

jsonに関してjson文字列をresponse.json()に入れるとダブルクォーテーションの前にバックスラッシュが入るバグっぽいのが手元で起こって泣く泣くtextで返したりしています。

Deploy

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

はい、ここです。

if __name__ == '__main__':ってなんですか

[Python] if __name__ == '__main__' について理解しよう - Qiita
29.4. __main__ — トップレベルのスクリプト環境 — Python 3.6.5 ドキュメント

要はこのmain.pyが実行されたときにしか呼ばれませんよ〜ということです。

app.run()
こいつがメインなんですが、オプションは以下のとおりです

オプション デフォルト値 説明
host "127.0.0.1" サーバー名
port 8000 ポート番号
debug False デバッグ出力の可否
ssl None SSL対応させるか
sock None websocketの受け入れ
workers 1 動かすワーカー数
loop None asyncioでループ処理させるか
protocol HttpProtocol プロトコル

https://sanic.readthedocs.io/en/latest/sanic/deploying.html
https://github.com/channelcat/sanic/blob/master/sanic/app.py#L651

サンプル集とか

公式のGithubにいっぱいサンプルがあるのでそれっぽいやつを眺めてみると良いのではないでしょうか

https://github.com/channelcat/sanic/tree/master/examples
https://github.com/channelcat/sanic/wiki/Extensions
https://github.com/channelcat/sanic/wiki/Examples

まとめ

Flaskを触ったことある人ならお気づきですが大体Flaskと同じです。

個人的には初心者はある程度ナウい機能がデフォで入ってるsanicを使って限界を感じたらFlaskに行けばいいじゃないんですかね。

あと好きなsonicは、東京エンカウントで悠木碧さんが好きなゲームを聞かれて答えるモノマネをした杉田智和の「そ゛に゛っ゛く゛ー゛」です。

次回はsanicのテンプレート(Jinja2)で動的ページ作ったり、sanic × vue.jsでイケイケ爆速SPA作ったりする記事を書ければなと思います。

最後にREADMEから注意すべき部分を引用して終わります

TODO

http2

Limitations

No wheels for uvloop and httptools on Windows :(