「実装で知るasyncio」を聴いてコルーチンを全く分かっていないことに気づいた
本記事は「PyCon JP 2021 Advent Calendar」に5日目に掲載する記事です。
PyConJP2021で「実装で知るasyncio -イベントループの正体とは- 」を拝聴しました。
https://2021.pycon.jp/time-table/?id=272959
正直、「何も分からん」という感想でした。例の成長曲線の「完全に理解した」の次のやつではなく、完全に理解する前の段階です。 しかし、発表中に「もし難しく感じたらラッキー。それこそが PyCon JP に参加する価値です」というお言葉をいただきました。せっかくいただいたお言葉「この発表難しかった」で終わるのはもったいないと思い、この記事を執筆するに至ります。この記事を書きながら、コルーチンとは何かを説明出来るくらい理解することが目的です。
※この記事では、asyncioについてはまったく扱いません。(最後の方で少ししか)
まとめ
- コルーチンとは、プログラムの呼び出し元と呼び出し先を行ったり来たりする仕組み
- Pythonでは、行ったり来たりするのに
generator
を使う - generatorを使ったコルーチンは、
generatorオブジェクト.send()
が呼び出されるまで処理を待つ仕組みである - generatorのコルーチンとは別に
async/await
を使ったnative coroutine
がPythonにはあるが、上述のコルーチンのように行ったり来たりの処理はできない。処理を待つという点ではgeneratorコルーチンと同じ特徴を持つ。
コルーチンとは(全プログラミング言語共通の概念として)
コルーチンは英語で書くとco-routine
です。
ルーチンは、「ルーチンワーク」のルーチンですね。 weblioには「きまりきった手続きや手順、動作など。また、日常の仕事。日課。」と書いています。
例えば、ウェイターの仕事で「ルーチン」を表してみると、下記の3つが決まりきった仕事になります。
注文を取る 料理を運ぶ 食器を下げる
「ルーチン」はコンピュータプログラムの文脈では「特定の処理を実行するための一連の命令群」という意味だそうです。
ウェイターの仕事をPythonで書いてみて、「ルーチン」を表してみます。
python3 >> print("注文を取る") >> print("料理を運ぶ") >> print("食器を下げる")
print文は「この文字列を標準出力してください」という命令です。 ...毎回print文を書くのは面倒ですね。
そこで、関数にまとめて仕事を表してみます。
def work(): print("注文を取る") print("料理を運ぶ") print("食器を下げる")
work()を呼び出したら、出力される文字列はこのようになります。
>> work() 注文を取る 料理を運ぶ 食器を下げる
work()
関数は、ウェイターの仕事を表す「ルーチン」となります。
3つのprint文を書くことは決まりきったことなので、関数にすると楽ですね。
ウェイターの仕事をするために、print文という命令をまとめているので、関数はルーチンと言えます。
さて、コンピュータプログラムには、ルーチンには2種類あります。「サブルーチン」と「コルーチン」です。 この2つの違いはなんでしょう。
先程のwork()
関数はサブルーチンです。
サブルーチンは一度呼び出されたら処理が終了するまで呼び出し元には戻らないルーチンです。
それに対してコルーチンは、呼び出し元と呼び出し先を行ったり来たりするルーチンです。
ウェイターの仕事は、ノンストップで「注文を取る」「料理を運ぶ」「食器を下げる」をするわけではなく、 以下のようになっていると思います。
サブルーチンだと「料理が出来る」を待たずに「料理を運ぶ」が実行されるイメージです。
ウェイターだけでなく、料理人やお客さんと相互のやり取りをしながら仕事を進めるのがコルーチンです。
Pythonのコルーチン
pythonでは、相互のやり取りを表すためにyield
を使います。
ウェイターの仕事だと、下記のようのなイメージです。
def work(): print("仕事開始") x = yield print(x) print("注文を取る") y = yield print(y) print("料理を運ぶ") z = yield print("食器を下げる")
work()
が返すオブジェクトをgenerator
と呼びます。
work()
を呼び出すとこのように文字列が出力されます。
>> w = work() >> w.send(None) # 生成されたばかりのgeneratorには、Noneを渡してsend()を実行しないといけない 仕事開始 >> w.send("コーヒーを頼む") # xに「コーヒーを頼む」という文字列が代入され、次の行が実行される コーヒーを頼む 注文を取る >> w.send("コーヒーが出来上がる") # yに「コーヒーが出来上がる」という文字列が代入され、次の行が実行される コーヒーが出来上がる 料理を運ぶ >> w.send("コーヒーを飲み終わる") # zに「コーヒーを飲み終わる」という文字列が代入され、次の行が実行される。行の終わりになるのでStopIterationという例外が発生する。 コーヒーを飲み終わる 食器を下げる Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
この時、send()
が呼び出されるまでは次の行の処理に移りません。
例えば、①の後にsend()
によってコーヒーが頼まれるまでは注文を取りません。
処理の終わりに到達するので、w.send("コーヒーを飲み終わる")
でStopIteration
という例外が発生しています。
また、send()よりも前の処理によってなされた状態を保っています。
入力された数値の平均値を求める関数を例に見てみます。
def averager(): print("開始") count = 0 sum = 0 while True: sum += yield count += 1 print(sum/count)
先程のウェイターの仕事では仕事を終えたらStopIterationエラーになりましたが、このプログラムでは任意のタイミングでコルーチンを止めるようにしました。
>> a = averager() >> a.send(None) # next(a)でも可 開始 >> a.send(2) # sumは2で、countが1 2.0 >> a.send(3) # sumは5で、countが2 2.5 >>a.close() # generatorオブジェクト.close()でコルーチンを終了させる >>a.send(4) # コルーチンは終了しているのでStopIterationエラーになる Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
この処理では、呼び出し元と呼び出し先を行ったり来たりしている間に、generatorがsumとcountの状態を保っていることが分かります。
前に実行した処理を引き継いだまま、send()
を実行しています。
Pythonのasyncioはコルーチン?
Pythonでは、上記のコルーチンとは別で、native coroutine
と呼ばれるコルーチンがあります。native coroutineはasync/await
を使ったコルーチンです。
PEP492で、上記のgeneratorを使ったコルーチンとは別のとしてnative coroutineが定義されています。
natavie coroutineとgeneratorを使ったコルーチンの違いが分からなかったところ、図解「generator・native coroutine・with」 〜 関心やコードを分離する文法と、処理順序・構造 という記事に解説がありました。
import asyncio import random async def main(): print('first') await asyncio.gather( native_coroutine(1), native_coroutine(2), native_coroutine(3), ) async def native_coroutine(x): await asyncio.sleep( random.random()) print(x) asyncio.run(main())
まず、asyncの付いている関数定義は、generatorと同じように、呼び出しをしても直ちに実行はされない関数になります。generatorの場合はsend()を都度実行するのでしたが、native coroutineの場合はasyncio.run()やasyncio.gather()などによって実行します。 asyncio.runでmain()を実行すると、まずfirstと出力されますが、次のawaitでnative(1),native(2),native(3)の結果が返されるまで処理を待つようになります。 native(1),native(2),native(3)は"同時に"実行されます。その事を模式的に表現したのが三本の矢印たちです。 ところで、このフローを見ると、(カタカナ表記の)コルーチンのような行ったり来たりする構造がありません。 どういうことでしょうか。 実は、await asyncio.gather()をすると、その引数のnative coroutine達の処理が終了するか、またはタイムアウトするまで待ち続けてしまう ので、コルーチンにおける行ったり来たりという処理ができないのです。ですが、Pythonではこれを(native) coroutineと呼んでいます。
図解「generator・native coroutine・with」 〜 関心やコードを分離する文法と、処理順序・構造 より引用
generatorコルーチンがsend()
が呼び出されるまで実行されないように、async/await
もawait
の処理が終わるまで待っているという点では同じようです。
しかし、行ったり来たり出来ない点ではnative coroutine
は、Python以外での文脈で使われる「コルーチン」とは違うようです。
参考
DjangoでPytestやるTips
仕事でDjangoやる時に、参画したての人が最初から知っておいた方が良さそうな事柄がまとまってきたので残しておきます。
pytest-djangoの機能
--no-migrations
これは、テスト用のテーブルを構築するためのオプションです。このオプションをつけると 定義されているモデルを元にテーブルが作成されます。 Djangoではテストを実行する際に、データベースを作り直します。その際、マイグレーションファイルをもとにテーブルを構成します。 マイグレーションファイルの数が多いと、1つ1つ適用するためテストが実行されるまでに時間がかかります。 このオプションをつけると、モデル定義から読み取って一気にテーブルを用意するので、オプションなしに比べてテーブルの作成が早くなります。
--reuse-db
Djangoでは通常テストを実行するたびにデータベースを作り直すところ このオプションをつけると、前回のテストで作成したデータベースを再利用します。 テーブルを作成する時間分、テストの実行時間が早くなります。 ただし、テスト実行前後でモデルの定義を変えているとテストが落ちるので その時はデータベースを作り直さないといけません。
pytestの機能でDjangoで開発する時に知っていると便利なやつ
filterwarnings
pytest.ini
に書く設定です。Django4系でurls.py
に書くurl()
がなくなります。テストを実行すると、url()
を使っている数だけwarningが表示されます。
url()
をpath()
に変えればwarningは解消されますが、たくさんのurl()
をすぐには変えることができない場合、filterwarnings
にRemovedInDjango40Warning
を無視する設定を書けば
warningが表示されなくなります。この設定を書けば、他の4系のdeprecatedエラーも表示されなくなります。
pytest.ini
[pytest] filterwarnings = ignore::django.utils.deprecation.RemovedInDjango40Warning
参考
「 RDRA2.0 ハンドブック」を読んで
要件定義がうまくいかず憤っていた所、一緒に仕事している人から紹介してもらいました。 影響範囲の調査にヌケモレがあって手戻りが多いことが悩みでした。 読んで見た結果、「なんか要件定義うまくいかない」状態から 具体的に何が良くなくて次はどうすべきなのか見えてきました。
https://www.amazon.co.jp/dp/B07STQZFBX/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1
目次
- この本を読む上での前提知識
- オススメの読者
- この本から得た学び
この本を読む上での前提知識
前提知識というか経験になりますが、システム開発(本著でのサンプルがソフトウェアなのでソフト寄りなイメージだけど、ハードにも当てはまるのかな?)における要件定義の経験がないとこの本がピンとこないかもしれません。
オススメの読者
- 要件定義の手法をなんかしら知りたい人
- 要件定義に課題感を感じている人(ふわっとしている、手戻りが多いなど)
この本から得た学び
RDRA2.0 とはどんな手法か
文章ではなく図で表現することと、機能だけでなく要求から定義することが特徴です。
- 図を使って要件を表現する。
- 要求、システムを使う業務での登場人物、外部システム、情報などをアイコンで表現する。
- アイコンで関連するもの同士を線でつなげる。
- 定義するのは、開発する機能だけではない。以下のレイヤーに分けられる
- そのシステム自体が提供する価値。いわゆる「要求」というやつ。
- そのシステムが使われる業務。これはシステム使わない仕事も含めて定義する、
- 業務内でシステムが使われる場面。
- システム内で扱われる情報。情報の項目・その情報の状態(ex: 未予約、予約中)・条件を定義。
アイコン同士の繋がりで表現した図は、各レイヤーのつながりが一度に見れること・つながりによって関連箇所が見えるので影響範囲がすぐ分かるのが良いです。整合性の担保も楽になりそうです。 また、すべての根本である要求につながるので、新しく何か開発する時に「要求を満たすのか?」という問いに集中できそうだと思いました。
本著では、図書館の司書業務を支援するソフトウェアをサンプルに要件定義しています。要件定義によって出来た図と実装のサンプルが公開されています。 実際の図や実装はこちらを見るとイメージしやすいです。 github.com
自分の要件定義の方法とRDRA2.0の比較
新しく開発する機能について文章化する方法をとっていたのですが、良くない点が明らかになりました。 特に影響範囲の調査でヌケモレがあって手戻りが多いのに悩んでいたので、アイコンのつながりをたどるアイデアに目から鱗でした。
開発する機能についてだけ文章化する方法は、影響範囲の調査が個人の能力や知識に左右されがちですが、 図で可視化すると影響範囲の調査でやることが明確(関連する線をたどればいい)なのでそのギャップが解消されそうです。 特に、条件や状態を追加する際の影響範囲の調査が難しいと感じていましたが、条件や状態を表現する項目が使われる箇所もアイコンで表現されているので良いと思いました。
従来 | RDRA2.0 | |
---|---|---|
機能の定義 | 機能についてだけ書く。使われる業務については必要なら書く | 業務だけでなく要求にも言及 |
影響範囲 | 技術者としての勘とその人が持っているシステムに対する知識に頼る | アイコンの繋がりでたどる |
分からなかった箇所
- 「システム価値」レイヤーにある「外部システム」に当てはまるものは何か。SlackなどのチャットツールやEmailは「外部システム」なのか判断出来なかった。ひとまずは、SlackやEmailは他社から提供されているので外部システムとみなして図を書いてみようと思う。
- 自動で行われる定期処理は「システム境界」の「イベント」にあてはまる認識でOK?
「実践Django」を読んで
このたび、「実践Django」を著者の芝田さんからいただきました! ありがとうございます🙏🙏🙏 DjangoでWEBアプリケーションを作りたい方に強くおすすめしたいです! どんな人におすすめか、本書のおすすめポイントを紹介したいと思います。
実践Djangoの見本誌いただきました!@c_bata_ さんの本です。
— みずき@コーヒー駆動Python (@mizzsugar0425) 2021年7月8日
パラ読みしたところ、実践的な内容であることに加え、Django自体の実装についても触れられているところもあり
Djangoで開発するなら手離せない1冊だと思いました!! pic.twitter.com/2RVuGEpJ8i
目次
- この本を読む上での前提知識
- オススメの読者
- オススメポイント
この本を読む上での前提知識
Pythonの基本的な文法とWEBアプリケーションを開発する上での基本的な知識を知っている必要があります。WEBアプリケーションの知識をもっと具体的に言えば、HTTPリクエスト/レスポンス、HTML、データベースでしょうか。 そういった基本的な知識の説明を省略しているため、WEBアプリケーションの開発自体が初めての方はDjango GirlsやDjango公式チュートリアルから始めて、なんとなく理解してそれらの内容をカスタマイズしようとした時点で本書を読むのが良いかもしれません。
オススメの読者
前提知識を踏まえて、下記のような方がおすすめだと思います。
- 他の言語・WEBフレームワークでWEBアプリケーションの開発経験があって、Djangoは初めての方
- 久々にDjangoを書く方。Djangoは2.0で使うのやめちゃった、みたいな
- Django公式チュートリアルの内容は理解できるが、そのカスタマイズや複雑な要件を実現しようとすると詰まってしまう方
オススメポイント
WEB開発の経験がある人でも満足感のある写経
第一章がDjango製WEBアプリケーションの写経なのですが、この章の写経はただの写経ではありません。一般的に、Djangoを始めるときにはDjango GirlsやDjango公式チュートリアルの写経で始まると思います。 これらの写経は、他の言語・WEBフレームワークでWEBアプリケーションの開発経験がある方にとって、踏み込んだ説明がなくて物足りないかもしれません。また、初学者向けが混乱しないように説明を省いている箇所にモヤっとするかもしません。 本書の写経には、そんな物足りなさやモヤっとポイントを充足する説明があります。
例えば、DjangoのモデルAPIにはobjects
があります。初めてDjangoを書く時に「objectsってなんだよ」って思うかもしれません。でも初学者向けのチュートリアルではその説明をあまり見かけません。。しかし、本書の写経の章の中のコラムは、objectsについて突っ込んでくれています。
他にもそういったモヤっとポイントを解消する内容が詰まっており、他の言語・WEBフレームワークでWEBアプリケーションの開発経験がある方には本書の写経をおすすめしたくなりました!
Djangoの複雑な仕組みを図式化してくれている
Djangoがどのような流れで処理をしているのか簡潔な図にされていて、とても価値の高い資料だと思いました…! 特に認証・認可の章は、実務でDjangoを触り始めた頃の自分に教えたいくらいです! 個人開発では公式チュートリアルの知識で認証・認可の機能を作れましたが実務での認証・認可の機能の要件は複雑でカスタマイズを避けられません。 仕組みを理解していれば「こういう仕組みだからここをこう変えれば実現出来るはず」と筋道を立てて実装出来るはずですが、 わからないので動かしながら「こう書けばこうなるのでこういう仕組みのはず」と手探りでしかも答え合わせもないので不安でした。
↑のような悩みを抱えている方には、すぐに本書を手にとっていただきたいです…! 自信を持ってDjangoでの認証・認可を実装する近道だと思います。 本書では、図で仕組みを解説してくれている他に、複数の実装方針とそれらのデメリメを提示してくれていて実践的でした。
Djangoの枠を越えて、WEB開発に必要な普遍的な知識の説明もある
特に、モデルの章のSQLに関する説明が充実していました。どんなSQLが実行されるかを知らなくてもDjangoでは意図したクエリを書けてしまいますが、実践を踏まえてSQLを確認する方法や効率的なクエリを書くための考え方に触れています。EXPLAINによる解析も取り扱っていたり、インデックスの仕組みが図式を踏まえて説明されているのも、Djangoの本なのにこんなにSQLについて書いてくれるのかと驚きました。Djangoだけでなく、他のWEBフレームワークや言語でのWEBアプリケーション開発に使える知識です。この章はWEBアプリケーションを開発する方どなたにとっても価値のある章だと思います。
また、これはWEBに限った話ではないですが、テストの章も特に実践的だと思いました。 より効率的なテストを書くためにモックオブジェクトの使いどころや使いみちも書かれています。 テストに関するテクニックだけでなく、「どこまでテストするか」「何をテストするか」を考えながら解説してくれています。 これらはテストを書く時に未だに悩むので、著者の芝田さんの考えに触れながら読めて大変有り難かったです。
FastAPIでDI(Dependency Injection)したい
最近、FastAPIを使った開発をしたので、そこでやったことを書きます。
この記事は2020年のAdvent Calendarで書く予定の記事でしたが、間に合わず、2020年12月31日に投稿したものです…涙
使用技術
- Python 3.8
- FastAPI 0.61
- gunicorn 20.0.4
- uvicorn 0.12.2
- pydantic 1.7.1
実現したかった構成
依存関係を
view -> domain -> repository
という風に依存するようにしたかったです。 ※domain -> repositoryはこの記事では扱いません。別の記事に書こうかと考えています。
viewにdomainの依存性注入を試みました。
依存性注入のメリットについては、本記事では取り扱いません。気になる方は、こちらの記事が参考になりますのでぜひ読んでみてください。
困ったこと
Python製WEBフレームワークPyramidだと、add_request_method
があります。
add_request_method
を使って下記のような実装を実現できます。こうすることで、view層にdomainを依存させることができます。
この例だと、domainしか依存させていないので、依存させているdomainのみview層で直接呼び出せるようになりました。
domain.py
class Domain: def __init__(self): pass def add(self, a: int, b: int) -> int: return a + b def sub(self, a: int, b: int) -> int: return a - b
wsgi.py
from typing import Final import pyramid.config import pyramid.router import domain def _init_domain(config: pyramid.config.Configurator) -> None: def request_method() -> domain.Domain: return domain.Domain() config.add_request_method(request_method, 'domain', reify=True) def main() -> pyramid.router.Router: """ This function returns a Pyramid WSGI application. """ with pyramid.config.Configurator(settings=settings) as config: config.include('.views.routes') _init_domain(config) return config.make_wsgi_app()
views/__init__.py
import http import json from typing import Any, Dict, Final, cast import pydantic import pyramid class Error(Exception): pass class MissingParameterError(Error): pass class Addiction(pydantic.BaseModel): augend: int addend: int class Subtraction(pydantic.BaseModel): subtrahend: int minuend: int def _take_json(request: pyramid.request.Request) -> Dict[str, Any]: try: return cast(Dict[str, Any], request.json) except json.JSONDecodeError: # Request BodyがJSONでない場合に起きます。 raise MissingParameterError() def add( request: pyramid.request.Request) -> pyramid.response.Response: parameter: Final = Addiction.parse_obj(_take_json(request)) result: Final = request.domain.add(parameter.augend, parameter.addend) return pyramid.response.Response( status=http.HTTPStatus.OK, json={'result': result}, ) def sub( request: pyramid.request.Request) -> pyramid.response.Response: parameter: Final = Subtraction.parse_obj(_take_json(request)) result: Final = request.domain.sub(parameter.subtrahend, parameter.minuend) return pyramid.response.Response( status=http.HTTPStatus.OK, json={'result': result}, )
views/routes.py
import pyramid.config import views def includeme(config: pyramid.config.Configurator) -> None: for pattern, view in [ ('/add', views.add), ('/sub', views.sub), ]: config.add_route(pattern, pattern, request_method='POST') config.add_view(view, route_name=pattern, request_method='POST')
起動コマンド
poetry run waitress-serve --call wsgi.main
FastAPIでこれ相当のことをする方法を探すのに苦戦しました。それで、今回の記事を書こうと決めました。
Domainオブジェクト→view層に依存を実現するためのDepends
View層に直接依存するものは、FastAPIの機能の一つ、Dependsを使いました。
Dependencies - First Steps - FastAPI
Dependsは、DIを実現するために、FastAPIが用意してくれている仕組みです。
views.py
from typing import Final import fastapi import fastapi.responses import pydantic import domain # 内容は上記のdomain/__init__.pyと同じです。 def _domain_factory() -> domain.Domain: return domain.Domain() class Addiction(pydantic.BaseModel): augend: int addend: int def add( addiction: Addiction, # FastAPIでは、request bodyとなるオブジェクトを引数で受け取ります。 domain: domain.Domain = fastapi.Depends(_domain_factory), # domain.Domainを返す関数をDependsに渡します。 ) -> fastapi.responses.JSONResponse: result: Final = domain.add(addiction.augend, addiction.addend) return fastapi.responses.JSONResponse({'result': result})
routes.py
import fastapi import views def add_routes(app: fastapi.FastAPI) -> None: app.add_api_route( "/add", views.add, methods=["POST"], ) app.add_api_route( "/sub", views.sub, methods=["POST"], )
wsgi.py
from typing import Final import fastapi import routing def main() -> fastapi.FastAPI: app: Final = fastapi.FastAPI() routing.add_routes(app) return app
起動コマンド
gunicorn 'app.wsgi:main()' -k uvicorn.workers.UvicornWorker
view層の関数の引数として、依存させたいオブジェクトを渡します。 引数の名前は、view層の関数内で利用したい変数名にします。
注意しないといけないのは、Dependsにdomain.Domainを返す関数を渡すことです。(理由は分かりませんが、Domainに引数となる値を渡す場合、関数を渡さないと関数定義時の値が評価されるからでしょうか? 実行時の値が評価されないと、日時オブジェクトなどは意図しない値になりますからね)
FastAPIでもPyramidのように、view -> domainの依存性注入を実現できることが分かりました。
実際のプロジェクトでは domain -> repositoryの依存性注入もしましたが、長くなってしまうので別の記事に分けて書こうかと思います。
PyConJP2020で登壇させていただきました。
ちょっと遅くなってしまいましたが…
PyConJP2020にて、「unittest.mockを使ってテストを書こう 〜モックオブジェクトを使ってより効率的で安定したテストに〜」という題で発表させていただきました。
長々とした45分間の発表で、スライドの枚数は合計で83枚になりました。
始まりは2019年10月
このトークは今回始めてではなく、最初に発表したのは2019年10月のStapyです。この時は15分間の発表でした。その次に、2020年2月にPyCon mini Shizuoka(以下、Shizuoka)で発表させていただきました。Shizuokaでは30分でした。発表のたびに、発表の方法(スライドの見た目にしても話し方にしても)や発表内容をブラッシュアップ出来ました。この2回がなければ今回のチャンスもなかったので、感謝しています。
CfP
CfPはShizuokaに出したものをベースに書き直しました。もともと単体テスト自体の話は少なくしてモックの話を多めに取るつもりだったので内容はちょっと変えています。
CfPはモックの話を30分枠と45分枠出しました。
45分枠に関しては、Shizuokaの時の内容に15分分新しい内容を追加しました。
15分枠でtyping.Literalの話とloggingの話も提出しました。この2つはまだどこにも発表していない内容なので、その内どこかで供養したいなと思います。
PyConJP2020のWEBページ見たらCfPそのまま載っていました。私が書いたCfPはここから確認出来ます。
https://pycon.jp/2020/timetable/?id=203572
レビューは、正しい日本語を使うことにこだわりがあり、私よりもPythonに詳しい人にお願いしました。
以下の点を注意して見てもらいました。
- 技術的に間違ったことを書いていないか
- 誤解を招くような表現をしていないか
- トークの目的を発表内容で達成できそうか
スライドのレビュー
これも、正しい日本語を使うことにこだわりがあり、私よりもPythonに詳しい人にお願いしました。
以下の点を注意して見てもらいました。
- 技術的に間違ったことを書いていないか
- 誤解を招くような表現をしていないか
ありがたいことに、スライドレビュー時に「これってどういうことですか?」という質問があったので、発表後の質疑応答で想定される質問に対する回答を用意出来ました。しかもPythonのコアな部分だったので、調査が大変でした。スライドレビューは遅くても本番の2週間前にお願いするのが良さそうです。
私もスライドレビューする機会があったら、質問しようと思います。
また、Shizuokaの時もやったのですが、レビュワーさんのスケジュールを考慮して、「何回レビューしていただけるか」を予め決めるのが良いと思います。例えば、最初の1回と指摘事項を修正してからの1回の合計2回とか。
45分枠の発表の練習方法
45分いきなりぶっ通しでやるのはすごく気が滅入りました。
過去の発表を見ると、45分枠は途中で休憩が入るものが多かったので、休憩前と後に分けて練習しました。休憩前だけor休憩後だけに慣れたところで45分通しでやるようにしました。45分通しを練習するのは腰が重いですが、この方法は我ながら良かったと思います。
水飲むタイミングも練習したのですが、結局本番喉がカラカラになったので、意図しないタイミングで摂取しました(^^;
リハーサル
これやったとやらなかったでは全然違ったと思います。本番前の待機の方法から質疑応答までのZOOMでの操作手順をリハーサルしました。1回練習したし、登壇者用のZOOM操作マニュアルがあるので
ZOOMへの不安がない状態で本番に挑むことが出来ました。
登壇者のスケジュールに合うよう、複数日練習日を設けてくれて、スタッフの方に感謝です。すごく大変だったと思います… お陰様で安心して本番に挑めました。ありがとうございました。
おまけ
達成しました🎉
nikkieさんのこのツイートがなければPyConJPを視野に入れていませんでした。本当に「自分にPyConJP登壇は無理だよ」と思っていましたので… ありがとうございました。
今回の #pyconjp でこれ達成しました🎉(始まりは2019年10月の #stapy だったのです)@ftnext さん、@info_stapy さん、ありがとうございます🙏https://t.co/opDMKk2XDf
— みずき@コーヒー駆動Python (@mizzsugar0425) August 29, 2020
【読書録】リーン・スタートアップ
技術書ではないですが、技術的なことを含め意思決定の判断軸になる話でした。
リーン・スタートアップ | エリック・リース, 伊藤 穣一(MITメディアラボ所長), 井口 耕二 |本 | 通販 | Amazon
読んでみようとした経緯
- 仕事で感動させた人が2人もこの本の考えを仕事中に引用してたので、「これは読まないと!」と思ったこと。
概要
スタートアップで持続可能な事業を育てるための考え方や物事の進め方を紹介します。 この本での「スタートアップ」の定義は、組織の規模や立ち上げ時期に関係なく「とてもつなく不確実な状態で新しい製品やサービスを作り出さなければならない人的組織」(「リーン・スタートアップ」から引用)です。 不確実であるがゆえに計画通りにいかないけれども行きあたりばったりではうまくいかない状況でどう立ち向かうのか。 キーワードは「検証」と「学習」です。不確実な状況で創る新しい事業をどうやって持続可能にするかを「検証」によって「学習」します。
なぜ検証なのか?
従来のビジネスではウォーターフォール形式で物事を進めていました。ウォーターフォールが成り立つのは「これを実行したらこういう結果になる」と見えているからです。しかし、不確実な状況では予想外の出来事が多々起こり、最初に立てた計画通りに進みません。
ここで、ビジネスを前進させるための考え方として「検証」と「学習」が登場します。
スタートアップでは製品を世の中に出しても、上手く行くかどうか分かりません。ですから、製品を出すのがゴールではなく、出してみて顧客が同反応するかを実験する「検証」として捉えます。
実験で学んだことを踏まえて製品を改良するかそのまま進むか決めます。
この本では、検証と学習のサイクルを早く回すことが大事だといいます。
MVPとは何か?
MVPは製品デザインや技術的な問題を解決するためのものではない。基礎となる事業仮説を検証するためのものなのだ。
また、MVPに関してこんなツイートを見つけました。
MVPはここじゃなくて、ここ! pic.twitter.com/Wz74rKUOcs
— ken (@kecbon) July 1, 2020
MVPではデザインや技術的な問題を解決しないものの、検証対象となる価値を提供できるものでないといけないと学びました。
「リリースまでに最低限揃えないといけないもの」と今まで捉えていたのですが、「なぜリリースまでに揃えないといけないのか」のところを説明され、MVPで揃えるべき機能を判断する考え方が変わりました。
どういう仮設を検証したいか、検証のためにどんな価値を提供するかを考え、それをMVPの機能で実証出来るかを見ていきたいです。
正しく方向転換するために
検証したら、その結果を見て、成長するためには次何をするか決めなくてはなりません。その際に、正しく方向転換出来ないと、顧客が求めている価値を提供出来ず事業は停滞してしまいます。
正しい判断をするための基準として、「革新会計」という考え方が提唱されています。
売上や商談回数だけでなく、サイトの新規登録数・ダウンロード数・ログイン数などユーザーの行動を細かく分解してその結果を見ます。変更のたびにそれらの結果を見て
なぜその数字が改善・改悪されたのかを追う結果に対する要因が明確になり、舵取りの方向性も明確になります。
行動を促すような検証と行動を追うという考え方は、闇雲に何か変えようとなりにくく良いなと思いました。
スタートアップはスピードだけでなく開発の持続可能性も必要では?
エンジニアの仕事についてこんなことが書かれていました。
これに対してリーン・スタートアップの場合、たくさんのモノを効率的に作ることが目的ではない。持続可能な事業の構築方法をできるかぎり短時間で学ぶのが目的だ。
変化していく事業側のニーズに製品を対応させるのがエンジニアの仕事であり、事業に関する決定が正しいか否かにエンジニアは関与しないのだ。
これらを読んで、WEB+DB PRESS 113号の「体験ドメイン駆動設計」の一節を思い出しました。
私たちはいつごろから片道ロケットの打ち上げ競争をやめて、飛行機を安定的に運行させたいと願うようになったのでしょうか。 (中略) システム開発を取り巻く環境の変化は、片道ロケットに乗った開発者たちが返ってこなかったというのが大きな理由になっているのではないかと考えます。 プログラムは動かすだけなら簡単で、しかし動かし続けるのは難しい代物です。開発速度を明大に形作られたソフトウェアはそのあとの運用について考慮されておらず、ビジネスの変化のたびにつぎはぎだあらけの応急処置が繰り返されて、複雑怪奇な進化を遂げます。
たしかに、保守運用のために回すよりもガンガン作った方が今目の前では利益を生むので、現時点での利益のためにこういったことに時間を費やすべきではないという意見もあります。
でも、持続させることを目的とするならば、持続可能なシステムを構築する手段としてテストやアーキテクチャに目を向けるべきではないかと思いました。「スタートアップだから」こういったことを考慮しないというのは、スタートアップの目的から考えると筋が合わないので、自分もここらへんの学習をより一層がんばります。