保守性・可読性の高いPythonコードを実装するためにはどうすればよいか
はじめに
コードは理解しやすくなければいけない。
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- 作者:Dustin Boswell,Trevor Foucher
- 発売日: 2012/06/23
- メディア: 単行本(ソフトカバー)
コードの保守性や可読性を高めるために我々エンジニアはどんなことができるでしょうか?
まだまだ意識すべきことはあると思いますが、上記の項目はエンジニアであれば恐らく一度は目にしたことがあるような内容であり、暗黙的に了承されたいルールです。 しかし、これらはただの心構えであり、体現するために実際には以下のような項目に落とし込む必要があります。
- カバレッジを計測・可視化する
- コードスタイルを統一する
- 型チェックする
- コードのメトリクスを計測する
- ロガーを設定する
- etc...
本記事ではこれらの項目をPythonで実践する方法を紹介させていただきます。
- はじめに
- カバレッジを計測・可視化する
- コードスタイルを統一する
- 型チェックする
- コードのメトリクスを計測する
- ロガーを設定する
- おわりに
カバレッジを計測・可視化する
カバレッジを計測することでユニットテストで実行されたプログラムの割合を知ることができます。 カバレッジが100%であれば、そのプロジェクトにおけるプログラムの全ての行がユニットテストで少なくとも1度は実行されているということを意味します。
有名なプログラマーたちは「テストされていないコードは、壊れているコードだ」と言っています。
独学プログラマー Python言語の基本から仕事のやり方まで
- 作者:コーリー・アルソフ
- 発売日: 2018/02/24
- メディア: 単行本
テストがされていないということは実際の環境でそのプログラムが実行されるまで期待通りに挙動するか確かではないということです。そのようなプロジェクトに一体誰が変更を加えたいと思うでしょうか?
カバレッジを100%にした方がいいということだろうか?したほうがいいじゃない。そうしろといっているんだ。書いたコードはすべてテストしなければいけない。まる。
- 作者:Robert C. Martin
- 発売日: 2012/01/27
- メディア: 大型本
カバレッジを100%で維持し続けるというのは現実的な目標だとは思えませんが、カバレッジが高い水準であるということはソースコードの変更に対するエンジニアの心理的なハードルを下げることにつながります。
結果、サードパーティやOSなどのバージョンアップを迅速に適用できたり、機能追加を行いやすくなったりします。そのためにまず実行するべきはプロジェクトのカバレッジを計測し現状を認識することです。計測した結果、カバレッジが高い水準とは言えない場合にするべき行動は、テストを書き加え、先にテストを書く文化を作ることです。
「テストなんかあとで書けるよ」と言うかもしれない。だが、それは違う。あとで書くことはできないのだ。本当に。あとで書けるテストも少しはあるだろう。注意深く計測していけば、あとでカバレッジ率を高めることもできるだろう。だが、あとで書いたテストは防御なのだ。先に書くテストは攻撃である。あとで書くテストは、コードを書いた人や問題の解決方法を知っている人が書くものだ。こうしたテストは、先に書くテストほど鋭いものではない。
by Clean Coder
Pythonでカバレッジを計測するにはunittestであれば Coverage.py, pytestであれば pytest-cov がよく見かける選択肢ではないかと思います。
Coverage.py
Coverage.pyは、Pythonコードのカバレッジを計測するためのサードパーティです。 テストでコードのどの部分が実行されたのか監視することで、実行されたコードの割合を計測したり実行されていないコードを特定して可視化してくれたりします。
1. インストールする
$ pip install coverage
2. coverage run
コマンドでプログラムを実行しデータを集計する
$ coverage run my_program.py arg1 arg2
3. coverage report
コマンドで結果をカバレッジレポートをコンソールに出力する
$ coverage report -m Name Stmts Miss Cover Missing ------------------------------------------------------- my_program.py 20 4 80% 33-35, 39 my_other_module.py 56 6 89% 17-23 ------------------------------------------------------- TOTAL 76 10 87%
4. coverage html
でより詳細なカバレッジレポートをHTMLで出力する
$ coverage html
.coveragerc
という設定ファイルにカバレッジを計測しないファイルを指定したり、カバレッジレポートを生成するディレクトリ名を指定したりすることができます。下記はその例です。
[run] branch = True source = . [report] ignore_errors = True omit = */migrations/* */__init__.py */const.py */tests.py */tests/* */apps.py */manage.py */wsgi.py */urls.py */settings.py [html] directory = coverage
pytest-cov
pytestのプラグインでカバレッジを計測するためのサードパーティです。Coverage.pyのオプションはほぼすべてサポートしているようです。
1. インストールする
$ pip install pytest-cov
2. --cov
オプションでカバレッジレポートをコンソールに出力する
$ py.test --cov=myproj tests/ -------------------- coverage: ... --------------------- Name Stmts Miss Cover ---------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94%
3. --cov-report=html
オプションでより詳細なカバレッジレポートをHTMLで出力する
$ py.test --cov=myproj --cov-report=html tests/
pytest.ini
というファイルにpytestのオプションを設定できます。毎回指定するようなオプションはこのファイルに設定しておくと便利です。下記はその例です。
[pytest] addopts= --cov=. --cov-report=term-missing --cov-config=.coveragerc
カバレッジを可視化する
せっかく計測したカバレッジは可視化しなければ意味がありません。
GitHubであればCI/CDツールやCodecov のようなサービスをうまく活用することでREADMEにカバレッジバッチを追加するとよいと思います。
GitLabであれば Badges という機能を利用するとカバレッジバッチをプロジェクトのトップページに自動追加してくれます。
カバレッジレポートを可視化する
ついでにHTMLで出力したカバレッジレポートも可視化しましょう。
https://jumpyoshim.gitlab.io/django-polls/
上記のURLはHTMLで出力されたカバレッジレポートのサンプルです。GitLabPagesに出力されたHTMLをホスティングしています。GitHubであればGitHubPagesを利用するとよいと思います。
このようにカバレッジやカバレッジレポートは誰もがすぐに確認できる状態にしておくとよいでしょう。
ユニットテストの書き方
今回はPythonでのテストの書き方については言及しませんが、わかりやすい参考文献がありますので紹介させていただきます。DjangoCongress JP 2018 で発表されたセッションでtell-kさんによる「できる!Djangoでテスト!」です。
また、こちらの資料でも紹介されていますが、aodag memoの「効果的なunittest - または、callFUTの秘密」は個人的にもとても勉強になったブログです。
コードスタイルを統一する
開発者が同じコーティング規約に準拠していることはとても重要です。統一されたスタイルガイドを利用することはコードの可読性を高めてくれるためです。
Pythonで推奨されるスタイルガイド、コーティング規約についてが PEP8 に記載されています。
例えば、インデントの仕方やモジュールのインポート方法までPythonをコーディングする際に推奨されるお作法がまとめられています。有志で日本語に翻訳されたものが公開されています。
また、Pythonのチュートリアルでも簡潔にまとめられています。
4. その他の制御フローツール — Python 3.9.1 ドキュメント
PEP8に準拠しているかどうかをチェックしてくれるツールが flake8 、インポートのスタイルについてチェックしてくれるのが isort です。
flake8
PyFlakes
, pycodestyle
, Ned Batchelder's McCabe script
のラッパーでflake8のコマンドを実行することですべてのツールが実行されます。
1. インストールする
$ pip install flake8
2. flake8コマンドを実行する
ファイルまたはディレクトリを指定することができます。
$ flake8 path/to/code/to/check.py
# or
$ flake8 path/to/code/
また、チェックするルール、無視するルールを指定することもできます。
$ flake8 --select E123,W503 path/to/code/
$ flake8 --ignore E24,W504 path/to/code/
flake8のオプションはsetup.cfg
という設定ファイルに設定することもできます。
以下はその例です。
[flake8] exclude = .git,*migrations*,__init__.py max-line-length = 100
isort
インポートのスタイルは個人によって癖が出やすい部分ではないでしょうか。isortはインポートをアルファベット順にソートし、自動的にセクションに分割してくれるツールです。
1. インストールする
$ pip install isort
2. isortコマンドを実行する
$ isort -rc .
isortのオプションはflake8と同じく setup.cfg
で設定することができます。以下がその例です。
[isort] line_length = 100 known_django = django sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER
また、flake8-isort をインストールするとisortで設定したオプション通りにインポートがソートされているかチェックしてくれます。
型チェックする
変数の型が明示されているとプログラムの理解を助けてくれたり、デバックを容易にしてくれたりします。
Python3.5以上であれば型アノテーション(PEP484)の構文を利用することが可能です。この型アノテーションをチェックしてくれるのが mypy です。
mypy
Pythonの型をチェックしてくれる静的ツールです。PEP484 の型アノテーションが適用されているプログラムをチェックすることができます。
1. インストールする
$ pip install mypy
2. mypyコマンドを実行する
$ mypy program.py
また、型アノテーションを自動生成してくれる PyAnnotate というサードパーティもあるのでうまく活用するとPythonでの静的型付け開発を効率的に行えると思います。
コードのメトリクスを計測する
コードのメトリクスとはプログラムの複雑度や行数のことです。これらを計測することでプログラムのどの部分に潜在的なリスクがあるかがわかり、リファクタリングの対象が明確にすることができます。
例えば、以下のようなコードはメトリクスを計測することで指摘されます。
- 行数が大きすぎる関数やメソッド、クラス
- 条件分岐が多すぎる制御文
- ネストが深すぎるコード
いずれもとても読みやすいと言えるようなコードではないことは直感的に理解していただけると思います。 コードのメトリクスを計測することでこういったコードをしっかりと把握できる状態にしておき、適切なリファクタリングをすることで読みやすくメンテナンスがしやすいコードにしましょう。
Pythonプログラムのメトリクスを計測してくれるのが Radon や Xenon です。
Radon
Radonはコメント行や空白行、複雑度などさまざまなコードメトリクスを計測することができるツールです。
1. インストールする
$ pip install radon
2. radonコマンドを実行する
$ radon cc sympy/solvers/solvers.py -a -nc sympy/solvers/solvers.py F 346:0 solve - F F 1093:0 _solve - F F 1434:0 _solve_system - F F 2647:0 unrad - F F 110:0 checksol - F F 2238:0 _tsolve - F F 2482:0 _invert - F F 1862:0 solve_linear_system - E F 1781:0 minsolve_linear_system - D F 1636:0 solve_linear - D F 2382:0 nsolve - C 11 blocks (classes, functions, methods) analyzed. Average complexity: F (61.0)
上記のように実行することで問題があるブロックと平均ランクを出力してくれます。
オプションやランクについては上記の記事がとてもよくまとまっています。
Xenon
Xenon (キセノン) は Radon をベースとしているコードメトリクスの計測ツールですが、 要求に満たない結果だった場合に失敗( non-zero
を出力 )します。これの何がよいかというと、CIで Xenon を実行するように設定することで強制的にリファクタリングの機会を提供することができます。
コードメトリクスによるより厳密なリファクタリング文化を醸成したいときには Xenon を利用するとよいかもしれません。
1. インストールする
$ pip install xenon
2. xenonコマンドを実行する
$ xenon --max-absolute B --max-modules A --max-average A
--max-absolute
はブロックに対する絶対しきい値、 --max-modules
はモジュールに対するしきい値、 --max-average
は平均複雑度のしきい値を設定できるオプションです。指定されたランクより低い箇所がひとつでもあった場合、 non-zero
を返却します。
ロガーを設定する
障害が発生した際にその原因を素早く追及できることは保守性を高める上では欠かせない要素の一つです。 障害発生の調査をしやすくするためには適切な ログ出力 と エラー通知 がされていることが重要です。
エラー通知に関しては Datadog
や、AWSであれば CloudWatch
、GCPであれば Stackdriver
などの監視ツールを利用する必要があり、Pythonの責務からは離れているので今回は割愛させていただきます。
Pythonでログ出力をするために便利なのが logging というPythonプログラム内で起きたイベントを記録するための標準ライブラリです。
logging
logging はあるプログラムが実行されているときに起こったイベントを追跡するための手段です。イベントにはメッセージやロガーレベル、日時などを含めることができます。
loggingを利用するにあたって理解しておきたいのはロガー、ハンドラ、フィルタ、フォーマッタの役割です。
ロガーは、アプリケーションコードが直接使うインタフェースを公開します。
ハンドラは、(ロガーによって生成された) ログ記録を適切な送信先に送ります。
フィルタは、どのログ記録を出力するかを決定する、きめ細かい機能を提供します。
フォーマッタは、ログ記録が最終的に出力されるレイアウトを指定します。
参考: https://docs.python.org/ja/3/howto/logging.html#advanced-logging-tutorial
1. ロガーを作成する
import logging logger = logging.getLogger('simple_example')
2. ハンドラを作成する
handler = logging.StreamHandler()
3. フィルタを作成する
logger.setLevel(logging.INFO) handler.setLevel(logging.INFO)
4. フォーマッタを作成してハンドラに追加する
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
5. ハンドラをロガーに追加する
logger.addHandler(handler)
6. ログを出力する
logger.debug('debug message') # 何も出力されない logger.info('info message') # 2019-01-08 12:18:45,965 - simple_example - INFO - info message logger.warning('warning message') # 2019-01-08 12:18:45,967 - simple_example - WARNING - warning message logger.error('error message') # 2019-01-08 12:18:45,968 - simple_example - ERROR - error message logger.critical('critical message') # 2019-01-08 12:18:45,971 - simple_example - CRITICAL - critical message
上記の方法以外にもロガー、ハンドラ、フィルタ、フォーマッタの設定方法があります。今回は1の方法を紹介しています。
- 上述の設定メソッドを呼び出す Python コードを明示的に使って、ロガー、ハンドラ、そしてフォーマッタを生成する。
- ロギング設定ファイルを作り、それを fileConfig() 関数を使って読み込む。
- 設定情報の辞書を作り、それを dictConfig() 関数に渡す。
参考:https://docs.python.org/ja/3/howto/logging.html#configuring-logging
Djangoを利用する場合は LOGGING
という環境変数にロガー、ハンドラ、フィルタ、フォーマッタの設定ができます。以下はその例です。
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'simple': { 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s' } }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'simple' }, }, 'loggers': { '': { 'handlers': ['console'], 'level': 'INFO', 'propagate': True, }, }, }
参考:https://docs.djangoproject.com/ja/2.1/topics/logging/
上記の参考文献はDjango公式ドキュメントのロギングに関するページですが、「クイックロギング入門」という章はロギングに関してとてもわかりやすくまとまっています。Djangoが知らなくても読める章なのでぜひ一度目を通してみてください。
おわりに
Coverage.py, pytest-cov, flake8, isort, mypy, Radon, Xenon, logging といったライブラリを活用して保守性・可読性が高いPythonコードが実装できないか模索してみました。(Django成分がやや多めになってしまいました。すみません。)
これらは決して保守性・可読性が高いPythonコードを実装するためのベストプラクティスというわけではなく、およそ2年間Pythonともがき苦しんできた1プログラマの血と涙の結晶だと思っていただければ幸いです。
また、少し触れている章もありましたが、これらはCIで実行することでよりその効果を得ることができます。むしろCIで実行しなければあまり意味がありません。継続的に実行し続ける仕組みが重要です。
「他にもこういう方法がある」とか「ここは違うんじゃない?」といった意見があればぜひご教授ください!
最後になりましたが、これらのノウハウが習得できたのは優秀なチームメンバーのおかげです。ありがとうございます!