著書『実践Django Pythonによる本格Webアプリケーション開発』が本日から発売されます。

Twitterでは以前から告知を行っていましたが、著書『実践Django Pythonによる本格Webアプリケーション開発』が本日から発売になります。 購入を検討している方は、Amazonもしくはお近くの書店で探してみてください(エキスパートPythonプログラミング 改訂3版も今月末発売予定です)。

f:id:nwpct1:20210717141541j:plain

初の著書であり単著ということもあり大変なこともありましたが、満足の行く一冊が出来上がってホッとしています。

追記: 単行本派データベースカテゴリでベストセラー1位、Kindle版もNPONGOカテゴリでベストセラー1位になっていました。購入してくださった方、Twitterで拡散してくださった方ありがとうございました!

本書の対象読者

本書の対象読者やおすすめの読み方については、レビューをお手伝いいただいた中川さんが素晴らしい記事を書いてくださいました。 本書がアピールしたいことや、どういうふうに読んでほしいかをまさに汲み取ってまとめていただいているのでぜひこちらを先に読んでみてください。

shinyorke.hatenablog.com

"Djangoをやる方はもちろん, Djangoを抜きにしてもWebアプリケーション開発をされる方にめちゃくちゃオススメしたい!" と思いました, レビューさせてもらったときからすごく良かったんですよ, それぐらい興奮しました ※1

※1: レビューしながら, 「そうだよ!そこが大事なのよ!!」とか家で一人声を出しながら食い気味に読ませていただきました苦笑. 発売前に原稿が読めて幸せでした(感謝)

中川さんもこうおっしゃってくださっていて嬉しいです。

例えばデータベースのインデックスチューニングやHTTPまわりの基礎知識、ユニットテストを書くときの考え方、認証認可まわりの注意点などは、Djangoに限らず必要な知識だと思います。こういったフレームワークに依存しない知識は、Django公式ドキュメントで扱っていないことも多く、初学者の方は勉強のきっかけを掴めないことも多いと思います。

でも、むしろそういったところにWeb開発者として長く役に立つ知識が詰まっているとも思っています。 他のPython Web関連の書籍に比べて高度な内容も多く扱っていると思いますが、詳しく解説しすぎるとどんどん難しくなる話もちょうどよいボリュームでまとめるよう心がけて執筆したので、ぜひ多くの方の手にとっていただきたいです。

サンプルコードや目次

サンプルコードや目次はこちらのGitHubリポジトリで公開しています。

github.com

Amazonレビューのお願い

すでに本書を読んでくださったという方もいらっしゃると思いますが、もしよければAmazonでレビューをつけていただけると嬉しいです。

(素晴らしい書籍であるにも関わらず、残念なレビューがついてしまう悲しい話 も見たことがあるので、どんな評価であれできるだけ多くの方がAmazonレビューをつけていただけると嬉しいです...!)

最後に

Djangoを触り始めてもう7年くらい経ちますが、今見てもこれだけ多くの機能を見通しよく設計し使いやすく提供できていることに驚きます。 たまにパッチを書いてコア開発者の方々からレビューをもらったときには、彼らの設計能力の高さやデータベースまわりの知識の深さに圧倒されます。 Djangoに限らないとは思いますが、世界中のWebシステムの開発に使われるフレームワークを開発している人たちのさまざまなドメインに対する知識の深さは頭がいくつも抜けていて、自分はいまだに全然届かないなと思いました。 彼らがDjangoの開発を続けてくれていることにとても感謝しています。

GitHub SponsorsこちらのページDjango Software Foundationへ寄付ができます (自分はGitHub Sponsors経由にしています)。 本書をきっかけに少しでもDjangoの採用事例が増え、寄付をする人が増えたら嬉しいなと思っています。ぜひ検討してみてください。

最近の登壇資料と出版予定の書籍、インタビュー記事

最近は勉強会での登壇や書籍の出版などアウトプットが色々重なりました (昨年は一度もプロポーザルを書かず登壇依頼もなかったので随分増えました)。 そのたびにツイートもしてきましたが、ほとんど流れてしまって少しもったいない気がしたのでブログにまとめておこうと思います。

登壇資料

PyData.Tokyo Meetup #23「サイバーエージェントにおけるMLOpsに関する取り組み」

運営の方からお声がけいただき、MLOpsについてお話させていただきました。あまりMLのコミュニティで登壇してこなかったのと、参加者が500人以上いて緊張していたので、資料も気合を入れて準備しました。

登壇資料の中では過去一番拡散され、Twitterでの反応もよく久しぶりに満足のいく発表ができたかなと思います。動画も公開されているのでよければ見てみてください。


www.youtube.com

Optuna Meetup #1「CMA-ESサンプラーによるハイパーパラメータ最適化」

自分もコミッターを務めているハイパーパラメータ最適化フレームワーク「Optuna」の勉強会で、CMA-ESという最適化アルゴリズムの話をしてきました。 こちらも450人程度参加者が集まっていて、Optunaの人気の高さに驚きました。動画は公開されていません。スライド資料だけ読んでもよくわからないかもしれませんが、以前Optunaのブログで記事も書いているので興味のある方はこちら読んでみてください。

World Plone Day「Web パネルディスカッション(Python Webと非同期)」

PythonCMSPlone」の国際カンファレンスであるWorld Plone Dayのパネルディスカッションで、寺田(@terapyon)さんにお声がけいただきパネラーをやってきました。 といってもPloneを使ったことがないので普通にPython Webフレームワークの非同期対応について話してきました。

一緒にパネルで話した @hirokiky さんや @aodagさんも含め全員がWSGIフレームワークを書いたりしたことがあるぐらいには、Webフレームワークの設計や実装を理解しているメンバーなのでやや踏み込んだ話が多かったと思います。 自分自身色々キャッチアップできてとても勉強になりました。個人的に印象に残った話だけメモしておくと:

  • SQLAlchemyの非同期対応が最近入った。
    • メソッド呼び出しだけでなくプロパティアクセスで暗黙的にSQLを発行したりするので簡単ではない。
    • DjangoよりSQLAlchemyのほうがそういう暗黙的な挙動が多いらしく、大変そうという話がでました (今思うとDjangoもjoin忘れると普通に起きるので同じぐらいかも?という気もします)
  • DBドライバーの非同期対応がなかなかまだ厳しいという話
    • DB-APIの非同期インターフェイスがまだ存在せず、各種DBドライバーは独自でインターフェイスを決めて実装している。
    • asyncpgとaiopgは微妙にシグネチャが違っていて、同じコードでは動かない。
    • 現状まともに使えそうな非同期のDBドライバーはasyncpgぐらい。
    • SQLite3とかはPythonの標準ライブラリに追加されるといいね(ついでに非同期版DB-API決まるといいね)と思った。


www.youtube.com

動画はちょっと長いのですが、前半45分がPythonと非同期に関する話になります。

CA BASE NEXT「サイバーエージェントにおけるMLOpsに関する取り組み」

サイバーエージェントの若手世代による社外向け技術カンファレンスである『CA BASE NEXT』でも話してきました。内容はPyData Tokyoで話したものと同じですが、時間がこちらは30分しかなかったため、GeventやWebSocketまわりの話を削ってお話しています。

ca-base-next.cyberagent.co.jp

書籍

実践Django Pythonによる本格Webアプリケーション開発(翔泳社:7月19日発売)

PythonのWebフレームワーク「Django」の書籍です。今月19日発売になります。初の単著書ということもあり出版にあたって色々想いもあるのでこちらについてはまた発売日にブログを書こうと思います。

エキスパートPythonプログラミング改訂3版(KADOKAWA:7月30日発売)

エキスパートPythonプログラミングの改訂3版です。今月30日発売になります。内容がアップデートされただけでなく新しく追加された章もあるので改訂2版をお持ちの方もぜひ手にとってみてください。

インタビュー記事

今年3月に会社のFEATUReSというメディアから、2本のインタビュー記事が出ました。 昨年も個人インタビュー記事を書いていただけたのですが、こんなに何度も取り上げていただけることはあまりないので嬉しいです。

PythonOSS開発とフレームワーク解析の日々はやがて世界5位の研究成果につながる FEATUReS

サイバーエージェント社内にはDeveloper Expertsという肩書きを持つエンジニアが8名在籍しています。自分は昨年からPython領域のDeveloper Expertsに任命されていて、そのときの就任インタビュー記事が今年3月に公開されました。

www.cyberagent.co.jp

生み出したのは、世界レベルの実績。研究/OSS開発で切り開くハイパーパラメータ最適化の未来 - FEATUReS

現在所属しているハイパーパラメータ最適化チームのインタビュー記事です。現在は同期のリサーチャーである野村と2人で活動しているのですが、ここ1年は色々とまとまった成果が出てきました。研究者も開発者もいい人がいたらぜひ手伝っていただきたいなと思っているので、もしこの辺興味がある方いたらお声がけいただけると嬉しいです。

www.cyberagent.co.jp

おわりに

ここ2-3ヶ月は、登壇資料ばかり書いていてメインタスクがおろそかになってしまった気がします。昨年いろいろ頑張って成果がでた貯金のおかげで、昨年末・今年の4月と全社表彰でベストエンジニア賞をいただけたりもしましたが、もう貯金も切れてしまったと思うのでここから挽回したいなと思います。

Pyppeteer(with headless Chromium) + GitHub Actionsでoptuna-dashboardの継続的E2Eテスト

以前 optuna-dashboard というWebツールを開発・公開しました。 もともと Goptuna のために実装したReact.js + TypeScript製のSPAのWebツールでしたが、Optunaでも使えるようにしたところ、周りでも使ってるよという声をいただくことが増えてきて、公式に利用が推奨されるようになりました。

基本的に1人で雑に開発をしていたのですが、バグを出すといろんな方に迷惑をかけそうなので、もうすこしちゃんとテストまわりを整備することにしました。 Python (Bottle) 製のAPIサーバーのユニットテストはもともと結構ちゃんと書いていたのですが、問題はフロントエンドのコードのテストです。 自分がフロントエンドのテストテクニックに詳しくないというのもあるのですが、Plotly.jsによるグラフ描画が主な処理になるため、ユニットテストで保証できる振る舞いはそれほど多くありません。またグラフの描画に必要な情報もやや複雑で、OptunaのFrozenTrialという試行情報を各テストケースで用意するのは大変です。

そこで今回はPyppeteerを使ってVisual regression testingのようなアプローチ *1をとったのですが、結構便利でやってよかったなと思ったので記事に残しておきます。

Pyppeteerを使ってさまざまな目的関数に対するDashboardの表示を確認する

OptunaのFrozenTrialに相当する情報を手動で用意するのは面倒なので、実際にさまざまな目的関数をOptunaで評価して、その評価結果の入ったInMemoryStorageをもとにDashboardを起動、Pyppeteerでアクセスします。コードを簡略化するとざっくり↓のような感じです (全体のコードは こちら)。

import asyncio
import threading
import time
import optuna
import os

from optuna_dashboard.app import create_app
from pyppeteer import launch
from typing import List, Tuple
from wsgiref.simple_server import make_server

host = "127.0.0.1"
port = 8080
output_dir = "tmp"


def create_optuna_storage() -> optuna.storages.InMemoryStorage:
    storage = optuna.storages.InMemoryStorage()

    # Single-objective study
    study = optuna.create_study(study_name="single-objective", storage=storage)

    def objective_single(trial: optuna.Trial) -> float:
        x1 = trial.suggest_float("x1", 0, 10)
        x2 = trial.suggest_float("x2", 0, 10)
        return (x1 - 2) ** 2 + (x2 - 5) ** 2

    study.optimize(objective_single, n_trials=100)

    # Pruning with no intermediate values
    study = optuna.create_study(
        study_name="binh-korn-function-with-constraints", storage=storage
    )

    def objective_prune_with_no_trials(trial: optuna.Trial) -> float:
        x = trial.suggest_float("x", -15, 30)
        y = trial.suggest_float("y", -15, 30)
        v = x ** 2 + y ** 2
        if v > 100:
            raise optuna.TrialPruned()
        return v

    study.optimize(objective_prune_with_no_trials, n_trials=100)

    # No trials
    optuna.create_study(study_name="no trials", storage=storage)
    return storage


async def take_screenshots(study_ids: List[int]) -> None:
    browser = await launch()
    page = await browser.newPage()
    await page.setViewport({"width": 1200, "height": 3000})

    for study_id in study_ids:
        await page.goto(f"http://{host}:{port}/dashboard/studies/{study_id}")
        time.sleep(5)
        await page.screenshot(
            {"path": os.path.join(output_dir, f"study-{study_id}.png")}
        )
    await browser.close()


def main() -> None:
    os.makedirs(output_dir, exist_ok=True)

    storage = create_optuna_storage()
    app = create_app(storage)
    httpd = make_server(host, port, app)
    thread = threading.Thread(target=httpd.serve_forever)
    thread.start()

    study_ids = [s._study_id for s in storage.get_all_study_summaries()]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(take_screenshots(study_ids))

    httpd.shutdown()
    httpd.server_close()
    thread.join()


if __name__ == "__main__":
    main()

このプログラムの流れは次のような感じです。

  1. Optunaで実際にOptunaで様々な目的関数の最適化を回す (create_optuna_storage() 関数)。
  2. InMemoryStorageを使ってoptuna-dashboardAPIサーバーを別スレッドで立ち上げ
  3. Pyppeteerを使ってheadless Chromiumで各ページにアクセスして、スクリーンショットを撮影
  4. Pyppeteerの処理が一通り終われば、WSGIRefのサーバーを終了

実際にOptunaで最適化を回し、そのStorage情報を使ってJSON APIサーバーを立ち上げているので、FrozenTrialのfixtureを手動で頑張って用意する必要はありません。実行すると↓の画像のようにスクリーンショットが生成されるため、様々な目的関数に対する表示結果を簡単に確認できるようになりました。

f:id:nwpct1:20210411201847p:plain

実際結構便利で、表示中にクラッシュしていたりすると真っ白のスクリーンショットが生成されるのですぐに気が付きますし、質的変数とかの表示がちゃんとできてるかみたいなユニットテストでの確認が難しい問題も手軽に目視で確認できます。

GitHub Actionsで動かす。

せっかくなのでGitHub Actionsで動かしてみます。 撮影したスクリーンショットGitHubのコメント欄に貼り付けるようにしようかと思ったのですが、結構いろんな目的関数で評価していることもあり画像の枚数が多く全部コメント欄に貼り付けるのはやや大変です。 将来的には画像をGIFとかにまとめて一気に見れるようにするのもいいかなと思いましたが、今回はもう少し簡易的なチェックのみを実行するようにします。

async def contains_study_name(page: Page, study_name: str) -> bool:
    h6_elements = await page.querySelectorAll("h6")
    for element in h6_elements:
        title = await page.evaluate("(element) => element.textContent", element)
        if title == study_name:
            return True
    return False


async def take_screenshots(storage: optuna.storages.BaseStorage) -> List[str]:
    validation_errors: List[str] = []

    browser = await launch()
    page = await browser.newPage()
    await page.setViewport({"width": args.width, "height": args.height})

    study_ids = {s._study_id: s.study_name for s in storage.get_all_study_summaries()}
    for study_id, study_name in study_ids.items():
        await page.goto(f"http://{args.host}:{args.port}/dashboard/studies/{study_id}")
        time.sleep(args.sleep)

        if not args.skip_screenshot:
            await page.screenshot(
                {"path": os.path.join(args.output_dir, f"study-{study_name}.png")}
            )

        is_crashed = not await contains_study_name(page, study_name)
        if is_crashed:
            validation_errors.append(
                f"Page is crashed at study_name='{study_name}' (id={study_id})"
            )

    await browser.close()
    return validation_errors

チェックしているのは、study_nameを含むh6タグが存在するかどうかです。 もしアプリケーションがクラッシュして真っ白になっていた場合、このチェックに引っかかるので最低限クラッシュしていないということはチェックできるようになりました。これをGitHub Actionsで動かします。

name: integration-tests
on:
  pull_request:
    branches:
      - main
    paths:
      - '.github/workflows/integration-tests.yml'
      - '**.py'
      - '**.ts'
      - '**.tsx'
      - 'package.json'
      - 'package-lock.json'
      - 'tsconfig.json'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Set up Node v14
        uses: actions/setup-node@v2-beta
        with:
          node-version: '14'
      - run: npm install
      - run: npm run build:dev

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.x'
          architecture: x64
      - name: Install dependencies
        run: |
          python -m pip install --progress-bar off --upgrade pip setuptools
          pip install --progress-bar off .
          pip install --progress-bar off pyppeteer
      - name: Cache headless chromium
        id: cache-chromium
        uses: actions/cache@v2
        with:
          path: ./local-chromium
          key: chromium
      - run: pyppeteer-install
        if: steps.cache-chromium.outputs.cache-hit != 'true'
        env:
          PYPPETEER_HOME: ./local-chromium

      - run: python visual_regression_test.py --skip-screenshot --sleep 3
        env:
          PYPPETEER_HOME: ./local-chromium

GitHub Actionsの設定はこんな感じで、思ったよりもスッキリかけました。特にハマりどころもなくスッと動きました。

  1. Nodeや依存ライブラリをインストールして、JSのコードをビルド
  2. Pythonや依存ライブラリをインストールして、optuna-dashboardを動かせるようにする。
  3. actions/cache を使ってheadless Chromiumのバイナリ (~150MB) をキャッシュ (PYPPETEER_HOME 環境変数でキャッシュ先のディレクトリを変更できます)。
  4. Visual regression testingを実行。スクリーンショットの撮影は必要ないので、スキップしています。またページ遷移してからchromiumの描画が完了するまで少し待つ必要があるので、各ページ3秒待ってからチェックを実行しています。

まとめ

Pyppeteerを使って手軽にVisual regression testが走らせられるようになり、動作確認がだいぶ楽になりました。 Google Summer of Codeのプロジェクトテーマになっていることもあり、先にこの辺を整備しておけてよかったかなと思います。 1つ気になるのはPyppeteerの開発が活発ではなさそうな点ですが、かなり便利なツールだったのでまた動かなくなることが増えてきたら自分も開発手伝いたいなと思います。

*1:開発のなかで見た目に変化が入ることは多いため、なにか画像のdiffをとるようなSnapshot testing的なことはやっていません。あまりこの辺の用語の厳密な定義がよくわからなかったので「のような」とぼかしました