BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

型安全なPythonで堅牢なアプリケーション開発

この記事はBASE Advent Calendar 2022の19日目の記事その2です。

こんにちは。BASE 株式会社 New Division BASE BANK Section にて、Engineering Program Managerをしている永野(@glassmonekey) です。

私個人としては、今年のアドベントカレンダー2回目です。 前回は以下の記事で普段の仕事への取り組みざまを書いたのでそちらもよかったらご覧ください。

devblog.thebase.in

今回の記事の趣旨としては、私が主に担当しているYELL BANKというプロダクト開発で行ったPython の改善ネタになります。

ちなみにYELL BANKについては同僚のyuniさんが熱く語ってくれたのでそちらもご覧ください。

devblog.thebase.in

YELL BANKのプロダクトの裏側

YELL BANK では、過去の売上データを元に提供金額を算出し、BASE加盟店さまの規模感に応じた資金調達体験を実現しています。

その際以下のフローで金額の提示を行います。

  1. 売上予測といった機械学習の内容を返却するAPIから金額算出のための情報を取得する
  2. 我々の事業状況に応じて提供金額算出 API が提示する金額の計算を行う
  3. 計算した金額を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からの応募お待ちしています。 一緒に最高の開発体験を作っていきましょう!!

以下の採用リンクからのエントリーお待ちしています。

open.talentio.com

もしくは@glassmonekeyまでDMいただけると嬉しいです。

明日は同僚の@re_yuzuyさんによる、BASEカードの裏話と、デザイナーの@hotecoさんによる入社後の体験についての2本立てですです。楽しみですね!!