Python embeddableで環境を汚さず可搬で複製しやすい開発環境を整える(Windows)

## 1.はじめに
今年の4月あたりからAnacondaのライセンスが変更となり、一定の規模を越える企業においてWindowsでの環境構築が面倒になりました。
Pythonの環境構築にはいくつか方法があり、様々な解説ページが世に存在します。
venvとか使うのも手ですが、今回はUSBメモリで持ち運べて複製も楽で環境を汚さない点を重視した開発環境を探求してみようと思います。
（WinPythonを自作するイメージ…。多少スリムになると思います。）

※どの方法もメリットとデメリットが存在するので、ご参考にする際にはご自身に合っているか考えていただくようお願いいたします。

参考にさせていただいたのは以下の記事
[Windowsで環境を極力汚さずにPythonを動かす方法 (Python embeddable版)](https://qiita.com/rhene/items/68941aced93ccc9c3071)
[Python embeddableへのTkinterの導入方法](https://qiita.com/Neilus/items/17b44d737d5acd1982fb)
[PYTHON環境構築するなら絶対に理解しておかないといけない注意点](https://qiita.com/oxlunaxo/items/ca7ffc059be3ffc17622)

## 2.Python embeddableを入手する
こちらから **Windows embeddable package (64-bit)** 入手します。
[Python Releases for Windows](https://www.python.org/downloads/windows/)
以下の場所になります。(Python3.9.1の場合)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248663/57382e7e-44fe-c022-2431-cb7d62f267ca.png)

zipファイルがダウンロードされるので、適当なところに解凍します。
(日本語がパスに含まれないほうが幸せかもしれない)

今回は以下のようなディレクトリ構造にしました。
>
任意の場所/
　└PythonDev/
　　└ python-x.x.x-embed-amd64

※Tensorflowを使いたい場合は3.8を入れてください。
記事執筆時点ではPython3.8までの対応なので3.9を入れても使えません。

## 3.(念のための)動作確認
Python.exeを起動して動作確認します。

コマンドラインからやりたい人は、
**アドレスバーに`"cmd"`と入力して`Enter`を押せばそのディレクトリからスタートできます。**
（これを覚えておくと、後々の作業が楽になるとおもいます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248663/25ffb8c7-3660-2369-e7ba-a06799af19e6.png)

以下を入力してPythonが起動すればOKです。

```sh

#python.exeがあるディレクトリで実行
python
```

## 4.pipを導入する
すでにここまででPythonが使えるようになっているので、文法の勉強が目的の人など、ここまでで十分な人ももしかすると存在するかも知れませんが、実用的な開発環境には程遠いので色々導入していきます。

### 4.1 前準備
まずは、先ほど解凍したフォルダの中のファイル `_python39._pth`(数字はPythonのバージョン) をメモ帳で開き、コメントアウトされているimportの行を以下のように修正します。

```python:_python39._pth
#import site
↓
import site
```

### 4.2 pipを入手
以下のGitHubリポジトリから `get-pip.py` をダウンロードし、先ほど解凍したフォルダの中に放り込みます。
[ pypa/get-pip ](https://github.com/pypa/get-pip)

直リンクも貼っておきます
(https://github.com/pypa/get-pip/raw/master/get-pip.py)

### 4.3 pipをインストール
さっそく実行します。
コマンドラインで実行するのも良いですが、インストール出来てしまえばどうでも良いので、ドラッグ＆ドロップで軽く済ませます。(メッセージが一瞬で消えるので、それが嫌な人はコマンドラインで実行しましょう)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248663/a981e6cd-e9fd-7f17-8c02-13b51aa237bd.png)

コマンドラインなら以下です。

```sh
#python.exeがあるディレクトリで実行
python get-pip.py
```

当たり前ですが、`python.exe`や`pip.exe`までPATHを通しておくとどのディレクトリからでもコマンドが通るので作業がしやすいです。
がしかし、**今回は環境を汚さず可搬できるコンセプトなのでPATHは通さず作業します。**

**以降、記事内に登場するpipコマンドはすべて「`Scripts/`フォルダ内で実行」を前提とします**

### 4.4 可搬性の背反と対処<<要注意ポイント>>
`get-pip.py`を実行することで、`Scripts/`のフォルダが作成され、その中に`pip.exe`等のファイルが生成されます。
これをバイナリエディタで開いて中を読んだら分かるのですが、ファイルの最後の方に**python.exeの絶対パスを持っています**
pythonインタプリタが指定されない場合は、この絶対パスのインタプリタを起動しにいくようです。
したがって、フォルダ名が変わるだけでも`pip`実行時に「Fatal error in launcher: Unable to create process using ～」のようなエラーを吐いて動かなくなります。

<回避策(応急対応)>
インタプリタを指定すれば絶対パスは参照されないので、以下のようにコマンドを指定することで回避できます

```
<python.exeの絶対パス> <pip.exeの絶対パス> install <パッケージ名>
```
さすがに少し面倒なので、python.exeがあるディレクトリで以下のコマンドを打ちます。

```sh
#python.exeがあるディレクトリで実行
python ./Scripts/pip.exe install <パッケージ名>
```


<根本的解決策>
**`get-pip.py`を再度実行する**ことです。(こっちの方が楽な気がします)

これは`Scripts/`に作成されるexeファイル全体に当てはまるようなので注意が必要です。


## 5. Tkinter(Tcl/Tk)の導入
*この工程はpipをインストール後の前提です*

無いとmatplotlibなどでプロットを表示できなくなります。また、Tcl/Tkを使ったUIも表示できません。
逆に言うとその程度なので、不要な人はスキップしてください。

Linux環境とかだと`pip install python-tk`とかでインストールできたりしますが、Windowosだとできません。

冒頭で紹介した参考にさせていただいた記事
[Python embeddableへのTkinterの導入方法](https://qiita.com/Neilus/items/17b44d737d5acd1982fb)によると、

1. 使用するembeddable版と**同じバージョンの**インストーラー版Pythonをインストール
1. インストールフォルダからTcl/Tk関係のファイルをコピーしてくる
1. 設定ファイル(`pythonNN._pth`)にパスを1行追加する

今回は1,2のみ実施して、3は省略します。
その代わり、ディレクトリ構造を以下のようにします。
(太字がコピーしてくるファイルです。)

>
PythonDev/
　└ python-x.x.x-embed-amd64/
　　　├ **_tkinter.pyd**
　　　├ **tcl86t.dll**
　　　├ **tk86t.dll**
　　　├ **tcl/**
　　　└ Lib/
　　　　　└ site-packages/
　　　　　　　└ **tkinter/**

正しい作業を行うと、Pythonで`import tkinter`の命令が通るようになります。
**Tcl/Tkを引っ張ってくる元のPythonのバージョンが異なっていたり、ディレクトリを間違うとエラーが出ます。**

## 6. 色々導入する
まずは、Scriptsフォルダを開きます。(pip.exeの場所)
そこでコマンドプロンプトを開いてpipで欲しいパッケージをインストールします。
(もちろん、コマンドプロンプトでそのディレクトリに移動しても良いです)

```sh
#Scriptsフォルダの中で例えば以下のコマンドを実行
pip install seaborn pandas scikit-learn opencv-python
```

この環境はPATHを通していないので、それを前提としたパッケージのインストールが失敗することがあるようです。(tensorflowなど)
冒頭で紹介した以下の記事の中で対策の解説があります。

[Windowsで環境を極力汚さずにPythonを動かす方法 (Python embeddable版)](https://qiita.com/rhene/items/68941aced93ccc9c3071)
[PYTHON環境構築するなら絶対に理解しておかないといけない注意点](https://qiita.com/oxlunaxo/items/ca7ffc059be3ffc17622)

解説によると、拡張子`.pth`のファイルをpython.exeの横に作成し、
その中に`import sys; sys.path.append('')`を記載することで回避できるようです。


以上で、Python環境自体はほぼ完成かと思います。

## 7. 開発環境を整える(Visual Studio Code)
以降ではVisual Studio Codeとの連携設定手順とJupyter Notebookの導入を行っていきます。
まずは簡単な方のVisual Studio Codeを導入していきます。

### 7.1 Visual Studio Codeの入手と展開
[ダウンロードページ](https://code.visualstudio.com/download#)から **.zip版** を入手します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248663/d6485980-e0e9-6ebb-e93c-3a23da231bdb.png)

適当なフォルダに展開します。
先に作成したPython開発環境とセットで運用したいので、以下のフォルダ構造とかですかね？まぁ個人のやりやすい構成でよいと思います。

>PythonDev/
　├ python-x.x.x-embed-amd64/
　└ VSCode-win32-x64-x.xx.x/

### 7.2 ポータブルモードで起動するようにする
VSCodeは結構前からポータブルモードが用意されています。
ポータブルモードにする方法は簡単で、`data`という名前の空フォルダを作るだけです。

>
PythonDev/
　└ VSCode-win32-x64-x.xx.x/
　　├ data/　　←これを作る
　　 :
　　└ 色々なファイルやフォルダ

この状態で`Code.exe`起動すると初期設定状態のVisual Studio Codeが立ち上がります。
拡張機能やユーザー設定はこのフォルダに格納されていきます

### 7.3 拡張機能のインストール
通常のVisual Studio Codeと一緒なので、必要な拡張機能を入れていきます。
とりあえず、Pythonを触るだけなので、以下のものあたりでしょうか？Python拡張さえ入っていればあとは自由です。

- Japanese Language Pack for Visual Studio Code
- Python (必須)
- Pylance
- indent-rainbow
- Bracket Pair Colorizer 2

別の環境に今回のフォルダを丸々移動すると、その後の初回VSCode起動でUIが日本語化されていないことがあります。
その場合は一度終了してもう一度開くと直っています。
（初回だけは拡張機能の読み込みに失敗している？と推測されます。）

### 7.4 Python環境を指定する
デバッグを行うためには環境を指定する必要があります。
設定するだけなら色々方法はありますが、今回は実際の開発を想定して、作業フォルダ`dev`を作って中に`test.py`を置くことにします。
イメージは以下の感じ

>
PythonDev/
　├ python-x.x.x-embed-amd64/
　├ VSCode-win32-x64-x.xx.x/
　└ dev/
　　└ test.py

Visual Studio Codeで`dev`フォルダを開き、`test.py`を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248663/825565ef-92a2-5dce-63a0-649fb4403476.png)


するとPython環境が分かりやすい?ところにインストールされていればそれが読み込まれます。
（無い環境の場合は選択を促すメッセージが出るはず）
イメージは下の画像です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248663/f03d9600-accd-8980-321b-fb5cd369ffdd.png)
今回の場合、私の環境はVisual Studio 2019とセットでインストールされたPythonインタプリタが初期で指定されているようです。

今回は使わないので、左下の赤丸の部分をクリック→「Enter interpreter path...」をクリックして、先ほど用意したPython embeddableのインタプリタ(python.exe)を指定します。

### 7.5 動作確認
デバッグができるかチェックします。
適当なコードを入れて動作を確かめます。

```python:test.py
print("aaa")
```

`F5`を押してデバッグを開始します。
今回は`launch.json`を作っていないのでデバッグ設定を聞かれますが、`Python File`を選択すればよいです。

すると以下の出力が得られます。

```cmd
This version of python seems to be incorrectly compiled
(internal generated filenames are not absolute).
This may make the debugger miss breakpoints.    
Related bug: http://bugs.python.org/issue1666807
aaa
```

デバッガが警告吐いていますが実行は出来ているようです。
ブレークを失敗するかもしれないと言っていますね。
もしブレークができないとデバッグが辛くなりますが、エディタとしては優秀なので、もしブレークできなくても我慢することにします。
**（本当に困ったときは大人しく環境を汚してインストールしましょう）**

まぁ、社内教育や、即席の動作確認、簡単な自動処理くらいなら耐えられるでしょう。たぶん。

## 8. 開発環境を整える(Jupyter Notebook)
個人的にはVisual Studio Codeで大半は事足りると思いますが、教育用途などではJupyter Notebookは欲しいところです。
### 8.1 インストール
以下のコマンドで入ります。

```sh
#Scriptsフォルダの中で例えば以下のコマンドを実行
pip install notebook
```

### 8.2 起動
PATHを通していないので、Scriptsフォルダでコマンドを打ちます。

```sh
#Scriptsフォルダの中で以下のどちらかで起動
jupyter notebook <作業ディレクトリの絶対パスor相対パス>
jupyter-notebook <作業ディレクトリの絶対パスor相対パス>
```
ディレクトリを指定しないとカレントディレクトリで起動します。
それは都合が悪いと思うので作業用のフォルダへの相対指定を行います。

起動が成功するとそのまま標準ブラウザが立ち上がってJupyter Notebookが開くと思います。

ターミナル(コマンドプロンプト)は作業終了まで放置です。

### 8.3 終了
ターミナル（コマンドプロンプト）で`Ctrl+C`を入力すると数秒後に終了します。

### 8.4 可搬性の背反への対策
さて、Jupyter Notebookは`Scripts/`フォルダの中に多くのの実行ファイルをインストールします。
既に気づいている方もいるかと思いますが、pip.exeと同様、**python.exeの絶対パスを内部に持っています**

したがって、ディレクトリが変わると起動しなくなります。
先ほどpipと同様にインタプリタの指定が必要です。

```sh
#python.exeのディレクトリに移動して実行
python ./Scripts/jupyter-notebook.exe <作業ディレクトリの絶対パスor相対パス>

#[実行例]今回のディレクトリ構造の場合は、python.exeのディレクトリで以下を実行
python ./Scripts/jupyter-notebook.exe ../dev
```

>※余談
コマンドを見て気づいたかもしれませんが、`jupyter.exe`を指定していません。
`jupyter.exe`は引数で渡された文字列`notebook`を`jupyter-`の文字列と連結して、`jupyter-notebook.exe`を起動しているだけのようです。


例によって**解決策は再インストール**です。
強制再インストールは以下のコマンドになります。

```sh
#python.exeのディレクトリに移動して実行
python ./Scripts/pip.exe install --force-reinstall notebook
```

### 8.5 使い勝手改善
いちいちコマンドを打ち込んで起動することも面倒だったり、
コマンドよく分かっていない人に使ってもらうこともあるかもしれません。
本質ではないところに時間を使っている場合ではないときは、バッチファイルでjupyter-notebook起動処理を書いてしまいます。

```bat:jupyter-notebook.bat
cd /d %~dp0
call "./python-x.x.x-embed-amd64/python.exe" "./python-x.x.x-embed-amd64/Scripts/jupyter-notebook.exe" "./dev"
```
※起動コマンドのフォルダ名はご自分の作っているフォルダ名に合わせて修正してください。

やっていることは
1. バッチファイルが置いてあるディレクトリに移動
2. call命令で起動する(python.exeの指定、jupyter-notebook.exeの指定、作業フォルダの指定の順で引数を与える)
だけです。ファイルの配置は以下の通りになります。

>
PythonDev/
　├ python-x.x.x-embed-amd64/　←この中にpython.exeが。
　├ dev/　　　　　　　　　　　←jupyter-notebookの作業フォルダ
　├ **jupyter-notebook.bat　　　←起動用バッチファイル**
　└ (その他色々ファイル・フォルダ)
  



## 9.GPUの利用
これができないと魅力が4割減くらいになってしまいます。(個人の感想です)
NVIDIA GPUが無い環境でセットアップを行うとTensorflowはcudaがインストール済みでもファイルが無いというエラーを吐きます。
使えるはずのGPUが使えないときは、GPU搭載(cudaやcudnnが正しく入っている)マシンでtensorflowを再インストールします。

```sh
#python.exeのディレクトリに移動する
python ./Scripts/pip.exe install --force-reinstall tensorflow
```

tensorflowは以前とは異なり、CPU版とGPU版でバイナリが共通なのは楽ですね。
GPUが使えない場合は、CPUで動きます。

## 10. 最後に
以上で可搬な開発環境ができました。（と思ってます。）

今回、このような環境を作る方法を探求しましたが、ベースにしているembeddable版が少し特殊です。
matplotlibによる簡単なグラフ描画やTensorflowのMNISTチュートリアルくらいなら走ることは確認しましたが、どこまで所望の動作をしてくれるかは分かりません。

機械学習や、自動データ処理、統計解析くらいなら環境内で閉じているので大丈夫な気がしますが、別システムと連携したりする場合は未知です。
`絶対参照`や`環境変数`を使っているモジュールも動かないリスクがあります。
また、誰でもアクセスできるところに置くなどすると、場合によってはセキュリティリスクを抱えたりもあり得るので注意願います。
**不具合や背反の存在を意識しながら自己責任でご利用ください。**

とはいえ、一定の困りごと解決の糸口になるのではないかと思います。

- 環境を汚したくない場合（汚せない場合）
- USBメモリで環境を持ち運んで、ネットワーク接続のない端末でPythonを実行したい場合
- 会社などでプロキシサーバーが厳しすぎて、何度も環境セットアップできない場合
- Windows + DockerでGPU使えないことに不便を感じる場合（GPU対応WSL2の正式リリースまで）
 - 安全性ならこれが一番ですが、組織によっては仮想環境使えないポリシーがあったり、素人にDockerは敷居が高いという課題があります
- 教育等でたくさんのPCのセットアップや復帰が面倒なとき

などなど。


以上になります。
記事の内容が少しでも皆さんの参考になれば幸いです。