venv を condaっぽく使う

長いことPython環境の管理には [conda](https://docs.conda.io/en/latest/miniconda.html) のお世話になってきたのですが、最近は [venv](https://docs.python.org/ja/3/library/venv.html) が主流になりつつあるそうです。
主たる理由は標準ライブラリに組み込まれたこと、それから依存関係で不具合を起こしやすかった機械学習ライブラリの多くが、`pip install` で十分解決できるようになってきたことかと思います。

そこで、一念発起してvenvへの意向を試みたのですが、condaとの微妙なシンタックスの違いで慣れない部分がありました。
特に、

```shell
python -m venv env/path/name       # 任意の場所に仮想環境を作れる
source env/path/name/bin/activate  # アクティブ化するときはパスを入力する 
```

のように、アクティブ化するときにパス名を入力するのが面倒だな、と思いました。
`conda`だと、仮想環境はすべて`miniconda/envs` 内に作られ、どこからでも `conda activate <envname>`で使えます。
プロジェクトフォルダ内に環境を作っていれば良いのですが、汎用的な環境を使い回す場面も多いので、小回りがほしいと思いました。
そういうわけで、`venv`を`conda`っぽく使う方法について考えました。

## tl;dr

下記のシェルスクリプトを `.bashrc` や `.bash_profile`など、端末開始時に読み込まれるファイルに追加することで、

1. `venvc <envname>` で仮想環境が作られます。実体のフォルダは `$VENVROOT` 以下に作られます。
1. `venvc2`, `venvc3` でPythonのバージョンを選べます。
1. `venva` <envname>` で仮想環境をアクティブにします。
1. `venvd` で仮想環境を非アクティブ化します。`deactivate`と同じです。
1. `venvr <envname>` で仮想環境を削除します。つまり、実体のフォルダを消します。
1. `venvl` で仮想環境の一覧を表示します。つまり、`$VENVROOT`直下のフォルダを表示します。
1. 通常通り `venv`コマンドを使って任意の場所に仮想環境を作ることには干渉しません。

```shell:.bashrc,.bash_profileなど
# choose the default venv location
VENVROOT="$HOME/venv"

function _venvc() {
  # internally used by venv3 and venv2
  # $1 : env name
  # $2 : python version
  if [ -z "$1" ]
  then
    echo "Provide env name; e.g. venvc$2 <envname>"
  else
    python$2 -m venv "$VENVROOT/$1" && echo "Created env at $VENVROOT/$1"
  fi
}

function venvc2() { _venvc "$1" 2; }
function venvc3() { _venvc "$1" 3; }
alias venvc=venvc3
# because i use python3 most of the time

function venva() { source "$VENVROOT/$1/bin/activate"; }
alias venvd=deactivate

function venvr() { 
  # $1   : env name
  # $2...: options to rm, in case -f or sort is needed
  envname="$1" 
  shift 
  if [ -z "$envname" ]
  then
    echo "Provide env name; e.g. vencr <envname>"
  else
    rm -r $@ "$VENVROOT/$envname" &&  echo "Removed $VENVROOT/$envname"
  fi
}

function venvl() {
  # $1...: options to ls
  ls $@ "$VENVROOT"
}
```

## 詳しい説明

### venvの通常の使い方

#### 仮想環境の作成

`venv`を用いて仮想環境を作るには、次のコマンドを実行します。

```shell
$ python3 -m venv myenv
# ここで python3としているのは、多くのOSでデフォルトのpythonが python2になっていることが多いためです
# python2 や python にすると、対応するバージョンをベースにした仮想環境になります。
```

すると、カレントディレクトリ内に、`myenv`というフォルダが作られ、そのなかに仮想環境関係のファイルが作られます。確認してみます。

```shell
$ ls myenv/bin -l

#-rw-r--r-- 1 user user 2189  8月 28 07:23 activate
#-rw-r--r-- 1 user user 1241  8月 28 07:23 activate.csh
#-rw-r--r-- 1 user user 2393  8月 28 07:23 activate.fish
#-rw-r--r-- 1 user user 8834  8月 28 07:23 Activate.ps1
#-rwxrwxr-x 1 user user  254  8月 28 07:23 easy_install
#-rwxrwxr-x 1 user user  254  8月 28 07:23 easy_install-3.8
#-rwxrwxr-x 1 user user  245  8月 28 07:23 pip
#-rwxrwxr-x 1 user user  245  8月 28 07:23 pip3
#-rwxrwxr-x 1 user user  245  8月 28 07:23 pip3.8
#lrwxrwxrwx 1 user user    7  8月 28 07:23 python -> python3
#lrwxrwxrwx 1 user user   16  8月 28 07:23 python3 -> /usr/bin/python3
```

- `myenv/bin/python` -> `myenv/bin/python3` -> `/usr/bin/python3` というようにシンボリックリンクが貼られています。つまり、この環境において`python`を実行すると、`/usr/bin/python3`が実行されるということです。
- `pip` はシンボリックリンクではないようです。サイズが小さいからでしょうか。

#### 仮想環境の利用

作成した仮想環境を利用するには、次のコマンドを実行します。

```shell
$ source myenv/bin/activate
``` 

試しに pythonのバージョンを確認してみます。

```shell
(myenv) $ python -V
#Python 3.8.10

(myenv) $ which python
# /***/***/myenv/bin/python
```

確かに `python3`が使われ、また、コマンドは仮想環境内のシンボリックリンクを指しているようです。
つまり、これは実行ファイルの探索パスが変わっているということです。調べてみます。

```shell
(myenv) $ echo "$PATH"
# /***/***/myenv/bin:********
```

確かに、仮想環境内の`bin/`フォルダが最初に探索される場所になっています。これにより、仮想環境内のPythonが優先的に使われるようになります。

仮想環境の主たる目的はパッケージ群の管理なので、仕組みが気になります。これも確認しておきます。

```shell
(myenv) $ python -c "import sys; print(sys.path)"
#['', '/usr/lib/python38.zip', '/usr/lib/python3.8',
# '/usr/lib/python3.8/lib-dynload', '/***/***/myenv/lib/python3.8/site-packages']
```

最初にローカルフォルダ、次にグローバルなパッケージ、最後に仮想環境内のパッケージ、という順になっています。ということは、グローバル環境をベースに、それに仮想環境内でパッケージを追加するイメージのようです。

#### 仮想環境の非アクティブ化

仮想環境の利用を終了するには、次のコマンドです。

```shell
(myenv) $ deactivate

$ python -V
#Python 2.7.18
```

非アクティブ化すると `(myenv)`の文字が消え、 Pythonのバージョンもデフォルトのものに戻りました。

#### 仮想環境を削除する

特に削除コマンドは用意されていないようです。仮想環境の実体はフォルダに過ぎないので、これを削除することで替えるのが現状のようです。
場合によっては、`-f`オプションが必要なこともあるかもしれません。

```shell
$ rm -r myenv
# or
$ rm -rf myenv
```

### condaっぽくする方法

`venv`による作業はコマンドの実行すぎないので、それをラッピングすれば`conda`っぽくできそうです。
つまり、共通のルートディレクトリ（`VENVROOT=$HOME/venv` としている部分）を指定し、その中に仮想環境を作るようコード化すればよいです。

少し考慮が必要なのが、アクティブ化の処理です。

```shell
$ source myenv/bin/activate
```

この `source`というのは、実行シェルと同じ環境でスクリプトを実行することを意味します。こうしている理由は、`$PATH`変数など、実行元の環境への変更を伴う作業だからです。そのため、

```shell
$ bash myenv/bin/activate
```
では目的は達成されません。`bash`から呼ぶと、スクリプトは別プロセスで実行されるので、その途中で起こった変数変更は元の環境へは影響しないためです。

ここで行いたいことは、`venva <envname>` とすると、 `source $VENVROOT/<envname>/bin/activate` が実行元のシェルで実行される、ということです。それを実現する1つの方法が、関数を作ることです。というのも、シェルスクリプトの関数は、同一シェルで実行されるからです。つまり、

```shell
function venva() {
  source "$VENVROOT/$1/bin/activate"
} 
```

という関数を作れば良いということです。
そこで、全てのコマンドをシェル関数にラッピングして、それを `.bashrc` に追加することで、端末で常に使える状態にしています。使っているとコマンドとの差違が見えませんが、ファイルの実体がないため、`which venva`が何も返さないという特徴があります。
その他の実装については、シェルの関数を書いているだけなので説明を割愛をします。