はてなの金次郎

Pythonエンジニアの技術系ブログ

保守性・可読性の高いPythonコードを実装するためにはどうすればよいか

はじめに

コードは理解しやすくなければいけない。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

コードの保守性や可読性を高めるために我々エンジニアはどんなことができるでしょうか?

  • テストを書く
  • 推奨されているコードスタイルに準拠する
  • コメントを書く
  • DRY原則に則る
  • 変更・拡張しやすく設計する
  • ログを出力する・監視する
  • 適切な命名をする
  • etc...

まだまだ意識すべきことはあると思いますが、上記の項目はエンジニアであれば恐らく一度は目にしたことがあるような内容であり、暗黙的に了承されたいルールです。 しかし、これらはただの心構えであり、体現するために実際には以下のような項目に落とし込む必要があります。

  • カバレッジを計測・可視化する
  • コードスタイルを統一する
  • 型チェックする
  • コードのメトリクスを計測する
  • ロガーを設定する
  • etc...

本記事ではこれらの項目をPythonで実践する方法を紹介させていただきます。

カバレッジを計測・可視化する

カバレッジを計測することでユニットテストで実行されたプログラムの割合を知ることができます。 カバレッジが100%であれば、そのプロジェクトにおけるプログラムの全ての行がユニットテストで少なくとも1度は実行されているということを意味します。

有名なプログラマーたちは「テストされていないコードは、壊れているコードだ」と言っています。

独学プログラマー Python言語の基本から仕事のやり方まで

独学プログラマー Python言語の基本から仕事のやり方まで

テストがされていないということは実際の環境でそのプログラムが実行されるまで期待通りに挙動するか確かではないということです。そのようなプロジェクトに一体誰が変更を加えたいと思うでしょうか?

カバレッジを100%にした方がいいということだろうか?したほうがいいじゃない。そうしろといっているんだ。書いたコードはすべてテストしなければいけない。まる。

Clean Coder プロフェッショナルプログラマへの道

Clean Coder プロフェッショナルプログラマへの道

カバレッジを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にカバレッジバッチを追加するとよいと思います。

f:id:gyuuuutan:20181229124915p:plain

GitLabであれば Badges という機能を利用するとカバレッジバッチをプロジェクトのトップページに自動追加してくれます。

f:id:gyuuuutan:20181229125655p:plain

カバレッジレポートを可視化する

ついでにHTMLで出力したカバレッジレポートも可視化しましょう。

https://jumpyoshim.gitlab.io/django-polls/

上記のURLはHTMLで出力されたカバレッジレポートのサンプルです。GitLabPagesに出力されたHTMLをホスティングしています。GitHubであればGitHubPagesを利用するとよいと思います。

このようにカバレッジカバレッジレポートは誰もがすぐに確認できる状態にしておくとよいでしょう。

ユニットテストの書き方

今回はPythonでのテストの書き方については言及しませんが、わかりやすい参考文献がありますので紹介させていただきます。DjangoCongress JP 2018 で発表されたセッションでtell-kさんによる「できる!Djangoでテスト!」です。

tell-k.github.io

また、こちらの資料でも紹介されていますが、aodag memoの「効果的なunittest - または、callFUTの秘密」は個人的にもとても勉強になったブログです。

コードスタイルを統一する

開発者が同じコーティング規約に準拠していることはとても重要です。統一されたスタイルガイドを利用することはコードの可読性を高めてくれるためです。

Pythonで推奨されるスタイルガイド、コーティング規約についてが PEP8 に記載されています。

例えば、インデントの仕方やモジュールのインポート方法までPythonをコーディングする際に推奨されるお作法がまとめられています。有志で日本語に翻訳されたものが公開されています。

はじめに — pep8-ja 1.0 ドキュメント

また、Pythonチュートリアルでも簡潔にまとめられています。

4. その他の制御フローツール — Python 3.7.2 ドキュメント

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での静的型付け開発を効率的に行えると思います。

mypy-lang.blogspot.com

コードのメトリクスを計測する

コードのメトリクスとはプログラムの複雑度や行数のことです。これらを計測することでプログラムのどの部分に潜在的なリスクがあるかがわかり、リファクタリングの対象が明確にすることができます。

例えば、以下のようなコードはメトリクスを計測することで指摘されます。

  • 行数が大きすぎる関数やメソッド、クラス
  • 条件分岐が多すぎる制御文
  • ネストが深すぎるコード

いずれもとても読みやすいと言えるようなコードではないことは直感的に理解していただけると思います。 コードのメトリクスを計測することでこういったコードをしっかりと把握できる状態にしておき、適切なリファクタリングをすることで読みやすくメンテナンスがしやすいコードにしましょう。

Pythonプログラムのメトリクスを計測してくれるのが RadonXenon です。

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)

上記のように実行することで問題があるブロックと平均ランクを出力してくれます。

qiita.com

オプションやランクについては上記の記事がとてもよくまとまっています。

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であれば CloudWatchGCPであれば 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の方法を紹介しています。

  1. 上述の設定メソッドを呼び出す Python コードを明示的に使って、ロガー、ハンドラ、そしてフォーマッタを生成する。
  2. ロギング設定ファイルを作り、それを fileConfig() 関数を使って読み込む。
  3. 設定情報の辞書を作り、それを 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で実行しなければあまり意味がありません。実践する際にはCIとともに実装することをおすすめします。

「他にもこういう方法がある」とか「ここは違うんじゃない?」といった意見があればぜひご教授ください!

最後になりましたが、これらのノウハウが習得できたのは優秀なチームメンバーのおかげです。ありがとうございます!