PyPyとPythonの実行環境をDockerで構築する

前回記事「[PyPyの実行環境をDockerで構築する](https://qiita.com/otomo6sm/items/85b5855fea014c4ebbf3)」の続編（姉妹編？）です。前回はPyPyの環境のみでしたが、今回はPyPyと素のPython両方の環境を作ります。同じ内容をかなり含んでいますがご了承ください。


## はじめに

### PyPyとは？
Pythonの実装のひとつ。
Pythonモジュールをふつうに実行できます。素のPythonよりも圧倒的に高速で動作します。

### なぜPyPyとPython？
PyPyの欠点は、利用できるライブラリに制限があることです。詳しくは把握していないのですが、サードパーティのライブラリは利用できない場合が多いようです。
したがって、一連の処理の中でサードパーティのライブラリを用いる部分は素のPythonで、それ以外で実行速度が必要な部分はPyPyで実行するという使い分けができると便利です。
今回はPyPyとPythonが両方使える環境をDockerで構築します。

### なぜDocker?

ローカルマシンの環境を汚したくないので仮想環境を作りたいと思ったのですが、PyPyでの仮想環境の作り方についてあまり情報が見つかりませんでした。
最近Dockerの勉強をしていたことと、そもそもWindowsのPython環境をあまり信用していないことから、じゃあDockerで環境作ろうと思いました。

### 環境
- Windows 10 Home （バージョン20H2 ビルド19042.685）
- WSL2でDockerが使える環境

最近はWindows10 HomeでもWSL2でDockerが動くようになって非常に使いやすくなりました。WSL2のインストール方法はたとえば[Microsoftの公式ページ](https://docs.microsoft.com/ja-jp/windows/wsl/install-win10)を参照してください。その後、Docker Desktopの設定でWSL2を使うよう指定すればOKです。

Macでもほぼまったく同じ手順でできるはずです。

## 手順

### 方針
基本方針としては以下です。

- PyPyの公式Dockerイメージをもとにしたコンテナ上にPyPyとPythonの環境を構築する
- PyPyの環境はPipとrequirements.txtで管理する
- Pythonの環境はPipenvで管理する

PythonでなくPyPyのDockerイメージを使う理由は、単にそれがPyPyの環境を用意するのに一番簡単な方法だからです。しかもPyPyのイメージにはPythonの実行環境も入っています。
環境がごっちゃにならないように、Pythonで使うライブラリはPipenvの仮想環境で管理することとします。pypyの環境もrequirements.txtに残しておき、環境の再構築や共有が容易になるようにします。
※この記事ではPipenvの説明はしないので、分からないひとは別途調べてください。



### ディレクトリ構成
以下のようにディレクトリを構成します。

```
│  docker-compose.yml
│  Dockerfile
└─src
        pypy_main.py        
        python_main.py
        Pipfile
        Pipfile.lock
        requirements.txt
```

### ファイル作成
`requirements.txt`にはpypy環境で使用したいライブラリを記述します。ここでは例として、numpyを指定しておきます。

```requirements.txt
numpy
```

`Pipfile`と`Pipfile.lock`ではPython環境で使用したいライブラリを指定します。ここでは例として、pandasだけをインストールするものを用意しておきます。また、Pythonのバージョンとして3.9を指定していることに注意します（あとで出てきます）。

```Pipfile.
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
pandas = "*"

[dev-packages]

[requires]
python_version = "3.9"
```

`pypy_main.py`はpypy環境で実行したいモジュールです。ここでは例として、numpyを実行するものを用意しておきます。

```pypy_main.py
import numpy as np

print(np.array([1, 2, 3]))
```

`pyhon_main.py`はPython環境で実行したいモジュールです。ここでは例として、pandasを実行するものを用意しておきます。

```python_main.py
import pandas as pd

print(pd.DataFrame([[1, 2, 3], [4, 5, 6]]))
```


`Dockerfile`は以下のように作成します。

```
# 1. PyPyイメージの取得
FROM pypy:3.7

# 2. pyenvとpython3.9のインストール
RUN git clone https://github.com/pyenv/pyenv.git ~/.pyenv && \
    echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc && \
    echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc && \
    echo 'eval "$(pyenv init -)"' >> ~/.bashrc && \
    . ~/.bashrc  && \
    pyenv install 3.9.1

# 3. ワークディレクトリを/srcに指定
WORKDIR /src

# 4. python仮想環境構築
COPY src/Pipfile* ./
RUN pip install pipenv && \
    pipenv install

# 5. pypy環境構築
COPY src/requirements.txt .
RUN pip install -r requirements.txt
```

1. DockerHubからPyPyの公式イメージをとってきます。
2. pyenvを利用してPython3.9をインストールします。Pipfileでバージョン3.9を指定しているためです。参考→https://codeaid.jp/pyenv-linux/
3. この後の処理のため、ワークディレクトリを/srcに指定します。
4. Pipenvを使ってPythonの仮想環境を構築します。PipfileとPipfile.lockをコピーした後にpipenv installコマンドを実行しています。
5. requirements.txtをコピーし、そこから必要なライブラリをインストールしてPyPy環境を構築します。

`docker-compose.yml`ファイルは以下のように作成します。

```docker-compose.yml
version: '3'

services:
  pypy-python:
    build: .
    volumes:
      - ./src/:/src
    tty: true
```
サービス名を`pypy-python`としていますが、何でもよいです。あとでこの名前でコンテナに入るので、自分にとって分かりやすいようにプロジェクト名をつければ良いと思います。
`build`で同ディレクトリにあるDockerfileを指定しています。
`volumes`でホストPCのsrcディレクトリをコンテナのsrcディレクトリにマウントしています。これでホストPCの編集がコンテナに反映されます。
`tty`をtrueにすることで、コンテナを立ち上げたあと起動しっぱなしとなり、コンテナに入ることができます。

以上で準備は完了です。

### 実行
`docker-compose.yml`のあるディレクトリでコマンドプロンプトを開き、以下のコマンドでコンテナを立ち上げます。

```
>docker-compose up -d
```

初回はちょっと時間がかかります。
オプション`-d`をつけることでバックグラウンド実行となり、このあとのコマンド操作が可能になります。

コンテナが立ち上がったら、以下のコマンドでコンテナに入ります。

```
>docker-compose exec pypy-python bash
```

ここで、引数`pypy-python`はdocker-compose.ymlファイルでサービス名としてつけたものです。
引数`bash`はコンテナに入ったあとコマンドライン操作を行うために必要です。というかこの引数がないとエラーになります。

コンテナ内のsrcディレクトリに入ったので、直下に`pypy_main.py`と`python_main.py`があります。

PyPyを実行するには`pypy`コマンドを使います。`pypy_main.py`を実行してみます。

```
# pypy pypy_main.py
[1 2 3]
```

このように実行できます。また、requirements.txtに記述したnumpyがpypy環境にインストールできていることが分かります。

続いて、Pipenvの仮想環境で`python_main.py`を実行してみます。

```
# pipenv run python python_main.py
   0  1  2
0  1  2  3
1  4  5  6
```

このように、Pipenv環境でpythonが実行できました。pandasが使えていることが分かります。

### ソースの編集
srcディレクトリをマウントしているので、ホストPCでファイルを編集すればそれがコンテナにも反映されます。
ホストPCのエディタで編集しコンテナで実行、という流れで開発を進めることができます。

### ライブラリの管理
pypy環境のライブラリはpipでインストールできます。たとえばコンテナ内で下記コマンドを実行してtqdmをインストールしてみます。

```
# pip install tqdm
```

ライブラリをインストールしたら、requirements.txtに記録しておくとよいでしょう。

```
# pip freeze > requirements.txt
```

srcディレクトリをマウントしているため、コンテナ内で書き換えればホストマシンにも反映されます。このファイルを保存しておけば、コンテナを作り直すときも同じ環境を再現できます。

Python環境のライブラリはもちろんPipenvで管理します。新しくライブラリをインストールしたいときはコンテナ内で`pipenv install`します。
書き換わったPipfileとPipfile.lockはホストマシンにも反映されるので、これを保存しておけば同じ環境を再現できます。

### コンテナ終了
使い終わったらexitコマンドでコンテナから抜けます。

```
# exit
```

コンテナは立ち上げっぱなしでもいいですが、使い終わったら落としておくとよいと思います。

```
>docker-compose down
```

使うときはもう一度`docker-compose up -d`します。2回目以降はホストPCにイメージが残っているので、素早く立ち上がります。
ただし、コンテナ内で新たにライブラリをインストールした後に`docker-compose down`した場合は、ホストPCのイメージにはその変更が反映されていません。ライブラリのインストールを反映するためには、以下のように`--build`オプションを追加してビルドしなおす必要があります。これで、新たにPipfileとreqruirements.txtを読み込んだうえでイメージからコンテナを作り直してくれます。

```
>docker-compose up -d --build
```

## おわりに
私は[秘境集落探索ツール](https://search-isolated-villages-2.herokuapp.com/)というものを作成しているのですが、これのデータ前処理で今回紹介したようにPyPyとPythonを併用しています。
GISデータを読み込んだ後に大量の計算処理をする必要があったのですが、GISデータを読み込むライブラリ（geopandas）はPyPyでは使えず、計算処理はPythonだといつまでも終わりません。そこで、PythonでGISデータを読み込んでテキストファイルに出力し、それに対してPyPyで計算処理をするという方法をとりました。
PyPyは速くて便利だと思うのですが、情報が少ない印象です。あまり使っている人がいないのでしょうか。

あと、Dockerは便利ですね。うまく使えると、環境というもので悩まずに済んで非常に快適です。（というか、最近Windows環境でろくにPythonが動かないのはなぜなんだろう……）