はじめに
本記事の読者対象
- Pythonの開発環境・ツールをさらに覚えたい方
- よりモダンに近いPython環境が欲しい方
想定していない方
- Python自体がはじめての方
- Python上級者
説明すること・しないこと
説明する
- ツールのおおまかな説明
- ツールを使用する理由・嬉しさ
- 参考になるドキュメント・URL
説明しない
- 具体的なコマンド
- 細かい文法
Modern Python
大学院で研究をするようになってから、かなりの時間Pythonを書くようになりました。
なぜならば、研究で利用しやすいライブラリが豊富であり、かつ研究のようなイテレーションがはやいプロジェクトに対して非常に有効であるためです。
しかし、Pythonは短期的にコードを試して動作を変更できる分、安定した動作が難しくなってきます。
たとえば、C++などはコンパイルを通す必要があるため、設計をうまく考えて実装する必要があります。
一方、Pythonはスクリプト言語なので最悪闇の設計
をすると設計が適当でもなんとかなってしまいます。
ただ、このような後先考えないコードは将来的に大きな負債になります。
実際、締め切りが近い論文に合わせて急ピッチで書いたコードが現在自分の前に立ちはだかり、また1から設計・実装し直す羽目になりました。
Pythonは良くも悪くも以下のような特徴があります。
- 型を設定しなくていい
- その分、変数名に思いを馳せることになる
- モジュールが多い
- バージョンがずれる
- パッケージマネージャーが豊富
- どれを使えばいいのかが分かりづらい
- 設計が甘くて良い
- 読み辛くなる
今回は、これらの問題を解決できる可能性があるツールについてまとめます。
ただし、実際の使い方や詳細については割愛しているため、他記事を参考ください。
自分もはやくPythonに慣れて中級者になっていきたいです。
パッケージマネージャー
Pythonのパッケージマネージャーには多くの種類の組み合わせがあります。
以前pythonの環境構築戦争にイラストで終止符をどうやら打てないという記事でPythonの環境の構築について説明しました。
そこから自分の理解もある程度進み、今のPythonの環境構築これでいいかな〜と感じるものがあったため、それについてまとめようと思います。
パターン1: pyenv + pipenv
pyenv
は複数のPythonのバージョンを管理できます。
具体的には、Python3.7、Python3.8、Python3.9をそれぞれインストールして好きに切り替えることができます。
このようにPythonのバージョンを切り替えることによって、複数のプロジェクトに対応できます。
たとえば、昔から続いているプロジェクトのコードはPython2.7でしか動かないんだ!みたいなこともあります。
そのため、pyenvでバージョンを切り替えられるとうれしいわけです。
しかし、pyenvはバージョンを切り替えることはできますが、仮想環境を作成することはできません。
この仮想環境とはプロジェクトごとに利用する環境のことを本記事では指します。
もし仮想環境を切り分けられない場合を考えてみます。
機械学習のプロジェクト開発を行った後、別のプロジェクトに割り当てられDjangoの開発になったとします。
このとき、機械学習のpytorchのライブラリはDjangoに直接必要はありません。
大量にライブラリが増えてくると動作も重くなり、必要なバージョンの整合性が取れなくなってきます。
そのため、プロジェクトごとにライブラリをインストールできるような仮想環境が必要なわけです。
この問題を解決すべく、pipenv
はあるバージョンのPythonにおいて複数の仮想環境を作成する
ことができます。
venvをラップするような便利な機能がpipenvにたくさん備わっているのでオススメです。
また、ここでは詳しくは述べませんが、ライブラリのバージョン管理をより賢く行えます。
まとめると、pyenv
でPython自体のバージョンを管理し、pipenv
で特定のPythonのバージョンにおける仮想環境を管理します。
Macではbrewでpipenv, pyenv両方インストールできます。
パターン2: pyenv + poetry
pyenvはさきほど出てきました。
今回、新しく出てきたのはpoetryになります。
poetryはrustっぽくライブラリ管理ができるマネージャーのようです。
まだ触ったことがないため詳しくないのですが、tomlファイル1つですべてを管理するらしいです。
かなり新しいマネージャーのようなので、今度触ってみたいです。
参考URL
型の導入
Pythonは型付けをしなくても動くスクリプト言語です。
そのため、初期段階ではかなりのスピードで開発できますが、後半になってくると変数に思いを馳せる時間が長くなり、実行時エラーに悩まされることが多くなります。
そこで、Python3.5から導入されたtyping
などを利用して型安全にコーディングできます。
しかし、プログラム実行時、変数に異なる型の内容が入っていてもエラーを出さないことに注意が必要です。
実行時に異なる型が入っていても止まりません。
しかし、IDEの恩恵を受けやすくなったり、第三者がコードの意味を理解しやすくなるため、積極的に書いていきましょう。
typing
def greeting(name: str) -> str:
return 'Hello ' + name
Python3.5から、上記のように変数や関数などに型を指定できるようになりました。
型を指定することで、IDEの恩恵を受けやすくなります。
また、長期的に見るとコーディングの効率化に繋がります。
Final
キーワードもあり、定数などをより安全に設定できます。
詳しくは参考URLを御覧ください。
data classes
データを格納するクラスを簡単に作成するデコレータなどを提供します。
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
namedtuple
名前をつけたタプルを宣言できます。
書き換えられないデータを保証できるので、変更しないものを管理するには便利そうです。
可能であればtyping.NamedTuple
を継承したクラスでタプルを宣言したほうがわかりやすいです。
class Employee(NamedTuple):
name: str
id: int
ジェネリクス
pydantic
FastAPIで採用されているライブラリです。
実行時に型情報を提供してくれます。
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
class User(BaseModel):
id: int
name = 'John Doe'
signup_ts: Optional[datetime] = None
friends: List[int] = []
external_data = {
'id': '123',
'signup_ts': '2019-06-01 12:22',
'friends': [1, 2, '3'],
}
user = User(**external_data)
型があっていないデータが入ると実行時でも例外を投げてくれます。
(一方typingなどは実行時エラーを投げません。そのため、型でおかしい箇所がよりわかりやすく、堅牢性が上がります。)
参考URL
- typings
- dataclasses
- namedtuple
- pydantic
- Pythonではじまる、型のある世界
- pydanticを使って実行時にも型情報が適用されるPythonコードを書く
- PythonとType Hintsで書くバックエンド
- Python 3.9 時代の型安全な Python の極め方
docstring
docstring
は、関数やクラスなどの情報を表す文字列です。
関数やクラスがどのような引数・属性を持つのか、どのような振る舞いをするのかを文字列で記述します。
def add(x:int, y:int) -> int:
"""add function.
xとyの和を計算する。
Attributes
----------
x: int
足される数
y: int
足す引数
Returns
-------
int
Notes
-----
こういう関数に普通はここまで書くことはない(わかるので)
"""
return x + y
docstringのメリット
docstringを書くことで以下のようなメリットがあります。
- 他のチームメンバーに関数やクラスの振る舞いを伝えることができる
- ドキュメントとコードの距離が近い
- IDEなどにおいて、型や補足情報がより詳細になる
- 後述しますが、
Sphinx
というドキュメント自動作成ツールでdocstringを利用できる - なにより将来の自分のためになる
スタイルの種類
docstringの書き方にはいくつかの流儀があります。
主なものは以下の3つです。
- reStructuredText
- NumPy
それぞれ書き方に特徴があります。
自分の好きな書き方を参照するのが良さそうです。
また、チームでdocstringの書き方が決まっていればその書き方にならいましょう。
docstringは書き方のリファレンスが少なく、何かのライブラリを参考にすると良さそうです。
参考URL
- NumPyスタイルPython Docstringsの例
- numpydoc docstring guide
- Pythonのドキュメントコメントの書き方(NumPyスタイル編)
- [Python]可読性を上げるための、docstringの書き方を学ぶ(NumPyスタイル)
- チームメイトのためにdocstringを書こう!
- numpy/numpy
- Pythonでdocstringを書く (Numpy-Style)
スタイルガイド
コードを複数人で書くと、それぞれ異なるコードの書き方の癖が現れます。
"
or '
、文字数、変数の付け方などさまざまです。
異なるコードスタイルを統一的にするべく、コードチェック・Lint・フォーマッターなどがあります。
これらを用いることでより統一的なPythonコードを書くことができます。
この節では、そのうち自分が使っているものについてのみ触れます。
そのため、これら以外にも他のLinterがあります。ぜひ調べてみてください。
pep
pep
は、python enhancement proposal
の略で、ドキュメント・コーディング規約を指します。
Pythonをより良くするための提案書であり、有名なのはpep8
です。
pep8は標準ライブラリなどのコーディング規約となっており、多くのPythonのコードはこのpep8を基準にしています。
flake8
flake8
はpipでインストールできるコードのフォーマットをチェックするツールです。
コーディング規約を守っているかをチェックしてくれます。
flake8は以下3つのライブラリのラッパーになっています。
- PyFlakes
- pycodestyle
- mccabe
flake8は、一行の文字数など、細かい規約の設定を行うことができます。
また、以下のようなflake8プラグインをpipインストールすると、flake8
のコマンドを実行したとき、自動でそれらのプラグインも実行してくれます。
- flake8-docstrings
- flake8-mypy
- flake8-isort
- flake8-print
black
black
はコードフォーマッターです。
flake8は規約違反の箇所を教える
ものでしたが、blackは実際にコードをフォーマット
します。
blackの特徴は比較的新しくできたものであり、そして変更できる設定がかなり少ないです。
そのため、blackを使っておけば多くのプロジェクトで似たような強制フォーマットになります。
blackは非常に使いやすいため、ぜひ使いたいですね。
mypy
mypy
は、コードのアノテーション・型を静的解析して、間違っている型を教えてくれます。
mypyのおかげで、間違っている箇所の型を治すだけで良くなります。
しかし、利用しているライブラリなどに対してエラーを出すこともあるため、そのときはスタブを生成したり、すでにpipで配布されているスタブをインストールする必要があります。
isort
isort
は、pythonのimportの順番を修正します。
flake8にisortプラグインがあるため、順番がおかしいと警告されたときにisortをすると良さそうです。
参考URL
- pep
- flake8
- black
- mypy
- Configuring Flake8
- IntelliJやPyCharmでPythonファイルの変更を検知してflake8で文法チェックをする
- Pythonによるパッケージ開発
- PythonでLintをする
設定ファイル
Pythonのプロジェクトを作る際はいくつかの設定ファイルが出てきます。
この節ではこれらの設定ファイルについて説明します。
setup.py
s
etup.pyはプロジェクトを第三者に向けて配布するために利用するファイルです。
setuptools`というモジュールを用いて、プロジェクトのファイルをpipでインストールできるようなパッケージを作成します。
パッケージの情報やインストールの方法、URLなどを記述します。
# https://packaging.python.org/tutorials/packaging-projects/?highlight=setup.py#creating-setup-py
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name="example-pkg-YOUR-USERNAME-HERE", # Replace with your own username
version="0.0.1",
author="Example Author",
author_email="author@example.com",
description="A small example package",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pypa/sampleproject",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.6',
)
普段何気なく書いているpip install numpy
というコマンドは、setup.pyで作成されたパッケージをオンライン(PyPI)から取得し、site-packages
というディレクトリに入れています。
プロジェクトをパッケージとしてオンラインで公開したい場合はsetup.py
を書きましょう。
MANIFEST.in
setup.py
でプロジェクトからパッケージを作成するときに、pythonファイル以外をパッケージに含みたいときがあります。
たとえば、画像ファイルや音声ファイルなどが考えられます。
このとき、MANIFEST.in
というファイルを作成することによって、より簡単にさまざま々なファイルをパッケージに含めてビルドできます。
setup.cfg
setup.py
はパッケージとして公開、インストールするために必要です。
しかし、直接Authorやインクルードするファイルなどを指定してしまうと、後から変更しづらくなります。
そこて、設定ファイルであるsetup.cfg
を追加で作成することによって、
パッケージで使う情報を独立して管理できます。
setup.pyを実行したときにsetup.cfg
があった場合は、情報を取り出して内容を上書きし、それからパッケージを作成します。
[metadata]
name = my_package
version = attr: src.VERSION
description = My package description
long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two
license = BSD 3-Clause License
classifiers =
Framework :: Django
License :: OSI Approved :: BSD License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
[options]
zip_safe = False
include_package_data = True
packages = find:
scripts =
bin/first.py
bin/second.py
install_requires =
requests
importlib; python_version == "2.6"
requirements.txt
pipでインストールしたパッケージのリストが表されたファイルです。
必ずしもrequirements.txt
という名前でなくても構いませんが、慣習的にこの名前になっています。
このファイルを作成してGitHubなどに含めることによって、
pip install -r requirements.txt
で簡単に第三者がパッケージをインストールできます。
しかし、依存性の解決に向いておらず、ライブラリのバージョンの更新をし辛いなどの問題がrequirements.txtには存在しています。
そのため、現在はpipenvではPipfile、poetryではpyproject.tomlという、別のパッケージ管理のファイルが利用されています。
Pipfile/Pipfile.lock
Pipfile
, Pipfile.lock
はrequirements.txtの問題を解決したpipenvの管理ファイルです。
Pipfileには、直接依存するライブラリが記入されます。
たとえば、requests
を使ってURLを叩くプロジェクトをつくる場合には以下のようなPipfileを作成します。
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
requests = "*"
このとき、requests
は他のライブラリに依存しています。
たとえば、chardet
と呼ばれるファイルの文字コードを判別するライブラリを内部で使用しています。
ここで、chardet
のバージョンはPipfileには記入されません。
一方で、ライブラリすべてのバージョンがPipfile.lock
に記入されます。
このとき、直接使用したいトップレベルのライブラリはPipfile
で管理できるため、簡単にバージョンのアップグレードを検討できます。
また、依存するライブラリすべてのバージョンはPipfile.lock
で管理されるため、第三者が同じ実行環境を簡単に用意できます。
これによって、依存性の問題をファイルを分けることで解決できました。
pyproject.toml
pyproject.toml
はPEP 518
で定義された、パッケージの設定を管理するファイルです。
最近ではpoetryというパッケージマネージャーがこのファイルを利用していますが、
poetryに限った設定ファイルではないらしいです。
対応している任意のパッケージマネージャーがpyproject.tomlを使えます。
これまでは、requirements.txt
, setup.py
, setup.cfg
, MANIFEST.in
など多くのファイルがパッケージ公開に必要でしたが
pyproject.tomlはこのファイルのみでこれらをすべて補う機能を果たします。
現在pipenvを使っていますが、poetryならびにpyproject.tomlに興味が湧いたため使ってみようと思います。
tox.ini
tox
は、Pythonのテストを自動化するライブラリです。
toxコマンドを打つことで、tox.ini
に書かれたテストの内容をすべて自動実行してくれます。
1つのpytest
ぐらいであれば、毎回そのコマンドを打つだけで良いです。
しかし、配布するバージョンに合わせて、python2.7
, python3.8
, python3.9
など複数バージョンでテストを行いたいことがあります。
また、flake8のようなコーディング規約を満たしているかのテストだけを行いたい場合もあります。
そこで、toxならびに設定ファイルtox.iniを使うことで、1コマンドのみですべてのテストを自動実行できます。
しかも、toxはtoxが扱う特別なディレクトリ内部に他のバージョンのPython環境を作成します。
そのため、テストごとに仮想環境が作成され、テスト間の依存がないという嬉しさもあります。
[tox]
# 使用する環境を指定する
# 名前が一致しているflake8-py38は[testenv:flake8-py38]を実行する
# py38は[testenv:py38]がないため[testenv]を実行する
envlist =
py38
flake8-py38
mypy-py38
[testenv]
deps = pipenv
# テストで実行するコマンド
# このコマンドはpipenv installするだけなので当然テストをパスする
commands =
pipenv install
[testenv:flake8-py38]
basepython = python3.8
description = 'check flake8-style is ok?'
commands=
pipenv install
pipenv run flake8 gym_md
# 設定ファイル
# https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations
[flake8-py38]
max-line-length = 88
[testenv:mypy-py38]
basepython = python3.8
description = 'check my-py is ok?'
commands =
pipenv install
pipenv run mypy gym_md
参考URL
- tox
- poetry
- Python Packaging User Guide¶
- ソースコード配布物を作成する
- pythonのsetup.pyについてまとめる
- Python パッケージ管理技術まとめ
- 【Techの道も一歩から】第21回「setup.pyを書いてpipでインストール可能にしよう」
- Pipfile/Pipfile.lockの必要性 -requirements.txtを用いる手法と比較して-
- Pythonのパッケージ周りのベストプラクティスを理解する
- Python パッケージングの標準を知ろう
- Pythonによるパッケージ開発
PyPI
PyPIは、pythonのライブラリをアップロードできるサイトです。
pip install
をした場合などは、ここからダウンロードされています。
テスト
pythonには複数のテストツールがあります。
unittest
標準のテストライブラリはunittest
です。
標準パッケージに含まれているため、インストールせずにテストを書けます。
TestCaseクラスを継承し、testから始まるメソッドを作ります。
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
pytest
pytest
は、サードパーティ製のテストライブラリです。
pytestは関数をベースにテストを行い、unittestよりも詳細なエラーを出してくれます。
たとえば、以下のように出力値が間違っていた場合に、間違えた場所とその値を出力します。
pytestは簡単にかけて出力値もわかりやすいため、私はpytestを使っています。
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_sample.py F [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================
doctest
doctest
は、さきほど出てきたdocstring
でテストを実行することを可能とします。
doctestをインポートすると、もしdocstringに>>>
で実行例が書いてあったらその通りの動作になるかをテストしてくれます。
pytest
のように、複雑なテストを書くことはできません。
しかし、docstringの中にあるため、テストとして利用しながら実行例として第三者に提示できます。
そのため、コードがどのような動作をするのか理解しやすく、コードも変更しやすくなります。
def square(x):
"""Return the square of x.
>>> square(2)
4
>>> square(-2)
4
"""
return x * x
if __name__ == '__main__':
import doctest
doctest.testmod()
tox
さきほど出て来ましたが、toxを書くことで複数のテスト・コマンドを自動化できます。
参考URL
ドキュメント
Sphinxは、きれいなドキュメントを簡単につくることができるツールです。
Pythonのライブラリのリファレンスの多くは、このSphinx
で作成されています。
Sphinxは、reStructuredText
と呼ばれるマークアップ言語を用いてドキュメントを作成します。
このとき、自動でドキュメントを作成する機能であるsphinx-apidoc
が付属しており、docstringをPythonコードにきちんと書いていれば、コマンド一発でドキュメントを作成できます。
そのため、docstringを書くと型や情報などを将来のために残すことができ、しかもそれがそのままリファレンスになります。
これによって、ドキュメントがコードに比べて更新されなくなり、ドキュメントが形骸化してしまうという負債も発生しづらくなりますね。
ただし、shpinx-apidoc
によって作成されるbstファイルはデフォルトのため、立派なものにするためには自分で追加編集する必要があります。
参考URL
cookiecutter
cookiecutter
は、Pythonのきれいなプロジェクトを簡単に作成することができるツールです。
利用できるテンプレートがGitHubで公開されており、このテンプレートを指定するとよく設計されたプロジェクトを簡単にローカルに作成できます。
boilerplateに自分の情報を設定しながら
ローカルに作成できるツールと考えると良さそうです。
たとえば、cookiecutter-pypackageを指定すると簡単に以下が設定されたプロジェクトを作成できます。
- 洗練されたプロジェクト設計
- TravisCIを用いたテスト自動化
- Shpinxを用いたドキュメント作成
- toxを用いた複数環境でのテスト
- PyPIへの自動リリース
- CLIインターフェイス(click)
これまで説明してきたものが一括で用意できるのでうれしいですね。
参考URL
さいごに
Pythonの開発ツールについてまとめてきました。
まとめる中で、自分もまだまだ理解が浅い、使いこなせていないと実感しました。
pythonic
なコードが書けるように、毎日練習していきたいです。