2025/03/06に公開
Set up virtual environments
Pythonで開発・実験を行う際にはプロジェクトごとに環境を切り替える。こういった一時的なPythonの実行環境のことを「仮想環境」と呼ぶ。複数のプロジェクトで同一の環境を用いることは推奨しない。
Management tools
仮想環境のマネジメントツールとしてuvの利用を推奨する。uvはPythonパッケージとプロジェクトを管理するツールで、動作の高速さの効率性などで注目を集めているツールである。uvはPythonパッケージだけでなくPythonのバージョン自体も管理することが可能なため、プロジェクトごとに異なるPythonバージョンや依存関係を管理するのに適したツールだと言える。uvの使い方については後のセクションで簡単に説明する。
他の選択肢の一つとしてcondaがあり、uvと同様にPythonのバージョン管理や依存関係の管理の両方が可能な上で、非Pythonパッケージの依存関係も管理可能だという利点がある。これはuvにはみられない特徴である。さらにAnacondaを利用すれば、condaだけでなくnumpyやscipyなどの、データサイエンスには重要なPythonライブラリがプリインストールされた環境が用意されているという面でuvと差別化される。ただしAnacondaは一定以上の規模の企業で利用する場合に、ライセンス料がかかってしまう点には注意したい。
また別の選択肢としては、pyenvにpoetryなどのツールを組み合わせる方法もある。pyenvはPythonバージョンの管理ツールであり、poetryは依存関係やパッケージングの管理をするツールである。他にPythonパッケージの依存関係を管理するツールの代表的なものにvirtualenvやpipenvなどがあり、これらをpyenvと組み合わせても良い。uvはまだ歴史が浅く発展途上であるため、破壊的な変更が加えられる可能性も高い。開発期間が長いpyenvやpoetryなどのツールの方が安定した利用が見込め、そういった観点ではこちらの選択肢もまだあり得るだろう。
ここまで仮想環境の管理方法に様々な選択肢を挙げたため、Pythonパッケージの依存関係を管理できるツールの特徴を下の表にまとめた。
- | uv | rye | conda | poetry | venv | virtualenv | pipenv |
---|---|---|---|---|---|---|---|
Pythonのバージョン管理 | 可能 | 可能 | 可能 | pyenv等と併用 | pyenv等と併用 | pyenv等と併用 | pyenv等と併用 |
仮想環境の再現 | uv.lock | requirements.lock, requirements-dev.lock | environment.yaml | poetry.lock | 難しい | 難しい | pipfile.lock |
利用可能ライブラリ | 多い | 多い | 限定的かつタイムラグあり | 多い | 多い | 多い | 多い |
依存関係の分離 | 可能 | 可能 | 不可能 | 可能 | 不可能 | 不可能 | 可能 |
ビルド自動化 | 可能 | 可能 | 不可能 | 可能 | 不可能 | 不可能 | 不可能 |
非Pythonパッケージの管理 | 不可能 | 不可能 | 可能 | 不可能 | 不可能 | 不可能 | 不可能 |
備考 | pipよりも高速 | uvでいい | pipとの併用で環境が壊れる可能性がある | - | python(3.3以降)に標準搭載 | venvでいい | - |
How to use uv (a simple introduction)
uvのインストールについてはこちらのリンクを参考にすること。この資料でのuvのバージョンはuv 0.6.0 (591f38c25 2025-02-14)
である。
Manage Python versions
Pythonは扱うライブラリによって依存するバージョンが異なるため、プロジェクト単位で切り替えるものとし、uvによってそれを実現する。
最新のPythonバージョンをインストールする場合。型や新しいシンタックスが使えるとコーディングの質が上がるため、できるだけ新しく安定したバージョンを入れると良いだろう。
uv python install
特定のバージョンを指定する場合は以下のようにする。
uv python install 3.12.9
インストールされているPythonのバージョンをリスト化。
uv python list
> cpython-3.13.2-macos-aarch64-none .local/share/uv/python/cpython-3.13.2-macos-aarch64-none/bin/python3.13
> cpython-3.12.9-macos-aarch64-none .local/share/uv/python/cpython-3.12.9-macos-aarch64-none/bin/python3.12
uv python pin
でプロジェクトで有効になるPythonバージョンを指定する。この時、.python-version
というファイルが生成される。
uv python pin 3.12.9
# optional
uv run python -V && cat .python-version
> Python 3.12.9
> 3.12.9
Manage Python package dependencies
前述した通りuvはPythonパッケージの依存関係を管理するツールという側面もあり、poetryやcondaと競合関係にある。ライブラリ間のバージョンが破綻しないように依存解決を解決したり、tomlファイルに様々な設定を組み込めたりするためバニラなpipと比べて利点が多い。
プロジェクトの初期化。
# for example
mkdir codebook
cd codebook
uv init --name codebook --package --python 3.12.9
Pythonライブラリのインストール。
uv add jupyterlab numpy pandas
仮想環境にインストールされているライブラリは以下のコマンドで可視化できる。
uv pip list
仮想環境内でのコマンド実行はuv run
をコマンドの先頭に追加することで実現できる。codebook.main()
はパッケージとしてプロジェクトを初期化した時に自動で生成される関数である。
uv run python -c "import codebook; codebook.main()"
> Hello from codebook!
Project code structure
Projectの構成例を紹介する。 ディレクトリ構成は以下の通り。
.
├── README.md (1)
├── conf/ (2)
├── data/ (3)
├── notebooks/ (4)
├── pyproject.toml (5)
├── setup.cfg (6)
└── src/ (7)
- ProjectのHow-toを示す。
- 一貫して参照させたい実験の設定値などを、例えばsecrets.ymlやparameters.ymlとしてまとめておき、OmegaConfやHydraから扱うと便利。
- データファイルの一時的なスペースとして利用する。前処理済のデータや中間ベクトルやモデルのPickleファイル、軽いデータの場合は直接ここに置いてもいい。data/フォルダを更に整理するTipsはKedroの資料が参考になる。
- 実験で最も使うであろうJupyterLab Notebookのファイルを置く。Notebookの管理方法についてはKeep readableで説明する。
- 前述したPythonのパッケージ管理ツールで使われる設定ファイル。
- pyproject.tomlに対応していないライブラリはこちらに設定を定義する。
- パッケージ化したPython codeを置く。実験中何度も使うような処理は関数やクラス化してsrc/配下にまとめ、Notebookから呼び出して扱うのが理想。
Keep readable
Follow coding regulations
公式のコーディング規約を熟読する。変数名・関数名・モジュール名はスネークケース (e.g. variable_nam
, my_module.py
) 、クラス名はキャメルケース (e.g. MyClass
) を用いるといった命名規則に始まり、インデントやimportの規約が細かく決まっている。 規約のないJupyter Notebook名もルールを決めてスタイルを統一する。例えば、10_CamelCase.ipynb
、10_snake_case.ipynb
など。Prefixに数字を使うなら優先させたいindexやissueと紐付ける。Notebookの整理に関しては後述する。また、使っていないコードは消す。Version管理をGitで行い、安直にコメントアウトを使わない。
Type hinting
Python 3.5から型を明示的に指定できる、Type Hintsが実装されている。動的型付け言語であるPythonであえて型をつけることで引数や返り値が予測しやすくなり、バグやコード共有時等のリーディングコストを抑えることができる。Pythonの動的型付けの特性は維持されることには注意していただきたい。慣れるまでは全てに型を書く必要はなく、特にわかりにくそうな関数や変数に付ける程度でいい。
ArrayLike Objects
numpyのような行列を扱う変数は型に加えて次元数もチェックしておくと不具合を起こす可能性が下がる。まずは、Type hintingだけを行う場合を示す。
>>> import numpy as np
>>> import numpy.typing as npt
>>>
>>>
>>> def np_add(
... a: npt.NDArray[np.float32], b: npt.NDArray[np.float32]
... ) -> npt.NDArray[np.float32]:
... return a + b
...
>>>
>>> np_add(
... np.ones([4, 4], dtype=np.float32),
... np.ones([4, 4], dtype=np.float32),
... )
array([[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.]], dtype=float32)
上記の例では変数の型は予測できるが、行列の次元数が分からないためにIndexErrorなどを起こしてしまうことがある。
>>> def np_add(
... a: npt.NDArray[np.float32], b: npt.NDArray[np.float32]
... ) -> npt.NDArray[np.float32]:
... dim2 = a.shape[2]
... ...
...
>>>
>>> np_add(
... np.ones([4, 4], dtype=np.float32),
... np.ones([4, 4], dtype=np.float32),
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in np_add
IndexError: tuple index out of range
そういった不具合を減らすためには、assert文を適宜用いて次元数を把握しやすくすることが有効である。以下の例ではaとbの次元が(4, 4)であることが一目で確認できる。
>>> def np_add(
... a: npt.NDArray[np.float32], b: npt.NDArray[np.float32]
... ) -> npt.NDArray[np.float32]:
... assert (
... a.shape == b.shape == (4, 4)
... ), f"Each shape of 'a' and 'b' is expected to be (4, 4). But got {a.shape} and {b.shape}."
... return a + b
...
>>>
>>> np_add(
... np.ones([4, 4], dtype=np.float32),
... np.ones([4, 4], dtype=np.float32),
... )
array([[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.]], dtype=float32)
>>>
>>> np_add(
... np.ones([1, 4, 4], dtype=np.float32),
... np.ones([1, 4, 4], dtype=np.float32),
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in np_add
AssertionError: Each shape of 'a' and 'b' is expected to be (4, 4). But got (1, 4, 4) and (1, 4, 4).
また、次元数だけでなく各要素の値の範囲などをassertしても良い。
>>> assert a.min() >= 0 and a.max() <= 1, "'a' is expected to be in [0, 1]."
3rd party tools
ArrayLike objectsの節ではassert文を用いて、Type Hintsの補助を行いつつランタイムで型チェックを行う方法を示したが、行列を扱うような関数の全てにassert文を書くのは労力の伴う作業である。assert文を書くのが面倒な場合は、beartypeやpydanticなどの3rd party製のライブラリを用いると可読性の高いType Hintsを行いながらランタイムでの型チェックを実現することができる。
ここでは例としてbeartypeの使い方を紹介する。beartypeはデコレータを使って関数の引数や返り値の型チェックを行う。
>>> from typing import Annotated
>>>
>>> import numpy as np
>>> import numpy.typing as npt
>>> from beartype import beartype
>>> from beartype.vale import Is
>>>
>>> Array4x4 = Annotated[npt.NDArray[np.float32], Is[lambda x: x.shape == (4, 4)]]
>>>
>>>
>>> @beartype
... def np_add(a: Array4x4, b: Array4x4) -> Array4x4:
... return a + b
...
>>>
>>> np_add(
... np.ones([4, 4], dtype=np.float32),
... np.ones([4, 4], dtype=np.float32),
... )
array([[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.]], dtype=float32)
>>>
>>> np_add(
... np.ones([1, 4, 4], dtype=np.float32),
... np.ones([1, 4, 4], dtype=np.float32),
... )
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<@beartype(__main__.np_add) at 0x1051109a0>", line 48, in np_add
beartype.roar.BeartypeCallHintParamViolation: Function __main__.np_add() parameter a="array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1.,...)" violates type hint typing.Annotated[numpy.ndarray[typing.Any, numpy.dtype[numpy.float32]], Is[<lambda>]], as <class "numpy.ndarray"> "array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1.,...)" violates validator Is[<lambda>]:
False == Is[<lambda>].
このように記述することで行列の形状を把握しやすくなり、かつランタイムでの型チェックを行うことができる。より詳しい使い方は公式のREADMEを参照すると良い。
Static type checking
Type Hintsを記述できたら、mypyで静的な型チェックを行なってコードの整合性を確認すると良い。
install
pip
pip install mypy
uv
uv add mypy # or uv add --dev mypy
usage
例として以下のようなファイルをmypy_example.py
という名前で用意する。
# mypy_example.py
def func(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
func(a=1, b=1)
func(a=1.0, b=1.0)
mypyで静的な型チェックを行うにはシェルでmypyを呼び出せば良い。
mypy mypy_example.py # or uv run mypy mypy_example.py
> mypy_example.py:7: error: Argument "a" to "func" has incompatible type "float"; expected "int" [arg-type]
> mypy_example.py:7: error: Argument "b" to "func" has incompatible type "float"; expected "int" [arg-type]
> Found 2 errors in 1 file (checked 1 source file)
Formatter and Linter
Formatterはコードがある一定のスタイルに従っているかチェックし自動整形するためのツールで、Linterはコードの問題点やコーディング規約に違反している箇所を静的に解析するツールである。FormatterとLinterを使ってコードのスタイルを統一することで可読性を高めることが期待できるため、チーム開発だけでなく個人開発でも導入するメリットは大きい。
本資料ではFormatter兼Linterとして使えるruffを紹介する。ruffはAstral社が開発するRust製のツールで、競合ツールであるFlake8, Black, isort, pydocstyle, pyupgrade, autoflakeなどと比べて高いパフォーマンスを実現しながら統一されたインターフェースを提供することを目的としているものである。
install
pip
pip install ruff
poetry
uv add ruff # or uv add --dev ruff
usage
例として以下のようなファイルをruff_example.py
という名前で用意する。
# ruff_example.py
def func(a: int, b: int) -> int: return c
if __name__ == "__main__":
func(a=1, b=1)
Linter
ruff check ruff_example.py # or uv run ruff check ruff_example.py
> ruff_example.py:1:41: F821 Undefined name `c`
> Found 1 error.
Formatter
ruff format ruff_example.py # or uv run ruff format ruff_example.py
# show file content
cat ruff_example.py
>
def func(a: int, b: int) -> int:
return c
if __name__ == "__main__":
func(a=1, b=1)
Pre-commit file checking
Gitのpre-commitを使ってコードをコミットする直前にruffとmypyのチェックを行う方法を紹介する。
install
pip
pip install pre-commit
uv
uv add install pre-commit # or uv add --dev pre-commit
usage
Git管理下のプロジェクトで以下のようなyamlファイルを用意し、インストールを行う。
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
hooks:
- id: ruff-format
description: "Ruff formatting"
types_or: [python, pyi, jupyter]
- id: ruff
description: "Ruff linting"
types_or: [python, pyi, jupyter]
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
- id: mypy
description: "Run mypy"
types_or: [python, pyi, jupyter]
インストールは以下のコマンドでできる。
pre-commit install # or uv run pre-commit install
あとはGitコミット時に自動的にruffとmypyが変更されたファイルの内容をチェックしてくれる。
git commit -m "a commit message"
>
ruff-format..............................................................Passed
ruff.....................................................................Failed
- hook id: ruff
- exit code: 1
ruff_example.py:2:12: F821 Undefined name `c`
|
1 | def func(a: int, b: int) -> int:
2 | return c
| ^ F821
|
Found 1 error.
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1
ruff_example.py:2: error: Name "c" is not defined [name-defined]
mypy_example.py:7: error: Argument "a" to "func" has incompatible type "float"; expected "int" [arg-type]
mypy_example.py:7: error: Argument "b" to "func" has incompatible type "float"; expected "int" [arg-type]
Found 3 errors in 2 files (checked 2 source files)
Keep notebooks tidy
Notebookで全部やろうとせず、インタラクティブな処理や可視化以外は再利用しやすい形でモジュール化する。
Project code structureの節で紹介した構成を前提とし、関数化したコードはsrc/
に定義、notebooks/
配下のnotebookからはそれを参照するように実装する。
.
├── notebooks/
│ └── example.ipynb
└── src/
└── codebook/
├── __init__.py
└── utils/
├── __init__.py
└── text_utils.py
例えば、example.ipynbは以下のような内容にし、codebookから必要な関数をインポートして利用する。
# example.ipynb
from codebook.utils.text_utils import clean_text
clean_text("Lorem Ipsum is simply dummy text.")
notebookの管理には様々な意見があると思うが、EDAからモデリングまでを1つのnotebookにまとめてやってしまうと長く煩雑になるので、以下のようにタスクごとに分けると良い。
.
└── notebooks/
├── 1_eda.ipynb
├── 2_base_models.ipynb
├── 3a_gbm_model.ipynb
├── 3b_rfc_model.ipynb
└── 4_eval_report.ipynb
Summary
色々とテクニックを紹介したが、流れの早い業界なのでこの内容も陳腐化していく。コードの書き方や構成に迷ったときは近しい技術スタックの著名なOSSのコードをGitHubで検索し、真似する。そしてStarをつけて、自分だけのコードの参考文献を作っておく。これが一番重要で、Pythonコードのまとめ方、README含むドキュメントの書き方、データの管理方法、テストコードの書き方など幅広い技術を世界的なコミッターから学べるだろう。注意点として、高StarのRepositoryでも学術的な貢献の場合は良いコードとは限らない(悲しいことだが)。OSSとして評価されているRepositoryを見極めるのも一つの経験である。