フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

人間のためのイケてるPython WebFramework「responder」、そして作者のKenneth Reitzについて

この記事は Python その2 Advent Calendar 2018 - Qiita の1日目です。

responderとは

GitHub - kennethreitz/responder: a familiar HTTP Service Framework for Python

f:id:mergyi:20181201121349p:plain

f:id:mergyi:20181201180257p:plain
Starの遷移

  • この記事では、responderが他のWeb Frameworkと比べて何が素晴らしいのか ということと、responderから見えてくる Kenneth Reitzというエンジニア について書きたいと思います。

Python WebFrameworkは戦国時代だが…

みなさんが「PythonのWebFramework」と言われて思いつくものはなんでしょうか。Django、Flaskでしょうか。まず、Python WebFramework界隈がどんな状況か見てみます。主流なFrameworkを時系列にまとめてみました。

f:id:mergyi:20181201161918p:plain
Python WebFrameworkの歴史

この図から下記のことがわかります。

  • 毎年のように新しいWebFrameworkが出ており 乱立 しているとも言える
  • 2011年以降はデファクトになっているWebFrameworkはない
    • sanicはstar数は多いですが、導入例の数を見るとデファクトとは言えない気がします
  • 2015年以降、GraphQL、asyncなどモダンな機能を備えたWebFrameworkが出てきているが、定着はしていない(未だにDjango、Flaskがメインに使われている)

PythonのWebFrameworkと聞くと、「また新しいWebFrameworkか(うんざり)」「DjangoとFlaskでいいじゃん」と思う方ももしかしたらいるかもしれませんが、今見たように、モダンな機能を備えたWebFrameworkが定着していない、という問題があったり、また今年はFlaskの作者がFlaskのメンテナンスを(実態として)辞めるという出来事もあり、実は今 新しいデファクトになるWebFrameworkが求められている と言えると思います。そしてこのresponderがそのデファクトになるのでは…と期待しながらこの記事を書いています。

responderを使う理由

まず、responderの魅力について書いていきたいと思います。

基本的な思想

READMEには下のような記載があります。

The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have.

記載の通り、FlaskとFalconの良さを1つにまとめたのが基本的な思想で、そこにKenneth Reitzが持っていた幾つかアイディアを加えたのがresponderになります。

使いやすいAPI

responderの一番の魅力はフレンドリーで使っていて楽しいAPIだと思います。 ToyExampleでresponderと、FlaskとFalconで書いたものを比較してみたいと思います。

Hello World

まずはHello worldです。

responder

import responder 

api = responder.API()

@api.route("/")
def hello_world(req, resp):
    resp.text = "Hello, World!"

Flask

from flask import Flask

app = Flask()

@app.route("/")
def hello_world():
    return "Hello, World!"

Falcon

import falcon

class HelloResource:
    def on_get(self, req, resp):
        resp.body = "Hello World!"

app = falcon.API()
app.add_route('/', HelloResource())

まさに、responderはFlaskとFalconの中間という感じですね。 これだけだとFalconよりはシンプルに書けそう、というのはわかりますが、さらにFlaskが簡単そうで、responderの魅力が伝わりませんね。

動的URL、QueryStringから情報取得&status_code、headerセットしてjsonをresponse

それでは次にほんの少しだけ複雑な例で見てみたいと思います。 URLとQueryStringからパラメータを受け取って、headerとstatus_codeを設定してjsonで返してみます。

responder

import responder 

api = responder.API()

@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
    foo = req.params.get("foo", "")
    resp.headers["X-Pizza"] = "42"
    resp.status_code = 200
    resp.media = {
      "Hello": who,
      "Foo": foo
    }

Flask

from flask import Flask, jsonify, make_response, request

app = Flask()

@app.route("/hello/<who>")
def hello_to(who):
    foo = request.args.get("foo", "")
    body = jsonify({
        "Hello": who,
        "Foo": foo 
    })
    resp = make_response(body, 200)
    resp.headers["X-Pizza"] = "42"
    return resp

途端にFlaskの方は気になる箇所が噴出してきます。

  • responseの構築方法に一貫性がない
  • 追加で何かやろうとするたびに import でオブジェクト/メソッドを呼び出さないといけない
  • <> 記法がPythonに馴染みがない
  • QueryStringsへローカル変数でないrequestオブジェクトからアクセスしないといけない
  • (ここに書いてないですが)status_codeの返し方など色々な書き方ができてしまう

などです。

※ ちなみに自分はFlaskに精通しているわけではないのでもっと洗練された書き方ができるのかもしれません。ただドキュメントページで一番最初に出てくる通りにやるとこうなります。

responderでは、これらの問題点が全て解決されていることがわかります。

  • responseの構築方法が一貫している
  • importする必要があるのは responderのみ
  • {} 記法(f-string記法)で馴染みがある
  • QueryStringsへのアクセスはローカル変数のrequestオブジェクトから行える
    • さらにこのreqestオブジェクトは requests ライブラリで互換している

これらの解決方法はすべて、Falconから得ています。 端的にいうと、responderは Flaskの少しイケてないところをFalconのデザインで圧倒的に改善した Frameworkだと言えると思います。

非同期処理が簡単に書ける

APIのデザインはFlask、Falconから得ていますが、responderにはそのどちらも持っていない機能のあります。その1つが非同期処理が簡単に書けることです。 公式docsに書いてあるExampleですが、例えば画像をアップロードするAPIなどで、「responseは先に返してバックグラウンドでアップロード処理を行いたい」場合に、async/await記法を使って簡単に書くことができます。

import time

@api.route("/incoming")
async def receive_incoming(req, resp):
    @api.background.task
    def process_data(data):
        """Just sleeps for three seconds, as a demo."""
        time.sleep(3)

    data = await req.media()
    process_data(data)

    # Immediately respond that upload was successful.
    resp.media = {'success': True}

GraphQL APIが簡単に書ける

またGraphQL APIも簡単に書くことができます。grapheneというWebFrameworkを使っています。

import graphene

class Query(graphene.ObjectType):
    hello = graphene.String(name=graphene.String(default_value="stranger"))

    def resolve_hello(self, info, name):
        return f"Hello {name}"

schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)

api.add_route("/graph", view)

他にもモダンな機能いろいろ…

その他にも、たくさんのモダンな機能を備えています。 この辺りは2018年に開発されただけあるな、という感じです。

  • FlaskなどのWSGI Applicationのマウント
  • SPA対応
  • HSTS対応

詳しくは公式docを御覧ください。(既に公式docの完成度が高い…)

https://python-responder.org/en/latest/tour.html

結論: responderは素晴らしい

シンプルでわかりやすいAPI、GraphQL対応,モダンな機能などresponderは素晴らしいWeb Frameworkですね!

https://media0.giphy.com/media/3ohs7R91OkVnSoKq4g/giphy.gif?cid=3640f6095c0266dc33556f4c41ef82ee

responderの裏側(構成要素)

外側から見たresponderの魅力がわかったので、内側も少しだけ解説しておこうと思います。

APIデザイン

APIデザインは上述したようにFlaskとFalconの良いとこ取りになっています。

ASGIサーバー

ASGIサーバー機能はresponderでは実装されておらず、uvicornというライブラリを使っています。

github.com

GraphQL

GraphQL機能もresponderでは実装されておらず、上述したようにgrapheneというWebFrameworkを使っています。

github.com

実は機能として実装しているものは少ない

このように、実はresponderは0から実装されたものではなく、既存の良いプロダクトを上手く組み合わせて作られている ことがわかります。これは responder の重要な特徴であり、また作者の Kenneth Reitz の性質を非常に表している要素です。

Kenneth Reitzが今まで開発したプロダクト

Kenneth Reitz というエンジニアの性質を知るために彼が今までに作ったプロダクトを見ていきたいと思います。

requests

GitHub - requests/requests: Python HTTP Requests for Humans™ ✨🍰✨

おそらく一番有名なライブラリだと思います。pythonを書いたことがある人の多くが一度は使ったことがあるのではないでしょうか。 requestsはpython標準moduleのurllibを使いやすくラップしたもの です。

pipenv

GitHub - pypa/pipenv: Python Development Workflow for Humans.

今年PyPAの公式Toolにも加えられた環境管理用のライブラリです。pipenvは主に標準moduleのvenvをラップしたもの です。

requests_html

GitHub - kennethreitz/requests-html: Pythonic HTML Parsing for Humans™

requests_htmlは、今年の上旬に公開されたライブラリで、requetsやbeautiful soup、pyppeteer、pyqueryなどをラップした 、htmlの取得、パースが簡単に行えるライブラリです。 知名度はまだそんなにないですが、responder同様かなりオススメです。

ライブラリの共通点

先ほど、responderについて、既存の良いプロダクトを上手く組み合わせて作られている と書きましたが、このように Kenneth Reitz の既存のライブラリは全てこの性質が当てはまることがわかります。Kenneth Reitzはrequests、pipenvのように、とても人気のあるライブラリを開発していますが、実は新しい機能の開発はほとんどしておらず、Kenneth Reitzのプロダクトの本質は APIデザイン にあると思います。

Kenneth Reitzは天才DX(Developer Experience)デザイナーである

Kenneth Reitzがどういうエンジニアなのか、ということを考えるときに「DX(Developer Experience)デザイナー」という言葉が自分の中でしっくりきます。

gfx.hatenablog.com

DX: Developer Experience (開発体験)とは、あるシステムを「気持ちよく開発・保守できるかどうか」を示すもの 開発者は開発・保守という行為を通じたそのシステムのユーザーであり、DXはUXの一種である DXがよいと日々の開発を楽しめるようになり、気持ちに余裕ができる

自分がrequestsやpipenv、そしてresponderを使っているとき、便利なのはもちろんなのですが、何よりも 「楽しい」 と感じます。Kenneth Reitzは楽しいと思えるAPIデザインをする天才だと思います。

例えば、前述したrequests_htmlでは下のようなAPIがあります。

from requests_html import HTMLSession
session = HTMLSession()
r = session.get('https://python.org/')

r.html.find('.foo > .bar', first=True) # first=True!

これはselectorで要素を取得して最初の要素を返すAPIなのですが、 この first=True というoptionがKenneth Reitzらしいと非常に感じます。 普通は下のように配列のインデックスとしてアクセスするようにのみAPIを設計するほうが一般的で first=True は醜い、と感じる人も多いと思います。

r.html.find('.foo > .bar')[0]

でも実際に書いてみると、 first=True の使い勝手の良さに気づくのではないでしょうか。実際、自分はこのAPIがとても気に入っています。 このAPIの例はかなり細かいものですが、Kenneth Reitzはこういった 「攻めている」だけど使うとしっくりくるAPIデザイン をすることにおいて天才的だといつも感じます。

requests_htmlでは他にも、1メソッドでHeadless Chromeインストールから実行までしてくれる下のオシャレなAPIも個人的なお気に入りです。

r.html.render() # Headless Chromeでrendering

Kenneth Reitz以外でDeveloper Experienceデザインに長けたエンジニアとして、真っ先に浮かぶのがkerasの作者のFrançois Cholletです。Theano、Tensorflowをわかりやすく楽しく開発できるようにラップしたkerasを開発した彼も間違いなく、Developer Experienceデザインの天才だと思います。

f:id:mergyi:20181201165116p:plain
機械学習/深層学習 市井の賢者から学ぶ(https://www.slideshare.net/yutakashino/ml15m2018-1027)より

最後に

  • Web Framework for Humanなresponderの紹介をして、作者のKenneth ReitzのDXの才能について書きました。
  • 是非responderで楽しいWebApplication開発を体験してみてください。
  • もしかしたら今後のデファクトスタンダードになるかもしれません…!
  • 自分はこれからもKenneth Reitzのfor HumanでDXの高いプロダクトを楽しみにKenneth Reitzを応援していきたいと思います。