VSCodeでのPythonのデバッグと、Dockerコンテナ環境に接続してリモートデバッグする方法


## 経緯

この記事は[PyCon JP 2017 Development Sprints](https://pyconjp.connpass.com/event/62858/)での取り組みと、
その後の勉強会([Nagoya.Swift+ 9月度勉強会 - connpass](https://nagoyaswiftplus.connpass.com/event/53941/))での取り組みをまとめたものです。

## 注記

下記の内容は2017年当時のものであり、現在ではよりよい方法があります。下記の内容に従って環境構築を行うのは、次の観点でおすすめできません。

1. `pyenv` が必要になるケースはかなり限定される一方、環境の構成に与える影響が大きいので導入には慎重になるべき
2. Docker Container への接続を行った後のデバッグ手法については [Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) でより容易に実現できる

例えば次に従うことで、Docker Container を開発環境として利用できます。

https://code.visualstudio.com/docs/remote/containers

下記の内容は 2017 年当時の環境を知るための資料として残しますが、現在でもこの手順に従うことを推奨するものではないことは重ねて記しておきます。

## 目標

普段Jupyterばっかりで他の開発環境を使っていなかったので、VSCodeでPythonやってみます。
次を目標に頑張ります。

1. `pyenv`で導入したローカルの環境でデバッグする
2. Dockerで導入した環境でデバッグする


ローカル環境で実行する
=================================================

## 環境

環境は次のとおりです。

* OS : macOS
* python : Python3.6
* 利用ツール : Anaconda, pyenv, pyenv-virtualenv

## pythonの環境構築

専用のディレクトリを作り、Anacondaの環境を構築します。
環境構築には好き好きあると思いますが、`pyenv`を使います(やりすぎな気もしているのでそろそろ`pyenv`から`venv`に乗り換えたい)。

```bash
mkdir ~/Documents/VSCodeHandsOn
cd ~/Documents/VSCodeHandsOn
pyenv install anaconda3-2.5.0
pyenv local anaconda3-2.5.0
```
### 参考

* [pyenv-virtualenvでディレクトリ単位のpython環境構築 - Qiita](http://qiita.com/niwak2/items/5490607be32202ce1314)

* [Pythonの仮想環境構築 2017.01版 - YAMAGUCHI::weblog](http://ymotongpoo.hatenablog.com/entry/2017/01/29/002039)

* [Pythonの仮想環境構築(2017年版) pyenvとpyenv-virtualenvとvirtualenvとvirtualenvwrapperとpyvenvとvenv - Qiita](http://qiita.com/maskedw/items/aaa2fd7abfd493cf2820)

* [pyenv/pyenv: Simple Python version management](https://github.com/pyenv/pyenv#homebrew-on-mac-os-x)


## VSCodeの環境構築

VSCodeをインストールしたら[Python with Visual Studio Code](https://code.visualstudio.com/docs/languages/python)を参考に進めていけば大丈夫そうです。
僕は[Python - Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=donjayamanne.python)を使ってます。

### VSCodeの設定

サンプルのために次のコードを作ってみます。

```python
import sys
import numpy as np # Numpyはanacondaを使ったら使いたくなると思うので

print('hello python!')

print('python version:' + str(sys.version_info))
print('numpy version:' + np.version.full_version)

```

このままPythonコードを走らせると、筆者の環境では「numpyが見つからない、Pythonのバージョンが予期したものと違う」といった不具合が発生しました。
VSCodeが使用するPythonを`.vscode/setting.json`内で指定することで回避できます。

```settings.json
{
    "python.pythonPath": "/Users/[USERNAME]/.pyenv/shims/python"
}

```

### 参考

* [uroshika's notes: VisualStudioCode で Python 実行時にモジュールが import できない場合の対処法](http://uroshika.blogspot.jp/2016/04/python-vs-code-python-import.html)

* [【Visual Studio Code】v1.12.1 設定のためのSettings.jsonファイルについて - Qiita](http://qiita.com/ayatokura/items/4301e0d1d8b339f722eb)

* [Python Path and Version · DonJayamanne/pythonVSCode Wiki](https://github.com/DonJayamanne/pythonVSCode/wiki/Python-Path-and-Version)

* [Visual Studio CodeでPythonの開発環境構築を構築してみた。 ｜ Developers.IO](http://dev.classmethod.jp/tool/python-pyenv-vscode/)

## デバッグ

[Pythonにおけるデバッグ手法](https://gitpitch.com/TakesxiSximada/presentations/python-debug-tips)にある方法が一通りできるか試しました。

操作方法は[VS Code で Python をデバッグする環境構築（+ NumPy, SciPy, Matplotlib を実行する環境構築）](http://beachside.hatenablog.com/entry/2016/08/17/004500)に従えば大丈夫です。
この段階で設定ファイルの編集なしでデバッグまで実行できました。

### 参考

* [Visual Studio CodeでPython開発環境を整える - Qiita](http://qiita.com/84zume/items/27d143f529396c9fa1cc)

* [Visual Studio CodeでNode.jsデバッグ - Qiita](http://qiita.com/kuniken/items/a6341297a97925f5081a)

* [Pythonバージョンの取得方法 - Qiita](http://qiita.com/vmmhypervisor/items/67e5596f1188f00fbb09)

## lintツールを変更する

pylintからlintツールを`flake8`に変更してみました。理由は`pylint`は厳しすぎたのと(先程のコードだと1行目から注意されます)流行に乗りたかったという2点からです。

### flake8のインストール

ターミナルから`pip`でインストール。

```bash
% pip install flake8
```

### lintツールの変更

`settings.json`を編集して、利用するlintツールを変更します。macOSだとCmd+,で設定ファイルが開くので、次の内容を追記すれば大丈夫です。

```settings.json
{
    // それぞれのユーザー特有の設定
    // "files.autoSave": "afterDelay",

    // python関連の設定
    "python.pythonPath":"/Users/[USERNAME]/.pyenv/shims/python",   // pyenv関連
    "python.linting.pylintEnabled": false,                   // Disable pylint
    "python.linting.flake8Enabled": true                     // Enable flake8
}
```

### 参考

* [Linting · DonJayamanne/pythonVSCode Wiki](https://github.com/DonJayamanne/pythonVSCode/wiki/Linting#flake8)

* [Visual Studio CodeでPythonコーディングをする設定 - Qiita](http://qiita.com/fukuit/items/cd5a76b49af91de83499)


Dockerの利用
=================================================

事前準備
-------------------------------------------------

### Dockerのインストール

[公式サイトのインストール手順](https://docs.docker.com/engine/installation/)が参考になると思います。僕は`Homebrew`でインストールしました。

```shell
$ brew cask install docker # だったと思うけれどだいぶ前のことなので自信がない
```

#### 参考

* [Install Docker | Docker Documentation](https://docs.docker.com/engine/installation/)

* [The Docker Platform](https://www.docker.com/community-edition#/mac)

* [Get Docker CE for Ubuntu | Docker Documentation](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/)


### Jupyter Notebookのインストール

今回は[Jupyter Notebook導入済みのイメージ](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/docker/README.md)を使います。ローカルにインストールする場合はAnaconda推奨のようです。

#### 導入済みのイメージの例

* [tensorflow/README.md at master · tensorflow/tensorflow](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/docker/README.md)

* [docker-stacks/tensorflow-notebook at master · jupyter/docker-stacks](https://github.com/jupyter/docker-stacks/tree/master/tensorflow-notebook)

前者がTensorflowの実行環境だけではなくて、Scikit LearnとかScipy, Numpy, Jupyter Notebookを含んでいることは事あるごとに広めていこうと思います。Googleの次の動画でもDockerを唐突に薦めてきます。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">これ見やすいし、楽しそうに紹介してくれるし、サンプルコードあるしいい感じ！ | Hello World - Machine Learning Recipes #1 <a href="https://t.co/HQ0RYnBxCO">https://t.co/HQ0RYnBxCO</a> <a href="https://twitter.com/YouTube">@YouTube</a>さんから</p>&mdash; 龍一郎 (@K_Ryuichirou) <a href="https://twitter.com/K_Ryuichirou/status/909407323457601537">2017年9月17日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

後者でTensorBoardを利用するためには何らかの方法で改めて`pip install tensorflow`を実行する必要があります。

### VSCodeに拡張機能をインストール

DockerとDocker上でJupyter Notebookをどうせ使うので、拡張機能をインストールします。

* [Microsoft/vscode-docker: Docker extension for VS Code.](https://github.com/Microsoft/vscode-docker)

* [Jupyter - Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=donjayamanne.jupyter)

### 動作確認

`F1`キーでコマンドパレットを起動した後に次を入力します。途中まで入力すると補完が効くと思います。

```
>Docker: Add docker files to workspace
```

エンターキーを連打します。

![Select Application Platform](https://qiita-image-store.s3.amazonaws.com/0/41266/fa3f1681-bea5-ebd1-b19d-7e69b2dc35d5.png "スクリーンショット 2017-09-10 16.51.15.png")

![Select using port](https://qiita-image-store.s3.amazonaws.com/0/41266/bdb48857-dbc1-a570-3de5-6353c755be43.png "スクリーンショット 2017-09-10 16.51.28.png")

![Generated files such as .dockerignore, docker-compose.yml, Dockerfile](https://qiita-image-store.s3.amazonaws.com/0/41266/6762ea59-0515-8686-8b1d-e700d8c6dc2e.png "スクリーンショット 2017-09-10 16.51.47.png")

Dockerfileが次のような内容で生成されれば大丈夫です。

```Dockerfile
# golang:onbuild automatically copies the package source,
# fetches the application dependencies, builds the program,
# and configures it to run on startup
FROM golang:onbuild
LABEL Name=vscodehandson Version=0.0.1
EXPOSE 3000

# For more control, you can copy and build manually
# FROM golang:latest
# LABEL Name=vscodehandson Version=0.0.1
# RUN mkdir /app
# ADD . /app/
# WORKDIR /app
# RUN go build -o main .
# EXPOSE 3000
# CMD ["/app/main"]
```

コンテナの起動と接続
-------------------------------------------------

### コンテナの起動

VSCodeから実行できます。F1キーを押して`Docker: Run`で大丈夫です。

![スクリーンショット 2017-09-27 22.39.32.png](https://qiita-image-store.s3.amazonaws.com/0/41266/c7bd629a-b5b8-4f6c-e6cc-ed332a4aad02.png "スクリーンショット 2017-09-27 22.39.32.png")

![スクリーンショット 2017-09-27 22.40.24.png](https://qiita-image-store.s3.amazonaws.com/0/41266/8d11843e-becd-6a60-828b-a10f85559484.png "スクリーンショット 2017-09-27 22.40.24.png")

### コンテナへの接続

同様にF1キーから`>Docker: Attach Shell to a running container`でできます。

![スクリーンショット 2017-09-27 22.44.02.png](https://qiita-image-store.s3.amazonaws.com/0/41266/c22f390c-3dda-ef82-c71c-93493dd06516.png "スクリーンショット 2017-09-27 22.44.02.png")

実行すると、現在動いているコンテナが表示されるので、接続したいコンテナを選ぶと接続できます。

![スクリーンショット 2017-09-27 22.44.17.png](https://qiita-image-store.s3.amazonaws.com/0/41266/c6b0a2f9-21ef-acbf-55a2-bdfc4fa9ac74.png "スクリーンショット 2017-09-27 22.44.17.png")

おまけ: Kitematic
-------------------------------------------------

黒い画面怖い勢というわけでもないのですが、GUIがあれば欲しい勢なのでKitematicを利用しています。紹介記事はこちらが詳しかったです。

[[Docker] Kitematicを使ってみよう！ - Qiita](https://qiita.com/akiko-pusu/items/d07751a6bf0ce6df44ce)

KitematicではVOLUMEでマウントするホストのディレクトリを指定できるのですが、`dockerfile`にVOLUMEが指定されていないイメージだとその機能が使えないようです(2017年9月現在)

次のIssueを見ると2016年から対応が進められているようですが、まだリリースされていないようです。

* [Adding a volume to an existing kitematic container ? · Issue #1923 · docker/kitematic](https://github.com/docker/kitematic/issues/1923)

Tensorflowの公式イメージはVOLUMEコマンドを含んでいませんので、次のようなDockerfileを作っています。

```dockerfile
FROM tensorflow/tensorflow:latest-py3

RUN pip --no-cache-dir install \
        edward \
        ptvsd==3.0.0
RUN mkdir /notebooks/mmt

# TensorBoard
EXPOSE 6006
# Jupyter
EXPOSE 8888
# ptvsd
EXPOSE 3000

WORKDIR "/notebooks"
VOLUME "/notebooks/mmt"

CMD ["/run_jupyter.sh", "--allow-root"]
```

edwardを入れてみたのは趣味です、またptvsdについては次で解説します。


Docker上のコンテナとのリモートデバッグ
=================================================

VSCodeはPythonのリモートデバッグに対応しています。ここから設定を見ていきます。

今回は、Docker上のコンテナでPythonのコードを実行し、Dockerホスト(= ローカル環境)のVSCodeからデバッグを行うこととします。

ptvsd
-------------------------------------------------

もともとPythonのリモートデバッグにVisual Studioが対応していて、その機能をVSCodeでも使えるようになっている、という状況みたいです。

PythonをVisual Studioで開発するのは、あまり聞き慣れない感じですがChainerの開発を行っているのを見たことがあります。

* [Python 開発者ツール | Visual Studio](https://www.visualstudio.com/ja/vs/python/?wt.mc_id=aka_ms_python&rr=https%3A%2F%2Fpypi.python.org%2Fpypi%2Fptvsd%2F3.2.1)

ptvsd自体はPyPIで配布されています。

* [ptvsd 3.2.1 : Python Package Index](https://pypi.python.org/pypi/ptvsd/3.2.1)


ptvsdのインストール
-------------------------------------------------

ptvsdはPythonのコードを実行する側にインストールしますので、Dockerfileの中でptvsdのインストールを行う記述を含めます。

また、リモートデバッグではTCP/IPポートで通信するので、Dockerfileなどで外部に公開するポートを指定しておきます。

こんな感じの記述が上のDockerfileにも含まれているのがご確認いただけます。

```Dockerfile
RUN pip --no-cache-dir install ptvsd==3.0.0

# ptvsd
EXPOSE 3000
```

なお、現在ではptvsdのバージョンを**最新ではなく3.0.0を指定しないと**動きません。ptvsd側でversion4.0で対応予定だったと思いますが、すみません、ソースが見つかりませんでした。

* [VS code remote debug connect fail. · Issue #1039 · DonJayamanne/pythonVSCode](https://github.com/DonJayamanne/pythonVSCode/issues/1039)


ptvsdのセットアップ (Docker側)
-------------------------------------------------

ptvsdの使い方はシンプルで、Pythonコード実行時にモジュールを読み込んで実行するだけです。

```python
import ptvsd
ptvsd.enable_attach("my_secret", address = ('0.0.0.0', 3000))
```

ただし、以降の手順でPythonコードをDocker上に加えて、ローカル環境のVSCodeからも動かす必要がある上、
ローカル環境ではこの行が実行されないようにしなければいけません。

公式ではDocker環境とローカル環境で2つのソースコードを分けて、ローカル環境ではこの2行をコメントアウトするように推奨されていますが、
環境変数を用いてDocker上でのみ実行されるように実行されるようにするのが良いように思います。

今回は、Docker環境での実行時にはコマンドライン引数に"debug"という文字列を与えることで、Docker環境とローカル環境を区別しています。

```python
# usage: $ python RemoteDebug.py debug
import sys

if (len(sys.argv) > 1) and (sys.argv[1] == "debug"):
    import ptvsd
    print("waiting...")
    ptvsd.enable_attach("my_secret", address=('0.0.0.0', 3000))
    ptvsd.wait_for_attach()
```

VSCodeのセットアップ (ローカル側)
-------------------------------------------------

Dockerコンテナに接続するため、VSCodeの`launch.json`に接続先の情報を書き込みます。多分、ほとんど編集しなくて大丈夫だと思います。

```launch.json
{
    "name": "Attach (Remote Debug)",
    "type": "python",
    "request": "attach",
    "localRoot": "${workspaceRoot}",
    "remoteRoot": "${workspaceRoot}",
    "port": 3000,
    "secret": "my_secret",
    "host": "localhost"
}
```

幾つか前提がありますので、補足します。

* DockerコンテナにマウントするフォルダーとPythonスクリプト、jupyter notebookをおくフォルダーは一致させました
  - 変更した場合は"localRoot", "remoteRoot"を変更すれば大丈夫だと思います
* 3000番ポートでDockerコンテナとローカル環境との通信ができることは前提とします
  - DockerコマンドやKitematicを使って確認できます
  - 変更も可能です

デバッグの様子
-------------------------------------------------

次のコードを使って検証してみます。

```python
import sys
import numpy as np

if (len(sys.argv) > 1) and (sys.argv[1] == "debug"):
    import ptvsd
    print("waiting...")
    ptvsd.enable_attach("my_secret", address=('0.0.0.0', 3000))
    ptvsd.wait_for_attach()

print('hello python!')

for i in range(10 ** 5):
    print("") #nop
    pass

print('python version:' + str(sys.version_info))
## i = ?

print('numpy version:' + np.version.full_version)
```

### ローカル環境での準備(VSCode)

VSCode側でデバッグの構成を "Attach (Remote Debug)" に変更します。

![スクリーンショット 2017-09-30 15.05.47.png](https://qiita-image-store.s3.amazonaws.com/0/41266/dea5bf70-5938-0ff3-d8d7-5e1463eed51f.png "スクリーンショット 2017-09-30 15.05.47.png")

VSCode側でブレークポイントを次のように設けておきます。

![スクリーンショット 2017-09-30 15.02.17.png](https://qiita-image-store.s3.amazonaws.com/0/41266/e5413d17-fd0c-b295-8e80-22d9994b23e0.png "スクリーンショット 2017-09-30 15.02.17.png")

また、ブレークポイントを編集し、条件付きのブレークポイントも作成しておきます。

![スクリーンショット 2017-09-30 15.03.29.png](https://qiita-image-store.s3.amazonaws.com/0/41266/11582e60-33d3-2c02-f47d-ede0c390825e.png "スクリーンショット 2017-09-30 15.03.29.png")

### Docker側での準備

何らかの適当な方法で上記のPythonコードを debug 引数付きで実行します。Jupyter Notebookの場合は次をセルに入力し、実行します。

```Jupyter Notebook
!python RemoteDebug.py debug
```

接続待ち表示になればOKです。

![スクリーンショット 2017-09-30 15.09.20.png](https://qiita-image-store.s3.amazonaws.com/0/41266/86a2fa26-6635-fbde-0ab2-f06fb56558e1.png "スクリーンショット 2017-09-30 15.09.20.png")

### リモートデバッグ

VSCodeからデバッグを実行すると、VSCode側が次のような画面になれば接続できています。(ブレークポイントの編集用ポップアップは表示されていないのが正しいです…。)

![スクリーンショット 2017-09-30 15.11.20.png](https://qiita-image-store.s3.amazonaws.com/0/41266/ce5cd207-ea87-c47f-4d22-15a408d0f641.png "スクリーンショット 2017-09-30 15.11.20.png")

Docker側を確認すると次の表示になっているはずです。接続待ちの状態から実行が進み、printが実行されたことがわかります。

![スクリーンショット 2017-09-30 15.12.03.png](https://qiita-image-store.s3.amazonaws.com/0/41266/dae8133d-e8b2-49a9-caf6-1284179648d7.png "スクリーンショット 2017-09-30 15.12.03.png")

VSCode側では変数とその中身が表示されていないため少し焦りますが、「コールスタック」中の`<module>`をクリックすると表示されます。

![スクリーンショット 2017-09-30 15.18.11.png](https://qiita-image-store.s3.amazonaws.com/0/41266/ca171691-3667-65b8-d281-9e606c589f9c.png "スクリーンショット 2017-09-30 15.18.11.png")

このあたりの挙動はちゃんと設定しきれていない気がしますので、要調査ですね。

終わりに
=================================================

無事VSCodeでDockerの環境に接続してデバッグができました！

これでTensorFlowの環境をDockerで用意してリモートデバッグしていけますね！

あとはTensorFlowのデバッグ用には`tfdbg`を使えればいいのですけれど、
明らかにターミナル前提なので諦めてDockerコンテナに乗り込んで実行する方針にします。

上記のプロジェクトをGithubに公開しましたので、お手元でお試しください。

* [AseiSugiyama/VSCodeHandsOn](https://github.com/AseiSugiyama/VSCodeHandsOn)


参考資料
=================================================

VSCode
-------------------------------------------------

### 一般的な話

* [PyCon JP 2017 Sprint - スラスラわかるPｙthon × VSCode](https://www.slideshare.net/ayatokura/pycon-jp-2017-sprint-pthon-vscode?next_slideshow=1)

* [Python: コマンドライン引数の取得 – sys.argv変数](http://www.yukun.info/blog/2008/07/python-command-line-arguments.html)

### python拡張

* [VS Code で Python をデバッグする環境構築（+ NumPy, SciPy, Matplotlib を実行する環境構築） - BEACHSIDE BLOG](http://beachside.hatenablog.com/entry/2016/08/17/004500)

* [DonJayamanne/pythonVSCode: Cross platform editing, debugging, linting, testing (and more) Python (2.7 to 3.6) code (including Jupyter support) using Visual Studio Code](https://github.com/DonJayamanne/pythonVSCode)

* [Home · DonJayamanne/pythonVSCode Wiki](https://github.com/DonJayamanne/pythonVSCode/wiki)

* [Remote Debugging | Python in Visual Studio Code](https://donjayamanne.github.io/pythonVSCodeDocs/docs/debugging_remote-debugging/)

### docker拡張

* [Microsoft/vscode-docker: Docker extension for VS Code.](https://github.com/Microsoft/vscode-docker)

Docker
-------------------------------------------------

* [Docker for Mac vs. Docker Toolbox | Docker Documentation](https://docs.docker.com/docker-for-mac/docker-toolbox/)

* [DockerImageをDockerHubに登録の仕方 - Qiita](https://qiita.com/kon_yu/items/7c40f4dfbd1cce006ce7)

* [Dockerfileのコマンド - Qiita](https://qiita.com/kmats@github/items/27c09f34aa4d338d40fc)

* [Dockerfile の書き方「私的」なベストプラクティス - ようへいの日々精進XP](http://inokara.hateblo.jp/entry/2013/12/28/121828)

* [Dockerfileを書いてみる - Qiita](http://qiita.com/nl0_blu/items/1de829288db2670276e8)

* [【入門】Dockerfileの基本的な書き方 | レコチョクのエンジニアブログ](https://techblog.recochoku.jp/1022)

* [DockerのVolume機能について実験してみたことをまとめます - Qiita](http://qiita.com/namutaka/items/f6a574f75f0997a1bb1d)

* [【Docker】Dockerでホストのディレクトリをマウントする - Qiita](http://qiita.com/Yarimizu14/items/52f4859027165a805630)


その他気になったからついでに調べたもの
-------------------------------------------------


* [kenkoooo/jupyter-autopep8: PEP8 formatter for Jupyter Notebook](https://github.com/kenkoooo/jupyter-autopep8)

* [blei-lab/edward: A library for probabilistic modeling, inference, and criticism. Deep generative models, variational inference. Runs on TensorFlow.](https://github.com/blei-lab/edward)

* [Debugging TensorFlow Programs  |  TensorFlow](https://www.tensorflow.org/programmers_guide/debugger)