仮想通貨 botter のためのモダン Python 環境構築ガイド

こんにちは、@supermomongaです。

仮想通貨 botter Advent Calendar 2021 のカレンダー 9 日目の記事です。

TL;DR

  • Python ランタイムの管理は asdf を利用する
  • Python 仮想環境の管理は poetry を利用する

Disclaimer

この記事では、仮想通貨システムトレードのために Python を使い始めた、本職プログラマではない方の為のモダンな環境構築の方法を解説します。導入するツールが必要となる背景も含めて解説していますので、プログラマの方は TL;DR 以外読み飛ばして問題ありません。

また、今回の記事は Linux ディストリビューションの 1 つである Ubuntu を OS とした環境を前提としています。もし Windows をお使いの場合、WSL2Windows Terminalを導入することを推奨します。

はじめに

Python をインストールし使い始める際の最もスタンダードな方法は、Windows であれば Python 公式サイトからのインストーラ入手、Linux であれば各種パッケージマネージャ経由となるでしょう。また、PyPI で公開されている各種 Python パッケージのインストールには、Python にデフォルトで付属している pip コマンドを利用することと思います。

この運用はお手軽に Python プログラミングを始める方法としてはシンプルで良い方法ではあるのですが、これは Python ランタイムと各 Python パッケージをシステムワイドにインストールすることを意味し、それによる様々な問題が発生します。以下にその一例を記載します。

問題 1. プロジェクトによって必要とする Python ランタイムバージョン要件が異なる

システムワイドに Python ランタイムをインストールするということは、あなたが利用するすべてのツール、およびあなたが開発しているプロジェクト(bot)はすべて、この Python ランタイムで動作させる必要があるというということです。(パッケージマネージャによってはバージョンごとに別名のパッケージとして提供し、python3.8, python3.9 といった様に別名の実行ファイルを用意してくれる場合もありますが、簡単のため、ここでは触れません)

例として、あなたは Python 3.8 をインストールし、プロジェクト(bot)を 10 個開発したとしましょう。それらのプロジェクトは Python 3.8 ランタイムで完璧に動作します。その後、Python 3.9 がリリースされました。あなたは新たなプロジェクトでは Python 3.9 の新機能を利用したいと考え、Python のバージョンをアップグレードし、11 個目のプロジェクトを開発しました。

ここで問題になるのは既存の 10 個のプロジェクトです。通常、プログラミング言語というのは完全な後方互換性を維持するものではありませんので、Python 3.8 では完璧に動作していたプログラムも、Python 3.9 では動作しないかもしれません。もしくは、あなたが直接書いたコード自体は Python 3.9 で動作したとしても、プロジェクトで利用していたサードパーティの Python パッケージが Python 3.9 では動作しないかもしれません。あなたは、すべての既存のプロジェクトについて Python 3.9 でも意図通りに動作することを確認する作業に工数を支払わなくてはなりません。そして、この作業は Python ランタイムのバージョンをアップデートする度に発生します。

問題 2. プロジェクトによって必要とそる Python パッケージが異なる

通常、プログラムのすべてを自作コードで賄うことは稀であり、サードパーティの Python パッケージを利用することで車輪の再発明を防ぎ、開発およびメンテナンス工数を削減します。例えば、仮想通貨のシステムトレードにおいては、pybotters などのライブラリを利用することで、各取引所の API クライアントを自作する必要がなくなり、トレードロジックの実装に注力することができます。

そして、そういったライブラリもまた同様に他のライブラリを利用していることが常です。こういった、ライブラリ同士の依存関係がネストしていったものを依存関係ツリーと呼びます。

ここで問題となり得るのが依存関係の衝突です。例えば、あるプロジェクト A にて pip コマンドを用い pybotters のバージョン 0.8.0 をインストールしたとしましょう。この時、pybotters が依存するライブラリもインストールされます。

依存関係一覽の定義はこちら

さて、ここでは aiohttp のバージョン指定が ^3.7.4 となっていますが、これは 3.7.4 以上かつ 4.0.0 未満のバージョンで aiohttp をインストールする(バージョン解決する)ことを意味します。実際にどのバージョンがインストールされるかはこの範囲を超えない範囲で pip コマンドが自動的に判断します。

Image from Gyazo

次に、新たにプロジェクト B を作り、そこで架空の foobar というライブラリのバージョン 1.0.0 をインストールしたとします。この foobar も同様に aiohttp に依存しており、バージョン指定は 3.7.0 とします。これはハット記号(^)が付いていませんので、そのままバージョン 3.7.0 をインストールすることを意味します。

ですが、システムワイドな仮想環境にはすでにプロジェクト A によってバージョン 3.7.4 以上の aiohttp がインストールされてしまっているので、バージョン 3.7.0 のインストールを行うことはできません。これが依存関係の衝突です。

Image from Gyazo

この様に、システムワイドな仮想環境を使う以上、あなたが作るすべてのプロジェクトの依存関係のバージョン解決が互いに影響を及ぼしてしまい、プロジェクトの数に比例して依存関係の解決が難しくなっていってしまいます。

asdf と poetry による環境の分離

上述した 2 つの問題を解決するため、asdf と poetry の導入を行います。

asdf の導入

asdf は Python を含めた様々なプログラミング言語ランタイムやツール郡の複数バージョン管理を行ってくれるプログラムです。

https://asdf-vm.com/

公式サイトのインストール手順を参考にインストールしてください。

sudo apt install -y curl git
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.8.1
echo ". \$HOME/.asdf/asdf.sh" >> ~/.bashrc
echo ". \$HOME/.asdf/completions/asdf.bash" >> ~/.bashrc
source ~/.bashrc

次に、asdf で Python ランタイムの管理が行えるように、Python プラグインを追加します。

https://github.com/danhper/asdf-python

asdf plugin-add python

次に、Python をビルドするために必要な依存 apt パッケージをインストールします。

https://github.com/pyenv/pyenv/wiki#suggested-build-environment

sudo apt-get update
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev

以上で準備は完了です。実際に Python をインストールしてみましょう。インストールコマンドは asdf install [プラグイン名] [バージョン] です。ここではバージョン 3.9.9 をインストールしてみます。

asdf install python 3.9.9

余談ですがバージョン指定部分を latest とすると、自動的に最新バージョンがインストールされます。install 以外のコマンドのバージョン指定部分でも同様です。

さて、無事に Python がインストールできましたが、インストールするだけでは利用することができません。実際に Python を利用するには、プロジェクトごとのディレクトリに移動し、asdf local [プラグイン名] [バージョン] というコマンドを実行する必要があります。

cd ~/path/to/project_A/
asdf local python 3.9.9
python --version # "Python 3.9.9" と表示されます

これで、/path/to/project_A/ 以下であれば、サブフォルダー含め python コマンドは Python 3.9.9 ランタイムが参照されるようになりました。

asdf local コマンドは実際には .tool-versions というファイルを作成しているだけですので、このファイルを git リポジトリ管理下とすることで、チーム開発時にランタイム依存バージョンを明示することができます。

cat ./.tool-versions # "Python 3.9.9" と表示されます

以上で Python プロジェクトごとの Python ランタイムの指定ができるようになりました。例として、別のプロジェクト B では Python 3.10.0 を使いたければ、以下のようなコマンドを実行します。

asdf install python 3.10.0
cd ~/path/to/project_B/
asdf local python 3.10.0

また、local ではなく global というコマンドを利用すれば、システムワイドに参照されるランタイムを変更可能です。

asdf install python 3.7.12
asdf global python 3.7.12

cd ~/
python --version # "Python 3.7.12" と表示されます

cd ~/other_dir/
python --version # "Python 3.7.12" と表示されます

cd ~/path/to/project_A/
python --version # "Python 3.9.9" と表示されます

cd ~/path/to/project_B/
python --version # "Python 3.10.0" と表示されます

poetry の導入

poetry 複数の機能を持ったツールで、今回の用途は仮想環境の管理とパッケージ依存関係の解決機能を利用します。

https://python-poetry.org/

前述の asdf はプラグインさえ用意すれば言語ランタイムだけでなく任意のツールの管理も可能となっています。poetry のプラグインも存在しますので、今回はこれを利用しましょう。

poetry のインストールには Python ランタイムが必要なので、まずは asdf を用いてシステムワイドに参照される Python バージョンを指定しましょう。2021 年 12 月 9 日現在、Python 3.10 系では poetry のインストールに失敗する問題がありますので、ここでは 3.9 系最新の 3.9.9 を利用します。

asdf install python 3.9.9
asdf global python 3.9.9

次に asdf 用 poetry プラグインを追加し、最新バージョンの poetry をインストールし、poetry コマンドがシステムワイドに参照できるようにします。

https://github.com/asdf-community/asdf-poetry

cd ~/ # プロジェクトフォルダの `.tool-version` が参照されないように特定のプロジェクトからは抜けておく


asdf plugin-add poetry https://github.com/asdf-community/asdf-poetry.git
asdf install poetry latest
asdf global poetry latest

更に、poetry は初期設定ではシステムワイドな仮想環境にパッケージをインストールしてしまいますので、プロジェクトごとに独立した仮想環境を作成しそちらにパッケージをインストールしてくれるように設定を変更します。

poetry config virtualenvs.in-project true

以上で poetry のセットアップは完了です。

プロジェクトのディレクトリに移動し、poetry init コマンドを実行すると .venv というディレクトリで仮想環境を作成してくれます。この仮想環境内に Python ランタイムや依存パッケージなどが格納されます。

cd ~/path/to/project_A/
poetry init

./.venv/bin/python --version # "Python 3.9.9" と表示されます

依存パッケージのインストールは pip コマンドの代わりに poetry add コマンドを利用します。

poetry add pybotters@0.8.0

追加したパッケージの依存関係要件は pyproject.toml ファイルへと記載されます。こちらを手動で書き換えた場合、poetry update コマンドを実行すれば再度依存関係の解決が行われます。

また、依存パッケージのバージョン指定は前述の通り ^3.7.4 といった様な曖昧な指定の方法ができますが、実際に解決されたバージョンの情報は poetry.lock というファイルに記載されます。このファイルを git リポジトリ管理下とすることで、チーム開発時に、全く同一のバージョンで各パッケージをインストールすることができます。これにより、チーム開発時に各々の手元で完全に同一の環境が構築できます。

poetry.lock ファイルを用いたパッケージのインストールは poetry install コマンドを用います。

cd ~/path/to/project_A/
poetry install

Image from Gyazo

この仮想環境を用いてプログラムを実行する場合は、以下のように .venv/bin/ 内のバイナリを指定して実行してください。

cd ~/path/to/project_A/
./.venv/bin/python main.py # main.py を実行

コマンドを提供するパッケージ、例えば JupytarLab などをインストールした場合、同様に実行コマンドは .venv/bin/ 以下に配置されます。

cd ~/path/to/project_A/
poetry add jupyterlab@4.0.0 # プロジェクトAではバージョン 4.0.0 の JupyterLab を使いたい
./.venv/bin/jupyter lab # Jupyter Lab 起動

以上で、asdf と poetry の導入によるプロジェクトごとの Python ランタイム及び仮想環境の分離が完了しました。お疲れ様です。

機会があれば、仮想環境を VSCode と協調させる設定についての記事も書きたいと考えております。

FAQ

Q. Python ランタイム管理は asdf ではなく pyenv を利用するのでも良いか

pyenv でも良いと思いますが、asdf を用いることで、統一したコマンド体系で様々なプログラミング言語のランタイムやツールを管理できるようになるので、筆者は asdf を使用しています。

Q. Docker の利用について

Docker を用いることでコンテナレベルでプロジェクトごとに環境を分離させるというのも好ましいアプローチです。Docker 自体の知識が無いと意図せず大量のコンテナを作ったままにしてしまうなどの混乱が起きやすいと考え今回は扱いませんでしたが、機会があれば別の記事で取り上げたいと思います。

Q. poetry 自体もプロジェクトごとに asdf local コマンドでバージョン管理すべきではないか

経験上 poetry のバージョンが変わることで問題になったことがないため試したことはありませんが、その方が好ましいかもしれません。