【venv不要】Remote Containersで作る最強のPython開発環境【VSCode/Docker】

# はじめに

人気言語Top1に輝いたり[^1]、IPA 基本情報技術者試験の試験科目になる[^2]などして注目を集めているPython。

でもちょっと待って！あなたはPythonのエコシステムの恩恵を正しく受けて開発できていますか？

開発環境をうまく作ることで、 **入力補間やフォーマッターによって開発が効率化**されるだけでなく**静的解析によって予期せぬ不具合を未然に防ぐ**ことができ、①簡単 ②爆速 ③安全 にプログラムを書くことができます。

もちろんそのような快適な環境を作るためのHow to記事はたくさんありますが、問題はせっかくオレオレPython開発環境を作れたとしても、それを異なるPC間で共有するのが困難であるということです。

なぜなら、

* Pythonやpipで入れるモジュールのバージョンを揃えてインストールしないといけない
* fommter / linter / IDEの拡張機能を揃えてインストール＆設定しないといけない

という、開発環境に依存する問題が発生するからです。
**個人 / 複数人を問わず、異なるPC間であっても同一の動作をすることが保証されて欲しい**。
かつ、 **テキストエディタでプロジェクトを開くだけで、その開発環境が勝手に構築されて欲しい**という要求があります。

ということで、この記事では、VSCodeのRemote Containerを使うことでこの問題を解決します。
本記事で述べる方法を用いると、以下のようなことが実現されます。

* Win / Mac / Linuxを問わず、全ての環境で同一の動作をすることが保証される[^3]、ポータブルなPython開発環境が作れる
* 新しい環境でも、プロジェクトを開いただけで静的解析や自動フォーマットなどの全ての設定が完了する

では、やっていきましょう

# インストール

本記事では、以下のツールを用います。
他の記事を参考にしながらインストールしてください。

* VSCode
* Docker

ターミナルから以下のコマンドが打てれば準備完了です。

```sh
$ docker --version
Docker version 19.03.5, build 633a0ea
```

# Remote Containersの設定

まずは、開発用のディレクトリを適当に作りVSCodeで開きましょう。

```sh
$ mkdir python-test
```

<img width="1025" alt="スクリーンショット 2019-12-21 16.27.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/16ecfe6c-05a6-809e-c5c5-85cb2864a35c.png">


以下のようなディレクトリ構成を作ります

```
.
└── python-test
    ├── .devcontainer
    │   ├── Dockerfile
    │   └── devcontainer.json
    ├── .vscode
    │   └── extensions.json
    └── src
        └── main.py
```

それぞれのファイルには以下の内容をコピペしてください。

```Docker:.devcontainer/Dockerfile
FROM python:3.7.3-slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    apt-utils \
    gcc \
    build-essential \
    && pip install --no-cache-dir \
    autopep8 \
    flake8 \
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
```

ここで、 `FROM python:3.7.3-slim` の部分の数字を変えると、好きなPythonのバージョンを指定することができます。

```json:.devcontainer/devcontainer.json
{
    "name": "Python Project",
    "dockerFile": "Dockerfile",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "python.pythonPath": "/usr/local/bin/python"
    },
    "extensions": [
        "ms-python.python"
    ]
}
```

ここで、 `"name": "Python Project"` の部分は好きな文字列にしてしまって構いません。

`settings` には、 `.vscode/settins.json` で書くものと同一のものを記述することができます。本来bashのパスやpythonのパスは環境依存であるため個人ごとに設定する必要があるのですが、今回はコンテナ内で実行されるのでパスの情報が既知であり、固定することができるというのがポイントです。

また、 `extensions` には、VSCodeの拡張機能を加えることができ、ここに記述した拡張機能は自動でインストールされます。VSCodeの拡張機能を強制的にインストールさせることができるのはRemote Containersならではなので、全員にインストールして欲しい拡張機能はここに書きましょう。今回はVSCode用のPython拡張機能を追加しています。


```python:src/main.py
import sys
print(sys.version_info)
```

Pythonスクリプトはバージョンを出力するものとでもしておきましょう。

```json:.vscode/extensions.json
{
    "recommendations": [
        "ms-vscode-remote.remote-containers"
    ]
}
```

VSCodeの拡張機能のRecommendationsにRemote Containersを指定します。
注意しなければならないのは `devcontainer.json` 内のextensionsと違って、記述しても実際にインストールされるわけではありません。ですので、ファイルをコピペし終わったら実際にインストールするためにVSCodeを開き直してください。
すると、右下に以下のようなポップアップが出てくると思います。Install Allを選び、Remote Containerをインストールしましょう。
<img width="450" alt="スクリーンショット 2019-12-21 15.09.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/4077827c-b05f-148f-0d5f-223bc1c433f0.png">

無事、インストールできたらVSCodeの左下に緑色のボタンが表示されます。
<img width="141" alt="スクリーンショット 2019-12-21 14.40.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/2239fe12-8517-baab-877f-d577ed4ede4c.png">

開くと、このようなメニューがでるので、
<img width="594" alt="スクリーンショット 2019-12-21 14.41.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/c9daa882-fc45-92ec-d7c9-f42653384ef9.png">

「Remote-Containers: Open Folder in Container...」を選びます。
フォルダ選択画面がでたら、プロジェクトのディレクトリ（今回の場合はpython-test)を選びましょう。

すると `.devcontainer/Dockerfile` に記述したコンテナのビルドが自動で走ります。detailsを開くことで 経過を見ることができます。もしbuildにこけてしまった場合もdetailsを見ましょう。
<img width="451" alt="スクリーンショット 2019-12-21 14.45.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/50aad18d-55f8-f073-f030-24ff0b65f0a7.png">

ビルドが正常に終わったらそのままコンテナを起動し接続してくれます。コンテナに接続されたらVSCodeの内部ターミナルを開いてください。
<img width="1138" alt="スクリーンショット 2019-12-21 14.58.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/f9a232e7-4c95-e395-0d88-079ea63f82f4.png">

おや？なにやらシェルのユーザーがrootになっていて、ディレクトリも `/workspaces` になっていますね。これは、ホストOS(Win / Macとか) のターミナルではなく、ホストOSから独立したコンテナのターミナルに入れていることを示しています。

Docker分かんないよ！という方もいるかもしれませんが、とりあえずPythonを実行してみましょう。

```sh
/workspaces/python-test# python src/main.py 
sys.version_info(major=3, minor=7, micro=3, releaselevel='final', serial=0)
```

`FROM python:3.7.3-slim` で指定したバージョンのpythonが入っていますね！
これで基本的な設定は完了です。

今後は他のPCで `git clone` しても、「Remote-Containers: Open Folder in Container...」でコンテナの中に入ることで、同一の環境を再現することができます。

# requirements.txtの設定

ここはコンテナの中なので、この中で好きに `pip install` しても構いません。ここでインストールしたモジュールはホストOSにはまったく影響を与えることがないので、破壊的に開発を進めることができます。

```sh
/workspaces/python-test# pip install numpy
/workspaces/python-test# python
>>> import numpy as np
>>> np.__version__
'1.17.4'
```
また、再度コンテナをビルドして作り直すとインストールしたモジュールは消えます。そこで、開発を進める中でこのpipモジュールはfixしよう！となった場合、Pythonで慣例として使われている `requirements.txt` を作って、そこに `pip install` するモジュールを書くこととしましょう。

ディレクトリ構成と `requirements.txt` の書き方は以下の通りです。

```
.
└── python-test
    ├── .devcontainer
    │   ├── Dockerfile
    │   └── devcontainer.json
    ├── .vscode
    │   └── extensions.json
    └── requirements.txt
```

```txt:requirements.txt
numpy==1.17.4
```

requirements.txtには、 `pip install` する際のバージョンを固定して書くことができます。再現性を担保するために、バージョンは固定することが望ましいです。

そして、 `devcontainer.json` を以下のようにしましょう。

```json:.devcontainer/devcontainer.json
{
    "name": "Python Project",
    "dockerFile": "Dockerfile",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "python.pythonPath": "/usr/local/bin/python"
    },
    "extensions": [
        "ms-python.python"
    ],
    "postCreateCommand": "pip install -r requirements.txt"
}
```

`postCreateCommand` というのが追加されましたね。これは、コンテナが生成された後に実行するコマンドです。今後は `requirements.txt` に記述したモジュールが必ずインストールされます。

ファイルを更新したら、更新をコンテナに反映させるために、左下の緑色のボタンを押して、「Remote-Containers: Rebuild Container」を選びましょう。

# formatter / linterを設定する

続いて、formatter / linterを設定していきます。
pythonのformatter / linterを追加したときの `devcontainer.json` は以下のとおりです。

```json:.devcontainer/devcontainer.json
{
    "name": "Python Project",
    "dockerFile": "Dockerfile",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "python.pythonPath": "/usr/local/bin/python",
        "python.linting.pylintEnabled": false,
        "python.linting.flake8Enabled": true,
        "python.linting.flake8Args": [
            "--ignore=E402,E501"
        ],
        "python.formatting.provider": "autopep8",
        "python.formatting.autopep8Args": [
            "--ignore",
            "E402,E501",
            "--max-line-length",
            "150"
        ],
        "[python]": {
            "editor.formatOnSave": true
        }
    },
    "extensions": [
        "ms-python.python"
    ],
    "postCreateCommand": "pip install -r requirements.txt",
}
```

linterとしてflake8、 formatterとしてautopep8を指定し、それぞれE402(Module level import not at top of file)とE501(Line too long)を無視しています。ファイルのセーブ時にformatがかかるようにしています。

これでこんなふうなめちゃくちゃなコードを書いても、

```python:before.py
a    =0
def method1(args)  :
    print(   args)
def method2() :
    if a    ==0:
            method1(   "hoge"   +    "fuga"   )
```

このように、保存する際に整形してくれます。

```python:after.py
a = 0


def method1(args):
    print(args)


def method2():
    if a == 0:
        method1("hoge" + "fuga")
```

通常、このようなformatter / linterの設定は `.vscode/settings.json` に記述します。しかし、 `.vscode/settings.json` は個人の設定ファイルであるから、 `.gitignore` に設定しているプロジェクトも多いです。加えて、 `pip install flake8 autopep8` を手動で実行する必要があるので、設定ファイルを書いたからといって、formatter / linterが適用されるわけではありません。

一方で、今回はRemote Containersの設定ファイルに書くことができるので、共有もしやすいですし、確実にflake8やautopep8が入っている状態を再現できるので "フォーマットがかかっていないソースコードがコミットされる"ということも防ぐことができます。

ところで、 `pip install flake8 autopep8` はどこで実行していたのでしょうか？
答えは `.devcontainer/Dockerfile` の中にあります。

```Docker:.devcontainer/Dockerfile
FROM python:3.7.3-slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    apt-utils \
    gcc \
    build-essential \
    && pip install --no-cache-dir \
    autopep8 \
    flake8 \
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
```

もしあなたが、コンテナ内で `pytest` を記述したいと思った場合は追加することができます。

```Docker:.devcontainer/Dockerfile
RUN pip install --no-cache-dir \
    autopep8 \
    flake8 \
    pytest
```

また、autopep8やflake8は開発時に必要なのであって、実行時には不要です。そうしたモジュールはDockerfileに書いて、実行時に必要なファイルは `requirements.txt` に書く、という使い分けができます。

# 静的型解析

最後にPython3.6以降[^4]で使えるType Hintsを導入しましょう。Type Hintsを導入することで、QoLが爆上がりします。

`devcontainer.json` を以下のようにしましょう
ここでは、Type Hintsの静的解析ツールとして、pyrightを用います。

```json:.devcontainer/devcontainer.json
 "extensions": [
    "ms-python.python",
    "ms-pyright.pyright"
 ],
```

変更したら、左下の緑色のボタンを押して、「Remote-Containers: Rebuild Container」を選びましょう。

続いて、pyrightの設定ファイルを書きます。
ディレクトリ構成は以下の通りです。

```
.
└── python-test
    ├── pyrightconfig.json
    └── src
        └── main.py
```

```json:pyrightconfig.json
{
    "include": [
        "src"
    ],
    "reportTypeshedErrors": false,
    "reportMissingImports": true,
    "reportMissingTypeStubs": false,
    "pythonVersion": "3.7",
    "pythonPlatform": "Linux",
    "executionEnvironments": [
        {
            "root": "src"
        }
    ]
}
```

pyrightconfig.jsonの詳しい書き方は他の記事を参照してください。（もしかしたら自分が他に書くかも）

ここで、 `src/main.py` を編集してみましょう。

```python:src/main.py
def hello(name: str, age: int) -> str:
    result1: str = "My name is " + name + ".\n"
    result2: int = "I am " + str(age) + " years old."
    return result1 + result2


result: int = 10
result = hello(name="Otao", age=23)
print(result)
```

pythonのType Hintsでは、このように関数や変数に型アノテーションを付けることができます。
pyrightを導入した状態で `src/main.py` を開くと、、、
<img width="432" alt="スクリーンショット 2019-12-21 16.13.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/a4e06f22-726d-7f3d-f020-1cb61d7bc93a.png">
result2はint型なので、str型は代入できないよ！
<img width="619" alt="スクリーンショット 2019-12-21 16.14.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/f38d1287-ddb8-4cf2-8330-4c860c7729ff.png">
str型とint型の `+` 演算子は定義されてないよ！！
<img width="507" alt="スクリーンショット 2019-12-21 16.14.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/2fc87f18-9f7f-757f-b5fc-79e093713d28.png">
resultはint型で定義されてるのに、そこにstr型を代入しようとしているよ！！！
hello関数の定義も貼っておくね！！！
<img width="570" alt="スクリーンショット 2019-12-21 16.14.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23949/d3b20420-6b2c-1997-f941-407bb85816c3.png">
といった風に、型にまつわるエラーを表示してくれます。

Type Hintsを使うことは以下の理由からおすすめです。

* 型を定義することによって可読性がめちゃくちゃ上がる
* 予期せぬ型が変数に入ってきて不具合を起こす可能性が下がる
* VSCode上の入力補完機能が効く

ぜひ導入しましょう。

まぁ不思議なことに(?)このスクリプト、実行はできるんですよねｗ
（Type Hintsはあくまでアノテーションなので、実行時には無視されます。）

```sh
/workspaces/python-test# python3 src/main.py 
My name is Otao.
I am 23 years old.
```


# 終わりに

快適なPythonライフを！
（記事は適宜更新します。）

* 著者情報
  * Twitter: [@0meo](https://twitter.com/0MeO)
 
[^1]: 習得したいプログラミング言語、したくない言語, https://tech.nikkeibp.co.jp/atcl/nxt/column/18/00501/111200004/
[^2]: プレス発表　基本情報技術者試験における出題を見直し, https://www.ipa.go.jp/about/press/20190124.html
[^3]: 実際にはホストOSによって挙動が違うことがあり、Dockerfileの修正が必要になることもあります。
[^4]: 関数へのType Hintsは3.5からあったので正確ではありませんが、変数にも型アノテーションを付けれるようになったのは3.6以降なので、3.6以降を使って欲しいです