この記事はBASE Advent Calendar 2022の19日目の記事その2です。
こんにちは。BASE 株式会社 New Division BASE BANK Section にて、Engineering Program Managerをしている永野(@glassmonekey) です。
私個人としては、今年のアドベントカレンダー2回目です。 前回は以下の記事で普段の仕事への取り組みざまを書いたのでそちらもよかったらご覧ください。
今回の記事の趣旨としては、私が主に担当しているYELL BANKというプロダクト開発で行ったPython の改善ネタになります。
ちなみにYELL BANKについては同僚のyuniさんが熱く語ってくれたのでそちらもご覧ください。
YELL BANKのプロダクトの裏側
YELL BANK では、過去の売上データを元に提供金額を算出し、BASE加盟店さま
の規模感に応じた資金調達体験を実現しています。
その際以下のフローで金額の提示を行います。
- 売上予測といった機械学習の内容を返却するAPIから金額算出のための情報を取得する
- 我々の事業状況に応じて提供金額算出 API が提示する金額の計算を行う
- 計算した金額を
BASE加盟店さま
向けの管理画面で提示する
資金提供APIを始めとして、我々の開発チームでは基本的にGoを扱うことが多いです。
しかし提供金額算出 API
に関しては、機械学習の仕組みも簡単に取り入れる選択肢がありえるだろうということで、Pythonで書かれています。
実際に現在は異常検知のために、機械学習の仕組みが導入されていたりもしています。
ある日の課題
リリースして5年にもなるサービスなので、金額計算のロジックを始めとしたドメイン知識の複雑さも増える一方の状況でした。
とくに金銭を扱うコードなので、一歩間違えると事業上大きなリスクを背負うことにもなり、品質にも気をつける必要がありました。
そのため、金額計算のテストは厚めに書いていたものの、複雑なドメインロジックの認知負荷を下げることにはそこまで対処できていない状況でした。
特に型の整備は全然できておらず、int
, str
といったプリミティブな型が基本的に使われるという状況でした。
特にYELL BANKにおいて、同じ金額という内容でも以下を区別しており、一言にint
として扱うのは限界がありました。
* 提供金額
: 利用者に提供する金額
* 支払い総額
: 手数料を含めた金額
いわゆるLintの整備はある程度できていたものの、とりわけmypy
による型の静的解析の対応ができていませんでした。型の静的解析があれば早めに気づけたミスも、テストを実行してみて初めて気づくという状況が度々発生してしました。
全てのテストで数分レベルなので、致命的とまでは言わないですが開発者体験としてあまりいいものではありませんし、今後のテストの内容次第では開発のボトルネックになるのは目に見えていました。
段階的に型をつけていく
そこで、全体的コード知識を整理すべく、以下を中心に型の追加・厳格化対応をしていきました。
dataclassの活用
NewTypeでプリミティブな型に意味付けをする
mypyのstrict化の実施
それぞれ詳しく説明します。
dataclassの活用
ある程度のドメイン知識を表す表現に当初はTypedDictをメインで使っていましたが、単体だとメソッドをつけられず不便でした。
一方でclass
を使えばオブジェクトに振る舞いを持たすことは可能ですが、
testに失敗したときに以下のように差分が不明瞭といった課題感がありました。(スクリーンショットはPyCharmでテストを実行したものです)
マジックメソッドの__eq__
や__repl__
を定義してなんとかやりくりしていましたが、クラスの構造に対する追従漏れが生じたりと、十全とは言えない状況でした。
そこで dataclass
を積極的に活用するようにしました。
dataclass
の詳細はPEP 557で定義されているので一読するのをおすすめしますが、3.7から導入された機能になります。3.6用のバックポートも存在しています
使い方はシンプルで、classに対して@dataclass
のデコレータを使用するだけです。
frozen=Trueを使用すると生成されるオブジェクトのプロパティの再代入が禁止され、イミュータブルなオブジェクトに簡単にできるので大変便利です。
@dataclass(frozen=True) class Point: x: int y: int
上記のclassに対して、あえて失敗するテストコードを書いてみます。
def test_sample(self): assert Point(1, 1) == Point(2, 1)
すると以下のように失敗したときの差分もわかりやすく表記してくれるので、開発効率は大幅に上がりました。
NewTypeでプリミティブな型に意味付けをする
前述した通り、YELL BANKでは様々な数値を扱うため全ての数値をint
だけで表現することに限界があると述べました。
ではどのようにするといいのでしょうか?
前述したdataclass
を使った表現も1つの方法としては考えられるでしょう。しかし、振る舞いをそこまで必要としない数値的な概念に対して全てにdataclass
を定義することは過剰なように思えます。
そこで、考えた手としてはNewType
の活用でした。以下のように簡単コメントで説明を併記しておくことで、簡単なドメイン知識が集約されるようにもなりました。
FactoringAmount = NewType('FactoringAmount', int) # 債権の提供金額。 PurchaseCreditAmount = NewType('PurchaseCreditAmount', int) # 提供金額に手数料とたしたもの。
strict な mypyの導入
そもそも mypy
とはPython用の静的型解析ツールです。
https://github.com/python/mypy
元々mypyそのものは導入されていましたが、まともに運用はしていない状況でした。 また、strictオプションを有効にした場合の変更差分が多い状況でした。
mypyのstrict化をしている間事業の更新を止めるわけにはいかないので、最初の方はstrictの設定をOFFにして、日々のスプリントの開発上で無理のない範囲でちょっとずつ対応していきました。
現在のmypyの設定情報は以下のような感じで現在運用しています。
ignore_missing_imports = True no_implicit_reexport = False strict = True
補足として
no_implicit_reexport
に関してはパッケージの引っ越し作業を行った影響で、明示的に残しています。(近いうちに削除予定です)ignore_missing_imports
に関してはサードパーティのライブラリに型情報のファイル(拡張子が.pyi
もの)が無いと、`error: Skipping analyzing "{パッケージ名}": module is installed, but missing library stubs or py.typed marker
というエラーが発生するので設定してます。本来は抑制は褒められた話ではないでしょうが、サードパーティのライブラリに個別に設定するのは現実的ではないでしょう。
対応してよかったこと
我々のチームでは外部libraryの更新にdependbotを利用しています。その対応時に問題があった際に、先にlintや型チェックで気付けたこともあり対応しててよかったなと感じました。
細かいところだと、安易なOptionalを利用している箇所があり、Optionalを外すために不要な複雑さを生んでいる箇所の発見にもつながったこともありました。おかげで、コードとしてシンプルさがあがったように思います。
また、一言にintといっても、金額なのか?どのような金額なのか?を対応前はコードジャンプして読み解く必要がありましたが、今は型を見れば済むので認知負荷の削減にもつながりました。
チームのナレッジ的にもPythonに詳しいメンバーがそこまで居ない状況だったので、型解析をはじめLintがおすすめするPythonのベストプラクティスに乗ることで特に困らず開発できているように感じています。
今後
現在我々のチームで扱っているPythonは3.9ですが、3.10, 3.11と型周りの力の入れようを感じているので、その恩恵に与れるように色々と追従はしていきたい所存です。 特に3.10で導入されたTypeGuardはcastでごまかしている部分もあるので使っていきたいなと思っています。 3.11では大幅速度改善をしているようなので、近いうちにアップデートしてAPIとしての品質も高めていきたいとかんがえています。
我々の開発チームはPHP/Go/Python/Vueとフルスタック・フルサイクルに開発しているチームなので、もしきょうみがあるよって方がいましたらDMや下記のlinkからの応募お待ちしています。 一緒に最高の開発体験を作っていきましょう!!
以下の採用リンクからのエントリーお待ちしています。
もしくは@glassmonekeyまでDMいただけると嬉しいです。
明日は同僚の@re_yuzuyさんによる、BASEカードの裏話と、デザイナーの@hotecoさんによる入社後の体験についての2本立てですです。楽しみですね!!