ごあいさつ
こんにちはNSS江口です。
少し前にロバストPythonの記事を書かせていただきましたが、今回はその中から依存関係について記事を書かせていただきます。
いつの間にかPythonが軟派な言語ではなくなっていた~ロバストPython~
依存関係
依存関係とはコードが動作するために必要とする別のコードとの関係のこととなります。
一般的に依存するコンポーネントの数が少なければ良いコードとされています。一方で依存先が増加することの要因として、モジュールの再利用が挙げられ、これもまた良いコードの特徴とされています。この相反するメリットの中でちょうどよいバランスを見つけるのが非常に苦労する所となります。
SonarLintなどでもよく依存関係チェックが走りますね。(依存関係は15個以下にしてください、とか)
規模が小さいうちは自分も他人も話し合いながら依存関係の上限を守ることができますが、
関わる方が増えて来ると、合意をとることが難しくなり、機械的な判定を諦めざるを得なくなってくることが多いですね。
依存関係のタイプ
依存関係には物理的・論理的・時間的の3種類があります。
- 物理的依存関係
- importや継承関係によって関係がなければソースコードが成り立たなくなる(ビルドが通らない)関係のこと、比較的把握がしやすい
- 論理的依存関係
- WebAPIのようにソースコードが成り立つ上での依存関係はないものの、サービス側のインタフェースの変更(エンドポイント変更やデータフォーマットの変更)により動作が影響を受けるような関係、ここから把握がしづらくなる
- 時間的依存関係
- 論理的依存関係と同様にソースコードが成り立つ上での依存関係はないものの、サービスを受け取る場合に必要な処理(利用前にconfigureメソッドを呼び出さなければならない等)
いわゆる静的コード解析の対象となるのは物理的依存関係のみとなります。
その他の依存関係については機械的な検出がなかなか難しいのですが、構築時に模索する中でできる限り親切で明確なインタフェースを心掛けるなどしかないかと思います。
WebAPI周りで論理的依存関係のトラブルが発生した経験としては、以下のことがありました。
- 新規項目を追加した際に提供側としては使わなければ良いだけだと思っていたとしても利用者側でJSONの文字列による単純差分チェックをしていたために誤検知してしまった
{
"code": "xxxx",
"price": 10000
}
// ↓ のように修正された。単純な文字列差分ロジックによっては誤検知される場合あり
{
"code": "xxxx",
"price": 10000,
"tax": 1000
}
- トラフィック軽減のためにJSONデータを
"name": null
といった宣言から項目自体を除去したところエラーが発生した - 無効な数値を
-9999999
からNaN
にしたらエラーが発生した - API仕様上特筆していないレコードの並び順が利用者側としては非常に重要で、利用者側でコード値によるソートをしなければならなくなった
提供側としては定めている仕様からは外れていないという言い分になるかもしれませんが、思った以上に暗黙的に要求される仕様は存在し、重要だったりします。
対策としては、事前アナウンスを丁寧に出すか、UATなどで実際に利用してもらうなどでしょうか。
私が以前WebAPIを設計する時ベストプラクティスを知るために参照した書籍は以下です。
Web API: The Good Parts
そういえば、時間的依存関係で思い出したことがあります。
以前JavaではJDBC接続を行う際に以下のようにクラス宣言を行い、JDBCドライバーを登録する必要がありました。
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test?user=xxx&password=yyy");
クラスロードによりstaticイニシャライザーを実行させて初めてJDBCドライバーへの登録が行われ、コネクションを取得できるようになるという寸法ですが、その後SPI(Service Provider Interface)
の登場により宣言する必要がなくなりました。
今考えると、この時の変更は時間的依存関係を廃止できただけではなく、既存のクラスロード処理があったとしても問題なく動作するというとても良い変更ですね。
依存関係の可視化
論理的・時間的依存関係の可視化は難しいかと思いますが、物理的依存関係はできるだけ容易に把握したいですよね。
そのためのツールがロバストPython内でいくつか記載されているため、それを紹介したいと思います。
パッケージの可視化
pipdeptree
とGraphViz
を利用することにより、パッケージが依存しているパッケージを整理(可視化)することができます
まずは https://graphviz.org/download/ よりGraphViz
をダウンロードし、インストールします。(pipdeptreeより実行されるため、GraphVizのインストールディレクトリはPATH
に追加するオプションにしてください。)
続いてpip
によりインストールを行います。
pip install pipdeptree graphviz
続いてpipdeptree
を実行し、GraphViz
で画像化します。
pipdeptree --graph-output svg --packages pydantic > deps.svg
--packages
を設定しなければ、全てのパッケージが対象となります。また、--exclude
で対象外とするパッケージの設定も可能です。
インポートの可視化
これを最も使うのではないでしょうか?pydeps
を利用することによりソースコードでimportでの依存関係を可視化することができます。
まずはインストールします。
pip install pydeps
次のコマンドで画像ファイルを作成します。
pydeps --show-deps qiita_aggregator.py -T svg -o deps.svg
ここで設定しているqiita_aggregator.py
は一定期間に投稿した記事のView数やいいね数を集計する自作ツールです。80行程度のプログラムですが、意外と依存関係があるとわかりますね。
関数呼び出しの可視化
インポートの可視化がどれだけ広く影響を与えているかを見られるのに対して、関数呼び出しはどれだけヘビーに利用されているか確認することができます。pyan3
によって確認することができます。
まずはインストールです。
pip install pyan3
ちなみに私の環境では解析の実行時にgraphvizとの互換性の問題により実行時に以下のエラーが発生しました。
TypeError: CallGraphVisitor.__init__() got multiple values for argument 'root'
その場合はあえてpyan3
をダウングレードするとよさそうです。
pip install pyan3==1.1.1
続いて先ほどと同じファイルを利用して実行します。
pyan3 *.py --grouped --annotated --html > deps.html
以上により呼び出される関数のパスを把握することができます。
多くの関数から呼び出されている関数は影響力が強いということになりますので、ユニットテストを作成してから修正を行う等の対策をする必要がありますね。
まとめ
Pythonのロバストネスを高めるための依存関係について勉強することが出来ました。
依存関係については多数の方にとっての最適解を求めるのは非常に困難なのですが、軽視していると自分たちの首をしめる可能性があるので、今後も追求してまいりたいと思います。