VimをJupyterノートブックの開発環境に仕上げる

スポンサード リンク

データサイエンス界隈では何かとJupyterのノートブックを開発する機会がある。そのたびにJupyterを立ち上げてブラウザで接続して...という操作も面倒であるし,JupyterのGUIでは開発効率があまり良くない。やはり,慣れ親しんだVimとターミナルのうえにJupyterと同じような開発環境を整備したいところだ。VSCodeならノートブックを編集したり,セルごとに実行できるのだから,同じことがVimにできないはずはないだろう。

ということで,本記事ではVimのプラグインを駆使してノートブックを

  1. 編集可能にする
  2. セルごとに実行可能にする
  3. リモートサーバに接続してノートブックを実行可能にする

ための手順を大きく3つに分けて説明する。ポイントはJupyterのように「セルごとにコード片を実行し,結果を確認できる」ことであり,これは REPL (Read-Eval-Print Loop) と呼ばれる作業だ。本記事の目的は「VimでノートブックのREPL環境を作る」ことと言える。

Vimにノートブック開発環境を整える方法は一通りではないので本記事ではそれぞれの方法を示すが,おすすめは goerz/jupytext.vimjupyter-vim/jupyter-vim を組み合わせて利用する構成だ。

1. [必須] Vimでノートブックを編集可能にする

ノートブックの実体はJSONファイルであるため,単にVimでノートブックを開くだけでは編集が難しい。そこで,一時的にPythonやMarkdownの形式に変換することで編集可能にするVimプラグイン goerz/jupytext.vim を利用する。

jupytext.vimはバックグラウンドでjupytextを実行することで,

  1. ノートブック .ipynb をVimで開いたら,Pythonスクリプトに変換し,Vimに表示
  2. 変換済みのPythonスクリプトを保存したら,ノートブック .ipynb に復元して保存

といった処理を自動実行する。これにより,Vimでノートブックをスムーズに編集可能になる。

図1. Jupyterノートブックの編集問題

Jupytext.vimのインストール

まず,Pythonのパッケージとして jupytext をインストールする。jupytextはノートブックをMarkdownやPythonスクリプトの形式に変換するツールだ。一旦,Pythonスクリプト等に変換しても,元のノートブックのセル出力を保持したまま復元できる特徴がある。Vimとの組み合わせ以外にもjupytextの利用シーンは多々あるので 公式マニュアル の内容も合わせて読んでおくと良い。

pip3 install jupytext

次に,Vimプラグインの jupytext.vim をインストールする。NeoBundleを使う場合は以下の1行を .vimrc に記述したうえで :Neobundleinstall コマンドをVimで実行する。

NeoBundle 'goerz/jupytext.vim'

最後に jupytext.vim 向けの設定を .vimrc に追加する。ここではノートブック .ipynb を Pythonスクリプト .py の形式に変換する設定を加えている。これにより,前述の図1の右下のようなPython形式に自動変換できる。

ポイントは py:percent の指定でセルの区切り文字をVSCode互換# %% に設定していること。Jupytextのデフォルト設定ではセル内に空行がない場合は区切り文字を省略するため,セルの境界が不明確になる。この点は,ノートブックのセル実行に必要なvim-ipython-cellやjupyter-vimプラグインと組み合わせるときに不都合が生じる。

" セルの区切り文字をVSCode互換の # %% に指定する
let g:jupytext_fmt = 'py:percent'

" vimのPython向けシンタックスハイライトを有効にする
let g:jupytext_filetype_map = {'py': 'python'}

その他,Jupytextのフォーマット指定の詳細は Notebook formats Jupytext documentation を参照いただきたい。

2. Vimでセルごとにノートブックを実行可能にする

VimでREPL環境を実現する (セルごとにノートブックを実行可能にする) 方式は大別して2つある。それぞれの利点と欠点を書き下すと以下のようになる。どちらも一長一短ではあるので本記事では双方の方法を明記する。なお,私個人は選択肢2の利点 セル実行中に次のセル実行を指示できる ことや Matplotlibの画像もインライン表示できる ことは捨てがたく,選択肢2を採用した。

  • 選択肢1. クリップボードを経由してipythonに貼り付ける方式
    • 利点:
      • SSH先のターミナルでも特別な設定なしに実行できる
      • ipython以外の任意のシェルにも利用可能であり,汎用性が高い
    • 欠点:
      • 実行中の処理が終わるまで,次のセルの実行を指示できない
      • 利用可能なターミナルが限られる
  • 選択肢2. tcp接続でipythonのカーネルにセル実行を指示する方式
    • 利点:
      • セル実行中に次のセル実行を指示できる
      • Matplotlibの画像をインライン表示できる
    • 欠点:
      • SSH先のリモートサーバへ接続する場合,設定が煩雑になる
      • リモートとローカルのファイル共有を意識する必要がある

[選択肢1] クリップボードを経由してipythonに貼り付ける方法

選択肢1を実現するためには,hanschen/vim-ipython-celljpalardy/vim-slime の2つのプラグインを組み合わせる。vim-ipython-cellはvim-slimeをベースにJupyter向けに機能追加したVimプラグインであり,以下の画像のようなノートブックのセル実行を実現する。

図2. vim-ipython-cellによるセル実行。画像はhanschen/vim-ipython-cellより引用した

vim-ipython-cellのインストール

まず,必要なパッケージをインストールする。

pip3 install ipython  # Pythonのコードセルを実行するipythonを準備
sudo apt-get -y install screen  # コピペの実装に必要。tmuxやX11等でも良い
sudo apt-get -y install python3-tk  # [オプション] matplotlibで画像を表示したい場合

次に,Vimプラグインをインストールする。NeoBundleを使う場合は以下の2行を .vimrc に記述したうえで :Neobundleinstall コマンドをVimで実行すれば良い。

NeoBundle 'jpalardy/vim-slime', { 'on_ft': 'python' }
NeoBundle 'hanschen/vim-ipython-cell', { 'on_ft': 'python' }

vim-ipython-cellの設定

最後に.vimrc に設定を加える。以下の設定は Screen と jpalardy/vim-slime を組み合わせる場合の設定である。この設定では同じScreenのセッションで,ウィンドウ名 ipython3 のシェルにセルのコードを送信する。なお,tmuxなどを使いたい場合は jpalardy/vim-slime を参照して適宜設定を加えること。

" screenを使う
let g:slime_target = "screen"
" ipythonを使う
let g:slime_python_ipython = 1
" セルの区切り文字を指定
let g:slime_cell_delimiter = "# %%"

" 環境変数 $STY から GNU Screen のセッション名を取得する
let g:slime_default_config = {"sessionname": $STY, "windowname": "ipython3"}
" 接続先情報はユーザ入力させない
let g:slime_dont_ask_default = 1

" mappings
"" 選択範囲を実行
xmap <F5> <Plug>SlimeRegionSend
nmap <F5> <Plug>SlimeParagraphSend
"" セルを実行
nnoremap <F6> :IPythonCellExecuteCellVerbose<CR>
"" セルを実行して次のセルへ移動
nnoremap <C-M> :IPythonCellExecuteCellVerboseJump<CR>

vim-ipython-cellの使い方

ここまで設定できたら,以下のような手順でノートブックのセルを実行できる。このときの操作感のイメージは図2を参照のこと。ただし,図2をよく見ると実行したPythonのコードがipythonのログに残っていない。これは :IPythonCellExecuteCell の仕様だが,これでは過去に実行済みのコードがわからなくなる問題があるので,きちんとコードも表示する :IPythonCellExecuteCellVerbose を利用したほうが良い。

  1. ターミナルでGNU Screenを起動
  2. Screenのwindowでipython3を実行,window名を ipython3 に設定
  3. Screenの別windowでVimを起動。ノートブックを開く
  4. Vimのカーソルを実行したいセルに合わせ,:IPythonCellExecuteCellVerbose を実行

また,vim-ipython-cellの実装は,クリップボードを経由して当該セルの内容をipythonに貼り付けるだけの実装である。ゆえにジョブのキューイングなどはできない。時間のかかる処理をipythonが実行している間は,次のセル実行を指示しても正しくipythonで実行できない。この点は実用上,要注意である。

[選択肢2] ipythonのカーネルにtcp接続してセル実行を指示する方法

選択肢2を実現するためには jupyter-vim/jupyter-vim のVimプラグインと jupyter/qtconsole を組み合わせる。この方式では,jupyter-vimプラグインからJupyterのQtConsoleにtcp接続をして,カーネルに実行したいコードを送付する実装となっている。これによりジョブのキューイングが正しく動作するため,QtConsole側で計算中に新たなセル実行を指示できる利点がある。また,QtConsoleであればMatplotlibのグラフをインラインで表示できる利点もある。

なお,jupyter qtconsoleはGUIアプリだが,ターミナルで起動する jupyter console と jupyter-vim を組み合わせて利用することもできる。しかし,Vimからセル実行を指示しても,jupyter consoleの側でEnterキーを押下しないと実行されない仕様になっているため,実用性は低い。ターミナルに閉じて利用したい場合は,vim-ipython-cellを使うほうが良いだろう。

図3. jupyter-vimとQtConsoleによるセル実行

jupyter-vimのインストール

まず,Jupyter QtConsoleを利用するために以下のパッケージをインストールする。

pip3 install jupyter
pip3 install qtconsole
pip3 install PyQt5 # もしエラーになるなら PyQt5==5.12.2 にダウングレードする

PyQt5のインストールは pip ではうまく出来ないことがある。もし,pythonの仮想環境を使わないなら apt でシステム側に PyQt5 とその関連ライブラリをインストールしても良い。

sudo apt-get install python3-pyqt5 pyqt5-dev-tools qttools5-dev-tools python3-pyqt5.qtsvg

次に,Vimプラグインをインストールする。NeoBundleを使う場合は以下の2行を .vimrc に記述したうえで :Neobundleinstall コマンドをVimで実行すれば良い。

NeoBundle 'jupyter-vim/jupyter-vim'

jupyter-vimの設定

jupyter-vimの動作に必要な設定を加える。まず,Jupyter QtConsoleの設定ファイルを生成する。

jupyter qtconsole --generate-config

生成された設定ファイル ~/.jupyter/jupyter_qtconsole_config.py に以下の1行を挿入する。

c.ConsoleWidget.include_other_output = True

jupyter-vimとQtConsoleの組み合わせには画像表示に画面スクロールが自動的に追従しない問題 がある。少なくとも私の環境 (Jupyter QtConsole 4.7.5) では再現した。例えば,以下のようなコードを実行して自動スクロールしない場合は修正が必要である。

import matplotlib.pyplot as plt
for i in range(10):
    plt.plot([1,2])
    plt.show()

この問題はQtConsoleのコードに1行を追加するだけで修正できる。具体的には Scroll down for output from remote command by ajtam Pull Request #349 jupyter/qtconsole の記載に従って qtconsole/console_widget.py の 970行目付近 _append_custom 関数の末尾に,以下のように self._control.moveCursor(QtGui.QTextCursor.End) の1行を挿入する。

    def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
        """ A low-level method for appending content to the end of the buffer.

        If 'before_prompt' is enabled, the content will be inserted before the
        current prompt, if there is one.
        """
        # Determine where to insert the content.
        cursor = self._control.textCursor()
        if before_prompt and (self._reading or not self._executing):
            self._flush_pending_stream()
            cursor.setPosition(self._append_before_prompt_pos)
        else:
            if insert != self._insert_plain_text:
                self._flush_pending_stream()
            cursor.movePosition(QtGui.QTextCursor.End)

        # Perform the insertion.
        result = insert(cursor, input, *args, **kwargs)
        self._control.moveCursor(QtGui.QTextCursor.End)  # この行を新規に加える
        return result

jupyter-vimの使い方

ここまでくれば,jupyter-vimを利用してノートブックのセルを実行できる。まず,コードを実行する画面を準備するため,以下のコマンドでQtConsoleを起動する。

jupyter qtconsole

次にVimを起動し, :JupyterConnect コマンドでQtConsoleに接続する。後は実行したいセルにカーソルをあわせ,:JupyterSendCell コマンドを実行すれば図3のようにセルを実行できる。

3. リモートサーバでJupyterノートブックを実行する

Jupyterをリモートのサーバで起動すれば,ローカルのノートPCでは実行できない重たい計算処理をスムーズに実行できる。同様にVimでもリモートサーバに計算処理を任せたいことはよくあるので,その方法を記す。

具体的な実装は,前述の選択肢1 (vim-ipython-cellを使う場合) と選択肢2 (jupyter-vimを使う場合) で異なる。複雑なのは後者であり,その詳細を説明する。

[選択肢1] vim-ipython-cellを使う場合

vim-ipython-cellを利用する場合は難しいことはない。単にリモートサーバにssh接続してから,screenとvim,ipythonを起動してローカルマシンと同様に利用すれば良い。

[選択肢2] jupyter-vimを使う場合

jupyter-vimを使う場合は,以下の2つの方式がある。こちらも,それぞれ利点と欠点があるが,私個人は方式1のほうがシンプルで好きだ。

  • 方式1. リモートにX転送付きのSSH接続をし,QtConsoleとVimを立ち上げる方式
    • 利点: 処理がリモートサーバで閉じるのでファイル共有の問題が生じない
    • 欠点: X転送ではQtConsoleの画面更新が遅い
  • 方式2. ローカルのQtConsoleとVimを,リモートのipythonカーネルと通信させる方式
    • 利点: リモートサーバのQtCOnsole等を環境整備する手間がない
    • 欠点: ローカルマシンとリモートサーバのファイル共有が必要になることがある

方式1の実施方法

図4. 方式1の構成図

VimとQtConsoleをリモートで実行する場合はほとんどローカルマシンに閉じて使う場合と同じ操作になる。異なる点は,リモートにX転送付きのSSH接続をしてからVimとQtConsoleを立ち上げる点である。

# リモートサーバにX転送を有効化したSSH接続をする
ssh -Y remote

# [オプション] 開発用の仮想環境を準備する
python3 -m venv .devel
source .devel/bin/activate

# リモートサーバにもQtConsoleに必要なライブラリをインストールする
pip3 install jupyter
pip3 install qtconsole
pip3 install PyQt5 # もしエラーになるなら PyQt5==5.12.2 にダウングレードする

# リモートサーバでQtConsoleを実行する
jupyter qtconsole

後はリモートサーバでVimを起動し,:JupyterConnect コマンドを実行して開発を開始するだけ。

方式2の実施方法

図5. 方式2の構成図

vagrant上のipython kernelに接続してjupyter を使う - Qiita の記事にあるように,リモートサーバで ipythonのカーネルだけを起動して,ローカルマシンのQtConsoleから呼び出す方式の実現方法を示す。この方式であれば,リモートサーバにipython以外の追加ライブラリは不要なので環境整備の手間が少ない利点がある。

まず,リモートサーバでipythonカーネルを以下のように起動して準備する。

# リモートサーバにSSHで接続する
ssh remote

# [オプション] 開発用の仮想環境を準備する
python3 -m venv .devel
source .devel/bin/activate

# リモートサーバに必要なライブラリをインストールする
pip3 install ipython

# リモートサーバでipythonを実行する
ipython kernel -f /tmp/ipython.json

次に,ローカルマシンで以下のコマンドを実行し,QtConsoleをipythonカーネルに接続する。

# ipythonカーネルの接続情報を取得する
scp remote:/tmp/ipython.json /tmp/ipython.json

# qtcosoleを起動してipythonカーネルに接続する
jupyter qtconsole --existing /tmp/ipython.json --ssh remote

最後にVimを起動して :JupyterConnect /tmp/ipython-ssh.json のコマンドを実行し,QtConsoleに接続する。

まとめ

以上の操作をシェルスクリプトにまとめると以下のようになる。ここでは,指定したリモートホストにSSH接続をして,ipythonカーネルを起動し,ローカルのQtConsoleで接続するところまでを自動化する。シェルスクリプトの1つめの引数は .ssh/config に記載のホスト名であり,2つめの引数はipythonカーネルの起動に利用するPythonの仮想環境 (venv) のパスである。もしも,1つめの引数がない場合は方式1のように,ローカルでQtConsoleを起動する

#!/bin/bash

function connect(){
    # 指定したリモートホストにSSH接続をして,ipythonカーネルを起動し,ローカルのQtConsoleで接続する
    # 1つめの引数は .ssh/config に記載のホスト名
    # 2つめの引数はipythonカーネルの起動に利用するPythonの仮想環境 (venv)
    # 1つめの引数がない場合はローカルでipythonカーネルを起動する
    REMOTE="${1:-default}"
    VENVIDR="${2:-~/.devel}"

    if [[ ${REMOTE} != "default" ]]; then
        echo 'Running a remote kernel'
        ssh ${REMOTE} "source ${VENVDIR}/bin/activate && ipython kernel -f /tmp/ipython.json &"
        scp ${REMOTE}:/tmp/ipython.json /tmp/ipython.json
        jupyter qtconsole --existing /tmp/ipython.json --ssh ${REMOTE}
    else
        echo 'Running a local kernel'
        rm -f /tmp/ipython-ssh.json
        jupyter qtconsole -f /tmp/ipython-ssh.json
    fi
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    connect "$@"
fi

参考URL

jupytext.vimの関連情報

vim-ipython-cellの関連情報

jupyter-vimの関連情報

Comments !

social