nikkie-ftnextの日記

イベントレポートや読書メモを発信

「あってもなくても同じなら捨てる」という片付けの考え方が(こんまりメソッドと)プログラミングに通じていました

はじめに

生まれたときめき。あの日から世界は、変わり始めたんだ! nikkieです。
片付けについて自分のスタンスを人に説明する機会がありました。
それをきっかけに脳内で技術ネタもそれ以外も繋がったので、アウトプットしてみました。

目次

片付けのスタンス:あってもなくても同じなら捨てる

新卒入社した会社の社長が「あってもなくても同じなら捨てる」を徹底していた影響で、私もこのスタンスです。

「あってもなくても同じなら捨てる」を貫くのは大変で、「後で使うかもしれないから取っておこう」という道が絶えず誘惑してきます。
特にプライベートは念のため取ってあるものが多く(片付け後回しにしがちなんです🙈)、年末は「取ってあったけど結局使ってないよね」というものを発掘しながら捨てていました。

片づけと言ったらこんまり

私のこんまりメソッドの認識は「ときめかなかったら捨てる」というものです。
海外ではkondo, konmariが片付けの意で使われているそうですね1

年末の発掘では「取ってあったけど結局使ってない」ものは「ときめきもしないから」とガンガン捨てました。

言い換えるなら、オッカムの剃刀

「あってもなくても同じなら捨てる」は、「オッカムの剃刀」にも通じると思います。

Wikipediaによると

「ある事柄を説明するためには、必要以上に多くを仮定するべきでない」とする指針

私の理解ですが、

  • XとYを仮定して、Aが導ける
  • Xを仮定して、Aが導ける

この場合、後者の方が仮定が最小限なので、好ましいことになります。

仮定Yはあってもなくても(Aが導けるという点では)同じなので、捨てられるわけですね2

さらに関連してKISSの原則

Keep It Simple, Stupid または Keep it Short and Simple

オッカムの剃刀とKISSの原則の関連は『プリンシプル・オブ・プログラミング』より。

Wikipedia によると

その意味するところは、設計の単純性(簡潔性)は成功への鍵だということと、不必要な複雑性は避けるべきだ、ということである。

例えば人に何かを説明するとき、仮定が多くなると説明は複雑になっていきますよね。
KISSの原則を適用すると、オッカムの剃刀を指針にするときと同様に、仮定は少なく、説明は単純・簡潔になると思います。

先人もシンプルさをかく語りき

KISSの原則で言及されるシンプルさは、『プログラマが知るべき97のこと』にもエッセイがありますね。

美しいコードとは、突き詰めれば、シンプルなコードのことです。システムを構成する各部分が全てシンプルで、個々の部分が担う責務も最小限に抑えられていて、部分どうしの関連もシンプル、そんなコードです

「こんなコード、私も書けるようになりたい!」とときめきます。

コードはシンプルなものであるべきです。変数や関数、宣言といった構成要素はできる限り減らすべきです。余分な行、余分な変数…、ともかく余分なものが少しでもあれば、即座に消すようにします。残るべきは、アルゴリズムを完成させ、必要な演算をすべて処理するための、必要最小限の要素だけです。

使わなくなった変数やモジュールが見つかったら「あってもなくても同じ」でもあるので、すぐ消しますね。

Pythonでキーワード引数の渡し方の好み

※これは好みの話です。

Pythonでは関数の引数はいくつかの指定方法が許されています。
(※ドキュメントの言葉を使うと、位置またはキーワード引数の話です)

def parrot(voltage, action='voom'):
    pass
parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments

私は位置引数voltageをあえてvoltage=1000と指定するのを好みません。
「キーワード引数として渡した方があとでコードを読む人にとって分かりやすい」という意見も聞きますし、チームで書く中で取られた選択だとしたら納得感もあります。
ですが、私としては「この位置の引数が何を受け取るかはドキュメントを見れば分かるから、わざわざ書く必要はない」という考えです。
この考え方にも「あってもなくても同じなら捨てる」が現れているかもと今回気づきました。

終わりに

「あってもなくても同じなら捨てる」というスタンスがプログラミングにも通じるという気付きがあったのでまとめました。

P.S. 人の気持ちは例外!

「あってもなくても同じなら捨てる」は人の気持ちには適用されないようです。
その例が『四月は君の嘘』。
主人公 公生は、あってもなくても同じならある方を選んでいます!3

いてもいなくても一緒なら
一緒にいるよ

片付けとは評価軸が異なるってことですね😜


  1. 「こんまり」が米国でブレイク!片付け番組がNetflixで大ウケの理由 | 海外セレブウォッチャーさかいもゆるの セレブ胸キュン☆通信 | mi-mollet(ミモレ) | 明日の私へ、小さな一歩!(1/2) より。ときめきは海外では「Spark joy」だそうです

  2. オッカムの剃刀」は機械学習の分野では、精度指標に対して同程度のスコアの2つのモデルがあるとき、パラメタ数の少ない単純な方を選ぶべきという指針としても聞きますね。ref: Occamの剃刀 - 機械学習の「朱鷺の杜Wiki」

  3. 「あってもなくても同じなら捨てる」立場からすると、公生の選択は不合理なのですが、不合理ゆえに愛が示されていることになるのかなと(そして椿は愛として受け取ったのだと)思いました ref:『世界は贈与でできている』(4章)(この本についてもまたの機会にアウトプットしたいです)

Sphinxでクロスリファレンスを機能させるためには、includeするファイルをrst以外の拡張子にすべきと身をもって学びました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
技術同人誌執筆以来使っているドキュメンテーションツールSphinxで、includeするファイルはrst以外の拡張子にするべきということを知りました。

Tips: you should use other extensions for included documents (cf .txt).

目次

要するに

Sphinxにはrefnumrefを使ったクロスリファレンスの仕組み1があります。
セクションの直前にラベルを付けて、それを参照できます(ドキュメントにある例)。

.. _my-reference-label:

Section to cross-reference
--------------------------

This is the text of the section.

It refers to the section itself, see :ref:`my-reference-label`.

これを使ったところ、PDFのビルドに限っては、includeしたファイルの中のラベルを参照することができませんでした。
HTMLでは参照できる2のですが、PDFになると参照できませんprobrefとラベルそのものが出てしまっています)。
出力先によって動きが違う理由に見当が付かず、「悔しくって死にそう」でした。

Issueを漁っていたところ、冒頭で紹介したものに出会い、解消できました。
includeするファイルはrst以外の拡張子にする必要がありました。

サンプルドキュメント

上記Issueに添付されたファイルを例にします。

.
├── Makefile
├── _build
├── conf.py
├── index.rst
├── make.bat
├── section1.rst
├── subsec1.rst
└── subsec2.rst

index.rst

sections1.rstからなるtoctreeです。

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   section1

section1.rst

subsec1.rst と subsec2.rst をincludeしています。
参照しているラベル probref は subsec1.rst にあります。

Section 1
=========

Labels in included files cannot be referenced.

.. include:: subsec1.rst
.. include:: subsec2.rst

Subsection 3
------------

References also do not work from parent document: :ref:`probref`

subsec1.rst

「Subsection 1」に probref ラベルを付けています。

.. _probref:

Subsection 1
------------

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt
ut labore et dolore magna aliquyam erat, sed diam voluptua.

subsec2.rst

subsec1.rst にあるラベル probref を参照しています。

Subsection 2
------------

References do not work from other included document: :ref:`probref`

環境

PDFビルドには sphinxdoc/sphinx-latexpdf:3.3.1DockerHub)を使いました。4

PDFビルドコマンド

docker run --rm -v $PWD:/docs sphinxdoc/sphinx-latexpdf:3.3.1

作られたPDFではラベルが参照できず、probrefというラベル名が表示されています(図を再掲します)

解決策

probref ラベルを定義したファイル subsec1.rst を subsec1.rst.txt のように拡張子をrst以外に変更すると解決します。

合わせて、section1.rst の include も更新します。

.. 変更がないので省略

.. include:: subsec1.rst.txt
.. include:: subsec2.rst

.. 変更がないので省略

「Subsection 1」とラベルが指すセクションの名前が表示されていますね🙌

解決する理由

前掲のIssueでは、includeされたドキュメントが二度処理されたためと説明されていました。

  1. section1.rst の処理で、subsec1.rst と subsec2.rst を include したドキュメントとなる(probref ラベルが section1に定義される)
  2. Sphinx*.rst という名前のファイルを検索することで、 subsec1.rst もドキュメントとして処理される。これにより probref ラベルが subsec1にも定義される5

同じラベルが2回現れるため、Sphinxは混乱し、最初の画像のようにリファレンスが壊れるとのことでした。

裏付けるように以下のwarningも出ています。

/docs/subsec1.rst:4: WARNING: duplicate label probref, other instance in /docs/section1.rst

include するファイルを subsec1.rst.txt とリネームすると、上記の2で処理されなくなります。
これにより、同じラベルが2回出てこなくなるということですね!

Sphinxのドキュメント6には include ディレクティブについて

単純に一つのファイルを別のファイルに"挿入"する場合、 include ディレクティブを使えます。

という注釈があるのですが、この「一つのファイル」がrst以外のファイルという前提があったことが分かりました。

拡張子を変えたあとの色分け設定(VSCode

nikkieはVSCodeを使っていますが、subsec1.rst.txtを開くと、rstファイルで見られた色分けがされません(文字は白一色です)。

これは、Select Language Mode から reStructuredText を選択することで解決しました。

(追記)選択を頻繁に繰り返していたので .vscode/settings.jsonfiles.associations を設定しました。

    "files.associations": {
        "*.rst.txt": "restructuredtext"
    }

終わりに

rstファイルを分割して include して使っていましたが、これはプラクティスに反していたことを身をもって体験しました。
拡張子を変えたことで、refnumrefでラベルが参照されるようになり、心の平穏を取り戻しました。

Sphinxの動きはまだまだブラックボックスですが、昨年のPyCon JPの以下はチェックしようと思います。


  1. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/roles.html#role-ref
  2. make htmlの実行に使いました。PDFビルドに使うDockerイメージとSphinxのバージョンを合わせる意図でバージョン指定しています。
  3. リポジトリhttps://github.com/sphinx-doc/docker 。DockerでLaTeXビルド環境が立ち上がってとても助かっています!
  4. 元のコメントでは subsec2.rst ですが、ラベルがある subsec1.rst のことを言っているのだと理解しました
  5. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#table-of-contents

2020年振り返りと2021年の目標(データサイエンティストとしてのnikkie編)

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
休暇最終日(1/4)を迎え、「振り返りをしていなかったな」とこの記事を書いています。
業務とプライベートの活動とで分けて書くことにしました。
今回は業務編です。

目次

nikkie as データサイエンティスト

2019年にユーザベースに転職し、データサイエンティストとして自然言語処理に携わっています。

「データサイエンティスト」は会社によって職務内容が異なると思うのですが、私はチームの一員として以下に取り組んでいます:

  • データ収集〜モデル訓練のパイプライン実装
  • モデルをAPIとしてデプロイ
  • モデル訓練

上2つがメイン1で、モデル訓練(Kaggleのコンペのようにいいモデルを作る)は少しずつ取り組む時間が増えてきました。

2020年🐭 業務で触った技術

自然言語処理

2020年1月〜3月まで、週1でブログを書き自然言語処理のキャッチアップに取り組みました(以下から始まります)。

『入門 自然言語処理』写経を中心に、ときには興味に任せてBERTなど気になる題材で手を動かした3ヶ月。
OOCのスタッフをした週など、書き上げるのが大変な週もあったのですが、3ヶ月やりきった達成感は気持ちよかったです。

『入門 自然言語処理』のおかげで全体像は大まかにつかめたと思っていますが、まだまだキャッチアップすべきところは広がっています。

パイプライン実装(データ収集)

パイプライン実装の中では、MySQLデータベースやGoogleスプレッドシートからデータを取得する処理の実装を経験しました。
スプレッドシートの扱いは、PyCon JP スタッフの自動化(カスタムリマインダーなど)で手を動かした経験がリンクした感じです。

モデル訓練

PyTorchを触るようになりました。

自動化やデータ取得処理を実装するときと比べると、モデル訓練スクリプトが動くようになるまでに、実装やデバッグで時間がかかるのが直近の悩みの種です。
このあたりの経験値が積めるよう、PyTorchのチュートリアルやKaggleへの取り組みなどを考えています。

API実装・デプロイ

以下の技術に触れました。

Kubernetesは4月時点では素振りを試みてハマっていましたが

業務中のペアプロなどを通して、kubectlコマンドの使い方を盗み、
9月にはminikubeでArgoを動かすなど、この1年でできるようになったことが多いと感じています。

MLOpsへの興味

2020年で一番影響を与えた文言が以下:

To make great products:

do machine learning like the great engineer you are, not like the great machine learning expert you aren’t.

「すごいエンジニアのように機械学習をやりなさい」、この言葉はサーバーサイドエンジニアを出自とする私の琴線に触れました。

2019年から仕事でPythonを書き始め、開発の仕方や設計など、エンジニアとして数々の伸びしろに直面した私は、データサイエンスの領域よりもエンジニアとしての力を付けることを優先しました(特に2019年)。
TDDには慣れましたが、設計は依然伸びしろ豊富、そんな状態でこの言葉を見て、「学んできたことが機械学習に活かせるし、両輪で歩めるんだ」と、興味関心がデータサイエンス側に振れました。
まだまだ知らないことばかりですが、エンジニアの考え方はデータサイエンスにも活かせると信じて取り組んでいきます。

MLOps関連のトピックでは、以下などで素振りしています:

アジャイル

いくつかの本でインプットしました。

Uncle Bobの『Clean Agile』がなかなか興味深かったです。

業務での開発はXPを実践していますが、「あのプラクティスはこういう考えで採用されているのか」と経験が理論付けされていきました。

もう1つ、社内勉強会で引用された「アジャイルは青春」というフレーズがじわじわ来ています。
私にとっての「青春」が想起するものは、アニメに対する「こんな青春送りたかった」というフレーズなのですが、
主人公たちが青春真っ只中にいる(青春するのではなくて青春である)ように、
アジャイルもするんじゃなくて、アジャイルであるものなんだなと思います2

リモートワーク

そうそう、3月くらいからリモートワークしています。

得たもの

  • 8時間程度働いたあと、シームレスにプライベートの開発やPyCon JPスタッフ活動に移行する生活
  • (5月以来)GitHubに毎日Contributeする習慣

失ったもの

  • 3ヶ月に1回のペースで体調を崩していたのがなくなった
  • 筋肉(体重)
  • 遠くを見なくなったせいか視力は落ちている気がします。。

2021年🐮 目標

2020年は「US PyConにプロポーザルを通す」というアホな目標を掲げた結果、海外での登壇を達成できた年だったのかなと考えています。
US PyConにこだわらず、海外PyConにプロポーザルを出し(まくり)、オンライン開催という変化と相まって、ムーンショット的に機能しました。

2021年もアホな目標ということで、「Oさんを倒す」という目標を掲げました!
Oさんは、今の職場に誘ってくださった凄腕データサイエンティストです。
倒すの定義は、私が訓練したモデルのスコアで、Oさんの作ったモデルのスコアを超えることです。

私とOさんの経験や技術力を比べると天と地の開きがあります(なので、この目標は相当アホだと思います)。
そして、先に述べたように、現状の私は独力でのモデルの訓練に課題感を抱えています。

目標の「倒す」にフォーカスするというよりも、取り組みや考え方を根本から変えて、達成できるラインが見えるようになることを狙っています。
なお、現実的ではなかったと痛感したら、意気消沈しても仕方ないのでこの目標は修正します。

2021年はPyCon JP座長など、これまでの年よりも「緊急かつ重要」なタスクを抱える年になる見込みです。
この記事で掲げた目標は「緊急ではないけれど重要」な目標です。
緊急なタスクに追われるだけでなく、データサイエンティストとして今よりももっと価値を出していきたいので、2021年やってみます!

P.S.

もしこの記事を読んで、データサイエンティスト職に興味を持たれた方は、お気軽にお声がけください。


  1. 会社によっては「データエンジニア」という呼び方が当てはまるのかもしれません。ただ機械学習モデルも作ります。データサイエンティストは名前空間が重要そうですね

  2. The Art of Agile Development, Second Editionのレビュー原稿にも「アジャイルをするのではなく、アジャイルである(アジャイルの考え方を身につける)」といった内容が書かれていました。アジャイルを"する"とは、考え方は変えずにアジャイルなメソッドだけを取り入れるという理解です

『Clean Architectures in Python』を写経しました(1st Edition, Part 2 - Chapter 2)

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
新年明けましておめでとうございます🎍🐮
ブログの書き初めは、冬の休暇に手を動かした、PythonでClean Architecture本についてです。

目次

Clean Architectureと私

ソフトウェアはやっぱりソフトに(=変更しやすいように)作りたいなと私は強く思います。
2020年はClean Architectureについてたびたびインプットしました。
例えばnrslibさんのYouTube配信です。

こういったインプットを経て、「Clean Architecture、概念の理解は深まったと思うけれど、Pythonで具体的にどうやればいいんだろう?」という課題感を感じていました。
層の概念はいわゆる"完全に理解した"けれど、それをPythonのコードに落とし込んだらどうなるのかが分からなかったわけです。

7月のEuroPythonで『Clean Architectures in Python』という本を知りました1

これは積ん読になっていたのですが、ヴァケイションには「緊急なことじゃなくて、自分にとって重要なことをやろう」と写経してみました。
学んだことをここにアウトプットします。

『Clean Architectures in Python』(1st edition)

この本では、物件情報を一覧にするWeb APIを例にClean Architectureが解説されます。

本は2つのPartに分かれます:

  • 前半(Part 1)はTDD導入パート
  • 後半(Part 2)は物件情報一覧API(面積や賃料、緯度経度)を実装
    • TDDで進める(まずテストコードが出てくる)
    • Flask

nikkieのレベル感は以下です:

  • ふだんはunittestでTDD。pytestのコードも雰囲気で読めそうなのでPart 1はskip
  • FlaskHello Worldから先はあまり踏み込めておらず。この本では blueprint や configuration を駆使2してAPIを実装。
    • Flaskの本ではないとのことですが、ドキュメントへのリンクは充実しています。深く理解しているわけではない概念についてもキャッチアップしてついていけました

今回はClean Architectureの理解が目的なので、pytestFlaskの厳密な理解はヤクの毛刈りと考え、あまり深追いせずに進めました。

Part 2 - Chapter 2 での学び

ビジネスロジック部分をClean Architectureで作ると、UIはCLIでもWeb APIでもなんでも採用できそうという感触を得ました。
12月のPHPカンファレンスでnrslibさんが共有していた、「次のフレームワークに持っていく」ってこういう考え方なんですね!
PythonはWebフレームワークが乱立してますが、Clean Architectureで作ったビジネスロジック部分は移動しやすそうです。

そう思った2章のコードはこちら:

repo = mr.MemRepo([room1, room2, room3])
use_case = uc.RoomListUseCase(repo)
result = use_case.execute()

やっていることは

  1. インメモリなリポジトリを作り
  2. そのリポジトリを渡して部屋一覧ユースケースを作り
  3. ユースケースを実行(execute)して結果(=部屋の一覧)を得る

これだけ!
このコード以外はCLIを扱うコードでもWeb APIを扱うコードでもいいわけです。

3つの層:エンティティ/ユースケース/外部システム

この本ではClean Architectureを3つの層のアーキテクチャとして紹介しています(Part 2 - Chapter 1参考)。

3つの層は

内側:エンティティ < ユースケース < 外部システム:外側

という包含関係にあります。
これらの層の間には

Talk inwards with simple structures, talk outwards through interfaces

(意訳:内側の層に対してはPythonのデータ構造やエンティティに定義された単純なデータ構造で依存せよ。外側の層に対してはインターフェースを介して依存せよ)

という原則があります。

この本を読んで、一番腑に落ちたのは

  • ユースケースは内側にあるエンティティの実装の詳細を知っている
  • ユースケースは外側の外部システムの実装の詳細は知らない
    • ただ外部システムのAPIは知っているので、外部システムを利用できる

というところです。

先のコードで use_case.execute() を実行すると

class RoomListUseCase:
    def __init__(self, repo):
        self.repo = repo

    def execute(self):
        return self.repo.list()

ユースケース初期化時に渡されたリポジトリlistメソッドを呼び出します。
ユースケースリポジトリ(外部システム)のAPIを知っていて、listを呼び出せば物件が取得できると分かっているわけです。

ユースケースという概念

今回ユースケースという概念を知れたのが大きいと思っています。

私がプログラミングに入門したとき、クラスの入門例がDogやCat、CarやSuperCarでした。
これらの例は、現実の具体的な事物と対応しているので(私が過学習してしまって)、現実の具体的な事物と対応しないクラスは独力ではなかなか思いつきません。
処理を表すクラス(現実の具体的な事物と対応するわけではない)を作るというのを知ったときは衝撃でした。
今回のユースケースもそのときと同じくらいの衝撃です。

ユースケースを初期化する時にリポジトリを渡すというのも変更しやすさの妙だなと思います。
Chapter 2ではインメモリのリポジトリを渡していますが、これはDBに接続するリポジトリに変わるでしょう。
そうなってもユースケースからはlistというAPIを使うだけなので、リポジトリを付け替えるだけで動作確認が簡単にできますよね。
DBを準備しなくてもインメモリのリポジトリを用意すれば動作させられます(nrslibさんたちが言ってたやつだ!)

用語:モデル/リポジトリ

この本でありがたかったのは、用語の誤解に先手を打っていたこと。

エンティティはドメインモデルの表現と説明する中で、ドメインモデルはDjangoなどのフレームワークにおける"モデル"とは異なると言っています。
ドメインモデルは軽量(lightweight)なモデルで、自身をストレージに保存するメソッドや、JSON文字列としてダンプするメソッドは持ちません。

リポジトリもGitのリポジトリとは無関係です。
外部システムのストレージにアクセスするものはリポジトリと呼ばれます。
リポジトリドメインモデルを返します
先の例ではユースケースリポジトリから返ってきたドメインモデルを扱うわけです。

Clean Architecture以外での学び

JSON文字列を返すためのserializer実装

json.JSONEncoderを継承したクラスを定義し、defaultメソッドを実装、それをjson.dumpscls引数に指定しています。

pytestのdeprecation

py.testというコマンドが出てきて気になったのですが、pytestコマンドが推奨されているそうです。

pytest-flask向けにtests/conftest.pyを作ったところ、@pytest.yield_ficturesPytestDeprecationWarningが上がりました。
deprecateされていて、@pytest.fixtureでいいそうです。

終わりに

「Clean Architecture、Pythonで具体的にどうやればいいんだろう?」
冒頭の問に対して暫定的な答えは得られました。
ユースケースを使った実装を練習していきます!

層の数の違いなどUncle Bobの『Clean Architecture』とは厳密には違うのかもしれませんが、「変更しやすい設計でどう作ればいいか」かなり具体的に分かったので、今の私としては大満足です。

今回のコードはこちら

続くChapter 3では部屋を絞り込む機能を追加します。
実装する上で、リポジトリへのリクエストを表すオブジェクトとレスポンスを表すオブジェクトが登場しました。
これらの扱いも学びが多く、またの機会にアウトプット予定です!

ここまで読んで『Clean Architectures in Python』に興味を持った方、先日2nd Editionがリリースされました🎉
サンプルは変わっていないそうですが、dataclassを使っていたり、分かりやすそうな図が追加されていたりするので、復習がてら読んでみようかと思います。

そしてなんと、月末にはUncle Bobの話が聞ける機会があるので、今よりもう少し理解度を上げて参加するのを目指します。

それでは!


  1. Clean Architectures in Python — EuroPython 2020 Online · 23-26 July 2020

  2. FlaskHello Worldで入門するのは簡単ですけど、うまく使おうと思ったらblueprintやconfiguration など結構キャッチアップがいりますよね(キャッチアップの総量はDjangoとあまり変わらない感覚)

魔法みたいな機能満載のStreamlit。その中でもとびっきりの魔法、キャッシュについて

はじめに

この記事はデータ可視化 Advent Calendar 2020 21日目の記事です。

頑張れば、何かがあるって、信じてる。nikkieです。
先日参加した #pyhack でデータ可視化アドベントカレンダーに空きがあることを知り1、最近推しているStreamlit2について締切駆動でアウトプットしてみることにしました。

目次

Streamlitとは

サーバサイドのロジックからUIまでカバーした、ラピッドプロトタイピングツールです。
データ可視化アプリや機械学習モデルを使ったアプリが簡単に作れると注目を集めている印象です。

nikkie的にポイントが高いのは、Streamlitを使えば、Pythonを書くだけでブラウザを通して操作できるアプリができるという点。
チュートリアルを見て「こんなに簡単に作れるのか!」と舌を巻きました。

import pandas as pd

import streamlit as st

st.title("My first app")
df = pd.DataFrame(
    {"first column": [1, 2, 3, 4], "second column": [10, 20, 30, 40]}
)
df

f:id:nikkie-ftnext:20201221222702p:plain

Galleryを覗けば、いろいろなアプリが見つかります。

上記のチュートリアルのTIPで@st.cacheというデコレータが紹介されているのですが、このキャッシュ機能も、Streamlitのヤバい✨機能の1つでした!
この記事では@st.cacheについてドキュメントに沿って見ていきます。

お断り:データ可視化アドベントカレンダーではありますが・・

今回の@st.cacheのアウトプットのサンプルアプリは、データ可視化アプリではありません。
ですが、この記事で登場する@st.cacheの使い方は、Streamlitを使ったどんなアプリにも適用できると考えています。

Streamlitはデータ可視化アプリや機械学習アプリに使われることが多いと思いますが、私自身は「Pythonスクリプトを他の人に使ってもらうように変換する」のにも使えるツールととらえています。
今回は画像リサイズスクリプトを元にした便利ツールアプリを例に@st.cacheを見ていきます。

ソースコードはこちら(のディレクトリ)

開発環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.8.6
$ pip install streamlit==0.72.0 Pillow==8.0.1 pandas==1.1.4

@st.cacheとは

ドキュメントの「Improve app performance」というページに、使い方が詳しく書かれています。
Streamlitを使うなら一読して損はないと思います。

※誤読と思われる箇所に気づきましたら、コメントやTwitterでお知らせください。大変ありがたいです

かいつまんで言うと、StreamlitのキャッシュはインメモリのKVS(key-value store)です3
@st.cacheでデコレートされた関数には、ざっくり言うと、入力された引数のハッシュをキーとし4、関数の実行結果をバリューとするキャッシュが用意されます。

関数を呼び出したとき、引数(のハッシュ)がキャッシュにヒットすれば、キャッシュが使われます(関数は実行されません!)。
キャッシュミスがあった場合は関数が実行され、引数(のハッシュ)と返り値がキャッシュに登録されます。

例(ドキュメントより)

ドキュメントにあるのが以下の例(Example 1):

import time

import streamlit as st


@st.cache
def expensive_computation(a, b):
    time.sleep(2)
    return a * b


a = 2
b = 21
res = expensive_computation(a, b)

st.write("Result:", res)
  • @st.cacheが付いていないとき、ブラウザでページをリロードするたびにexpensive_computationが実行されて、表示までに毎回2秒かかります
  • @st.cacheを付けると、初回表示では2秒かかりますが、2回目以降のリロードはキャッシュが使われるので2秒待つことはなくなります

この状態から

  • expensive_computationに渡す引数bの値を変更(Example 2)
  • expensive_computationの中の実装を変更(Example 3)
  • expensive_computationから別の関数を呼ぶように変更(Example 4)
  • さらに呼び出し先の関数の実装も変更(Example 4)

とすると、いずれも(ハッシュ(すなわちKVSのキー)が変わるので)キャッシュミスが起き、関数が実行されるとドキュメントは説明しています。

ヤバい!と思ったのはExample 5。
ユーザが画面のスライダで選択した値をbとするのですが、

  1. あるタブでスライダで9を選ぶ(→キャッシュミスにより2秒待つ)
  2. 別のタブでも表示し、スライダで9を選ぶ→1で登録されたキャッシュがヒットするので、2秒待たずに画面が表示される!!

Streamlitのキャッシュはグローバルにしており、あるユーザが作ったキャッシュが他のユーザのパフォーマンス向上に寄与できる設計とのことです。

This happens because the Streamlit cache is global to all users. So everyone contributes to everyone else’s performance.

キャッシュは参照なので、ValueがMutable objectのとき、Valueを書き換えると、キャッシュのValueも書き換わります(Example 6。CachedObjectMutationWarningが出ました)。

@st.cache適用 Before / After

Before

画像5リサイズアプリでは当初以下のような実装をしていました。

uploaded_files = st.file_uploader(
    "Specify image(s) to be resized in your computer",
    type=["png", "jpg", "jpeg"],
    accept_multiple_files=True,
)
max_length = st.slider("Specify max length", 100, 500, 300, 50)
if uploaded_files:
    random_id = uuid.uuid4()
    shrinked_dir_path = Path(f"images/{random_id}")
    shrinked_dir_path.mkdir(exist_ok=True)

    for uploaded_file in uploaded_files:
        resized_image_path = shrinked_dir_path / uploaded_file.name
        has_resized = resize_image(  # 画像を縮小する処理の呼び出し
            uploaded_file, resized_image_path, max_length
        )
        if has_resized:
            st.image(str(resized_image_path))

f:id:nikkie-ftnext:20201221223529p:plain

この実装で気になった点は、選択した画像を1つ削除したり、スライダーのサイズ指定をいじったりすると、すでに縮小済みの画像があったとしても毎回縮小した画像が作られる点です。
ファイルが重複してできていって、リソースを無駄にしている感覚がありました。

After

@st.cacheの存在を知り、画像を縮小する処理を関数にしてからデコレートしました。

  • 入力:画像ファイル1枚、縮小サイズ
  • 処理:画像を縮小して保存する
  • 出力:縮小した画像のパス(表示するのに使う)
@st.cache
def resize_uploaded_image(image_file_object, max_length):
    random_id = uuid.uuid4()
    shrinked_dir_path = Path(f"images/{random_id}")
    shrinked_dir_path.mkdir(exist_ok=True)

    resized_image_path = shrinked_dir_path / image_file_object.name
    has_resized = resize_image(
        image_file_object, resized_image_path, max_length
    )
    if has_resized:
        return resized_image_path


uploaded_files = st.file_uploader(
    "Specify image(s) to be resized in your computer",
    type=["png", "jpg", "jpeg"],
    accept_multiple_files=True,
)
max_length = st.slider("Specify max length", 100, 500, 300, 50)
if uploaded_files:
    for uploaded_file in uploaded_files:
        resized_image_path = resize_uploaded_image(uploaded_file, max_length)
        if resized_image_path:
            st.image(str(resized_image_path))

画像ファイルと縮小サイズの組(のハッシュ)がキャッシュにヒットしたら、すでに縮小済みの画像があるということなので、それを画面に表示します。
キャッシュを使うように処理を書き換えたことで、入力を変更するたびに縮小されなくなりました!

なお、st.file_uploaderの返り値はファイル名以外にファイルのサイズ(や内容?)も見ているようで、全く異なる画像をキャッシュにあるのと同名にしてアップロードしても、キャッシュがヒットしませんでした。
すごいですね!

終わりに

Streamlitの@st.cache、味方につけるためには関数の設計をカスタマイズする必要がありますが、パフォーマンスにもたらす効果は覿面と感じています。
データ可視化にとどまらず、Pythonスクリプトをアプリにするツールとして可能性を感じているStreamlit、2021年も使い倒していく予定です。

ここまで「魔法」が揃っているStreamlit、魔法をどう実現しているか気になってきますよね。
依存関係で入ってくるtornadoがサーバのようですし、UIはReact6と聞いています。
アドベントカレンダーに手を挙げた時点ではStreamlitの中を見るというのもネタの候補にありました。
今回は間に合わず、みんなのPython勉強会の登壇準備中に知ったネタを使ったのですが、時間のある年末年始に中を見てみようと思っています。

Happy prototyping with Streamlit!


  1. この記事は12月のみんなのPython勉強会発表のプチ続編でもあります。「Streamlitヤバい!」と連呼してましたね(語彙力😅)

  2. Think of the cache as an in-memory key-value store, (冒頭(=Example 1より前))

  3. ラピッドプロトタイピングなので、関数の中身が変更されることもあるでしょう。そんな状況でも対応できるように、引数のハッシュ以外に関数のコード(The body of the function)などもキーにしています。上記ドキュメントでは「Advanced caching」の節に詳しいです

  4. 画像はPyCon JP 2019のアルバムより

  5. ref: GKEでStreamlitをホスティングして社内用アプリを作った話 - エムスリーテックブログ

2020年積み残し解消:django-allauthを使ったソーシャルアカウント(GitHub・Slack)連携、素振りの記

はじめに

この記事はDjango Advent Calendar 2020 14日目の記事です。

頑張れば、何かがあるって、信じてる。nikkieです。
2020年はPyCon JPスタッフ活動の中で、力を付けたくてDjangoアプリを作りました。
Slackアカウントでアプリにログインする機能を実装する際に、素振り不足を理由にdjango-allauthの採用を見送りました。
これがちょっとした心残りとなっており、年内に解消すべくアドベントカレンダーの締切駆動でdjango-allauthを触ることにしました。

目次

経緯:積み残したdjango-allauth

※8月のPyCon JPで話した内容1と重なります。

PyCon JPスタッフ活動の中で、レビューに使うスタッフ内部用のWebアプリ2Djangoで実装しました。
スタッフだけがログインできるように、PyCon JP Slackのアカウントを使ってログインする機能を実装しました(スライド31〜33)。
実装中はdjango-allauthも候補にあったのですが、ドキュメントを見た感じすぐに使えるイメージがつかめず、別のパッケージを使いました。
「会期が終わった後に同じ実装をするならdjango-allauthを試したい」と思っていたので、今回試しました。
今回の素振りのゴールは、簡易的なアプリでdjango-allauthを使ってSlackアカウントログインを実装することです。
「レビューアプリにdjango-allauthを導入できそうという感触を持てたらいいな」と取り組みました。

開発環境

django-allauthPython 3.8までだったので、3.8を選択しました。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ python -V
Python 3.8.6
$ pip install Django==3.1.4 django-allauth==0.44.0 django-environ==0.4.5

Django Girls Tutorialに沿ったディレクトリ構成で今回の練習用アプリを実装しています。
DBはsqliteを使い、ローカルでのみ動かしました。

リポジトリはこちらです。

素振り1:『現場で使えるDjangoの教科書 実践編』に沿ってGitHubアカウントでログイン

参考にしたのは、akiyokoさんの『現場で使えるDjangoの教科書 実践編』。

django-allauthはソーシャルアカウント連携だけではなく、allと名に負う通り、ログイン周り全般も面倒を見てくれます!
※ただし、今回はソーシャルアカウント連携に絞って見ていきます。

  1. settings.py を変更
  2. GitHubでOAuth Appを作成
  3. Django AdminからSocial applicationを作成

前提

前提として、以下の状態とします(Django Girls Tutorialベースです)。

# 前提:作業ディレクトリにいる
$ python3.8 -m venv myvenv
$ . myvenv/bin/activate

# ... 上で挙げたパッケージをpip installする

$ django-admin startproject mysite .

# ... Django Girls Tutorialに沿って「設定変更」する(mysite/settings.pyを変更)

$ python manage.py migrate

作業ディレクトリの状態

.
├── db.sqlite3
├── manage.py
├── mysite
└── myvenv

settings.py 変更(URL & テンプレート追加)

django-allauthを使うために、まず mysite/settings.py を変更。

INSTALLED_APPS = [
    # : すでにあるものの下に以下を追加
    'django.contrib.sites',  # django-allauthに必要

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',
]

# ... 途中を省略 ... 以下は一番下に追加

##################
# Authentication #
##################

SITE_ID = 1

LOGIN_REDIRECT_URL = "home"
ACCOUNT_LOGOUT_REDIRECT_URL = "account_login"

django-allauthをインストールしたので、migrateが必要です。

続いて、以下の2つのファイルを用意します。

mysite/urls.py3

urlpatterns = [
    path('admin/', admin.site.urls),
    # 以下2行を追加
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('accounts/', include('allauth.urls')),
]

templates/home.html4(新規作成)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>ホーム</title>
  </head>
  <body>
    <h2>すごいアプリ</h2>
    {% if user.is_authenticated %}
      ようこそ {{ user.get_username }} さん
      <p><a href="{% url 'account_logout' %}">ログアウト</a></p>
    {% else %}
      <p><a href="{% url 'account_login' %}">ログイン</a></p>
    {% endif %}
  </body>
</html>

GitHub OAuth App作成

以下を参考にしました。

f:id:nikkie-ftnext:20201212204220p:plain

Authorization callback URLにはhttp://127.0.0.1:8000/accounts/github/login/callback/を設定。

Django Adminの操作

createsuperuserしてからDjango Adminにログインします。
「外部アカウント」の「Social applications」から追加していきます。

f:id:nikkie-ftnext:20201214131447p:plain

以上で、GitHubアカウントでログインできるようになります。

127.0.0.1:8000/(templates/home.htmlの表示)

f:id:nikkie-ftnext:20201212215812p:plain

「ログイン」をクリックするとログインページを表示(ビューとテンプレートはdjango-allauthが用意)
127.0.0.1:8000/accounts/login/

f:id:nikkie-ftnext:20201212215825p:plain

GitHubで初回ログイン(認可を求められる)※同じブラウザですでにGitHubにログインしています

f:id:nikkie-ftnext:20201212215837p:plain

ログイン状態で 127.0.0.1:8000/ に戻る

f:id:nikkie-ftnext:20201212215853p:plain

素振り2:Slackアカウントでもログイン

半年前にやりたかったSlackアカウントでのログインに挑戦してみます。

  1. Slack App作成
  2. Django Adminの操作の代わりにmysite/settings.pyに設定追加

Slack App作成

SlackはWorkspaceごとにアカウントがありますが、ログインに使いたいアカウントのあるWorkspaceでAppを作ります。

f:id:nikkie-ftnext:20201212220131p:plain

「OAuth & Permissions」に以下を設定しています

  • Redirect URLs
    • http://127.0.0.1:8000/accounts/slack/login/callback/5
  • Scopes > User Token Scopes
    • 未設定だとAppのインストールができないと表示されました
    • Slackのドキュメントによるとidentityが必要なようなので、identity.basicを設定

mysite/settings.pyに設定追加

Django Adminで操作しなくても、mysite/settings.pyに設定を追加してもできるようです。

INSTALLED_APPS = [
    # 省略。以下を追加
    'allauth.socialaccount.providers.slack',
]

# 省略

##################
# Authentication #
##################

# 省略。一番下に以下を追加
SOCIALACCOUNT_PROVIDERS = {
    # GitHub用のSocial applicationを削除して以下でも動きます
    # "github": {
    #     "APP": {
    #         "client_id": env("GITHUB_OAUTH_CLIENT_ID"),
    #         "secret": env("GITHUB_OAUTH_SECRET"),
    #     }
    # },
    "slack": {
        "APP": {
            "client_id": env("SLACK_OAUTH_CLIENT_ID"),
            "secret": env("SLACK_OAUTH_SECRET"),
        }
    },
}

127.0.0.1:8000/accounts/login/ にSlackアカウントでログインするためのリンクが追加されます!

f:id:nikkie-ftnext:20201212221020p:plain

リンクをクリックすると、Slackアカウントでのログインにあたり許可が求められます。

f:id:nikkie-ftnext:20201212221116p:plain

なお、今回はGitHubアカウントで連携してログインした後、http://127.0.0.1:8000/accounts/social/connections/ にアクセスしてSlackアカウントも連携しました。
なので、SlackアカウントでログインするとGitHubアカウントでログインしたときと同じように表示されます。

f:id:nikkie-ftnext:20201212215853p:plain

account_loginを経由せず、homeから直接ログインできるようにする

ここまでで作ったアプリは、homeからaccount_login(django-allauthが用意するログインページ)に遷移してログインします。

templates/home.html(新規作成)

      <p><a href="{% url 'account_login' %}">ログイン</a></p>

homeから直接ログインできるように、ソースコードを見ていじってみました(もっといいやり方をご存知の方いたら教えていただけると嬉しいです)。

      <!-- <p><a href="{% url 'account_login' %}">ログイン</a></p> -->
      お持ちの外部アカウントでログインをどうぞ
      <ul class="socialaccount_providers">
        {% include "socialaccount/snippets/provider_list.html" with process="login" %}
      </ul>

f:id:nikkie-ftnext:20201213195444p:plain

参考にしたのは、allauth/templates/account/login.html6
ソースコードを少し見たところ、settings.pyで有効にしたprovider(今回はGitHubとSlack)について、ログインに使うリンクが作られる挙動のようでした。

素振りしてみて

素振りする前は、キャッチアップが大変そうという印象のdjango-allauthでしたが、akiyokoさんの本を経由してドキュメントを読み、「ソースコード周りはsettings.pyの設定だけでソーシャルアカウント連携が実装できる!便利」と印象は大きく変わりました。
今回の素振りを元にやってみたい項目は以下です(年内にできたらいいな)。

  • レビュー用WebアプリでSlackアカウントでのログインを置き換える(力試し)
  • django-allauthリポジトリにあったexampleで素振りしてみる

django-allauthを使うと、ソーシャルアカウント連携は設定が大部分という感触を得ました。
来年作るDjangoアプリでは積極的に使っていけそうです!

補足:django-environ

先のコードで登場した env("SLACK_OAUTH_CLIENT_ID") などについて補足します。

OAuthの設定に必要なClient IDやSecret、DjangoSECRET_KEYなどは環境変数に設定していますが、コードではdjango-environで扱っています。
これも知ったのはakiyokoさんの本(実践編)です。

使い方は簡単で、ドキュメントの冒頭のコードが参考になります。

mysite/settings.py

import environ

env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(env_file=str(BASE_DIR / ".env"))

今回はリポジトリのルート(BASE_DIR)に.envファイルを置きました。
.envファイルを示すpathlib.Pathが受け取れないようなので、文字列に変換して渡しています。
環境変数.envファイルでまとめて管理でき、便利です。

.envファイルのイメージ

DEBUG=on
SECRET_KEY=very_very_secret_key

SLACK_OAUTH_CLIENT_ID=2****************4
SLACK_OAUTH_SECRET=hi_mi_tsu

  1. YouTubeアーカイブもあります。

  2. 嬉しいお言葉もいただきました。ありがとうございます。「いつ作ってるんあろうと思うぐらい作りこまれてまして」ref: まったりLog残し - PyCon JP 2020に参加してきました

  3. django.views.generic.TemplateViewはakiyokoさんの『基礎編』の教科書に解説があります

  4. リポジトリルートのtemplatesディレクトリからテンプレートを探せるように、mysite/settings.pyTEMPLATESDIRSBASE_DIR / "templates" を追加する必要があります

  5. ref: https://django-allauth.readthedocs.io/en/latest/providers.html#slack

  6. urls.pyを見たところ、account_loginという名前のパスに対応するビューはLoginViewでした。settings.pyで設定をしていない場合、account/login.htmlという名前のテンプレートが使われるようでした

イベントレポート | #はんなりPython python3.9を語る LT会 に参加しました(2020年11月)

はじめに

この記事ははんなりPython Advent Calendar 2020 1日目の記事です。

頑張れば、何かがあるって、信じてる。nikkieです。
2020年10月にPython 3.9がリリースされましたね!
はんなりPythonで開催されたPython 3.9についてのLT会1でインプットしたことをまとめます。

目次

勉強会の概要

【オンライン】はんなりPython #34 python3.9を語る LT会 - connpass

はんなりPythonは毎月3週目の金曜日に行っています。

参加者同士の交流や情報交換がしやすいような雰囲気を作っています。
現役のエンジニアや女性エンジニア、プログラミング学習者など幅広い方々との出会いの場になってほしいです。

11月のはんなりPythonはLT会,テーマは「Python3.9」です。
LT時間は7分から14分です。

21時〜23時というちょっと遅い時間帯が特徴だと思っています。

勉強会の様子

この会をまとめた公式ブログはこちらです。

発表資料

共同編集 Scrapbox

What's New in Python3.9を読む hide ogawaさん

変更点の概要(What's New)をdeepnoteで編集・実行可能な形式でまとめた発表。
触って気になるところの理解を深められるのがいいですね!
deepnote初めてでしたが、これで共同編集できるとなると、話題になるのも納得でした。

3.9 で追加された ast モジュールの新機能 ast.unparse t2yさん

ast.unparseドキュメント)で抽象構文木からソースコード戻せるようになったそうです。
ソースコード→抽象構文木(AST)ソースコードができる!
デモで、抽象構文木を加工してソースコードを書き換えていて、私はおおっ!と興奮しました。

プログラミング言語の抽象構文木に興味を持ったのは以下の記事です。
a = 2といったコードはもっと深く理解できるんだ。知りたい!」とワクワクしました。

hyというDSLの例では、今年PyCon Africaで聞いたteal-lang2を思い出しました。
どう作ったのか気になってましたが、抽象構文木を操作すれば作れるんですね。

t2yさんによると、抽象構文木を学ぶには、書籍は以下がオススメだそうです:

新規モジュール graphlib のご紹介 molo21stさん

みんなのPython勉強会で私もお世話になっている辻さんのツイートをきっかけに触ってみたとのこと。

モジュールのソースコードまで踏み込んだ解説でとても分かりやすかったです。
裏で何が起こっているかつかめ、木のように見た時に根元の部分から並べていることが分かりました。

素人考えですが、組織図のようなものを一直線上に表す時に使えるかもと思いました。
スタッフなどで複数の役割を兼務しているケースに重宝しそうです。

import graphlib as gl

graph = {
    "低音": {"黄前", "田中"},
    "トランペット": {"中世古", "高坂"},
    "北宇治": {"低音", "トランペット"},
}

ts = gl.TopologicalSorter(graph)
print(tuple(ts.static_order()))
# ('田中', '黄前', '中世古', '高坂', '低音', 'トランペット', '北宇治')
# このタプルを逆順にすれば、組織図の上から幅優先でたどった感じになりますね

PidfdChildWatcherについて Yasshieeee(やっしー)さん

PidfdChildWatcher の紹介 + 並列処理周りの整理。
質疑タイムで発表内容への補強もあり、並列処理を使いこなせていない私はいくつも学びがありました

multiprocessingの例(記憶を元に、またドキュメントを参考に)

import time
from multiprocessing import Process

def f(num):
    print("start", num)
    time.sleep(2)
    print("end", num)

if __name__ == "__main__":
    p1 = Process(target=f, args=(1,))
    p2 = Process(target=f, args=(2,))
    p1.start()
    p2.start()

# start 1
# start 2
# end 2  # 以下は2秒後に表示
# end 1

asyncioの例(記憶を元に、またドキュメントを参考に)

import asyncio


async def f(num):
    print("start", num)
    await asyncio.sleep(2)
    print("end", num)


async def main():
    await asyncio.gather(f(3), f(5))


if __name__ == "__main__":
    asyncio.run(main())

# start 3
# start 5
# end 3  # 以下は2秒後に表示
# end 5

zoneinfoを使おう hrsano645さん

# Python 3.9.0で確認
# Python 3.8まで
>>> from datetime import datetime, timedelta, timezone
>>> jst_tz = timezone(timedelta(hours=9))
>>> d1 = datetime(2020, 10, 31, 12, tzinfo=jst_tz)
# Python 3.9から
>>> from zoneinfo import ZoneInfo
>>> d2 = datetime(2020, 10, 31, 12, tzinfo=ZoneInfo("Asia/Tokyo"))
# どちらの作り方でも等しい
>>> d1 == d2
True

datetime.datetime.now()ドキュメント)はタイムゾーン指定なしではnaiveなローカル日時が紛らわしいは同意ですし、
これがnaiveなせいでawareなdatetimeと差を取ろうとしてエラーというのもあるあるです。

感想

Python自体について色々と知られて、楽しかった!これに尽きます。
みんなのPython勉強会やPyCon JPはスタッフとして参加なので、なかなかインプット全集中しにくいところがあります(運営タスクが脱集中してくるのです)。
参加者として参加すると、インプットに集中できるのが新鮮ですね。

また、Pythonを始めてそろそろ3年ですが、「Pythonのことは何でも知りたい!」とドキュメントをひたすら引いています3
そんな私にはPython 3.9縛りのLTはどれも聴きごたえがあり、とても楽しめました!

終わりに

はんなりPythonさんは第3金曜日以外も精力的に(週1ペース!で)勉強会を開催されているので、「これは!」と思うものがあったら、ぜひぜひ飛び込んでみてはいかがでしょうか。
12月はKaggle部や年末LT会がありますよー!

登壇者、運営スタッフ、参加者、スポンサーの皆様、ありがとうございました。


  1. 私もPEP 585について登壇したのですが、登壇報告はまたの機会に。ref: https://twitter.com/ftnext/status/1329775771766771713?s=20

  2. リポジトリを見たら今はhark-langというそうです

  3. Pythonに対してややヤンデレな気もします