Jupyter notebook で他の notebook の特定のセルをインポートする

# はじめに
Jupyter notebook でデータ分析なり実験なりしていると、分析用のクラス等を定義した他の notebook の特定のセルをインポートしたいなんてことがよくあります。うまくいきそうな方法を2つほど紹介します。

# 1. %%writefile マジックコマンド
一番手軽な方法はマジックコマンドの`%%writefile`を使うことです。セルの先頭に`%%writefile filename`とすることで`filename`にセルの内容がコピーされます。あとは他の notebook から`filename`をインポートするだけです。

```python:他のnotebookから読み込みたいセル
%%writefile hoge.py
class Hoge:
    def __init__(self):
        print('Hello from Hoge')
```

```python:出力された他のnotebookのセルの内容を読み込んで利用する
import hoge

x = hoge.Hoge()
```

# 2. 頑張って自作する
[公式](https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Importing%20Notebooks.html)が「それみんなやりたがるし、public な API だけでそんな苦労しなくてもできるよ！」って言ってる~~だったらコマンドひとつでできるようにしておいてほしい~~のでリンク先のコードを参考に自作します[^tdual]。

[^tdual]: リンク先の内容については @tdual さんが[記事](https://qiita.com/tdual/items/32d3918b4c8dd1f703e7)を書いてくださっています。調べたところ @tdual さんの記事で公開されているコード（つまり公式で紹介されているコード）は Python 3.4 あたりから非推奨になったレガシーなコードが混じっているようなので、modcell では若干の手直しが入っています。もしバージョンの関係などで modcell がうまく動かないときは @tdual さんのコードを試すとよいと思います。

リンク先のコードはモジュールとして読み込んだ notebook のコードセルをすべて実行してしまうので、コードセルの先頭にあるコメントに`# modcell`と書いてあるセルを識別して読み込むように改造してみました。

`modcell`というライブラリとして公開したのでもしよければ使ってください。

* GitHub: [wsuzume/modcell](https://github.com/wsuzume/modcell)

（探せば私よりも clever なライブラリ作ってる人がいるはずなので、もしいいライブラリを他にご存知の方がいらっしゃいましたら教えていただけると幸いです）

### Usage
`modcell`は`sys.path`に登録されているディレクトリに存在する拡張子`.ipynb`のファイルを探索の対象とし、コードセルの（マジックコマンドを除いた）先頭の行に書かれている`# modcell`というコメントを識別してモジュールとしてインポートします。

#### インストール
```
pip install modcell
```

#### modcell のインポート
`modcell`モジュールをインポートすると`.ipynb`を読み込むためのローダーが`sys.meta_path`に追加されます。以降は拡張子が`.ipynb`のファイルをインポートすることが可能になります。

```python
import modcell as mods
```

#### .ipynb ファイルをインポート
通常のモジュールと同じように拡張子を省いた名前でインポートすると、（IPythonのマジックコマンドを除いた）セルの先頭に`# modcell`と書かれたセルをすべてインポートします。 

```python:importされる側のnotebook(test_module.ipynb)のとあるセル
# modcell
class TestModule:
    def __init__(self):
        pass

    def hello(self):
        print('Hello from TestModule')
```

```python:importする側のnotebookのとあるセル
import test_module as mod

x = mod.TestModule()
x.hello()
```

`modcell`は IPython のマジックコマンド（`!`または`%`で始まる行）を無視します。つまり`modcell`によるインポートが行われたとき`%autoreload`や`%%time`は実行されません。

また`modcell`自体が`%autoreload`とかに対応してるかはテストしてないので知りません。`Restart and Run all`すれば他の notebook を再読み込みしてくれることは確認しております。

#### コンパイル
`%%writefile`の上位互換です。インポートしたすべてのセルをファイルに出力します。

```python
import modcell as mods
import test_module

with open('module.py', mode='w') as f:
    mods.compile(out=f, source_info=True)
```

以下のようにフォーマットされます。

```python:module.py
# test_module.ipynb ---------
# ---

class TestModule:
    def __init__(self):
        pass
    
    def hello(self):
        print('Hello from TestModule')

# ---
# --------- test_module.ipynb
```

デバッグなどの観点からセルに対応した区切りコメントが出力されますが、邪魔なときは`source_info=False`を指定すれば出力されません。

#### .ipynb から特定のタグを持つセルだけをインポート
セルの先頭に書くコメントを

```python:test_module.ipynbのとあるセル
# modcell: tagname
class TestModule:
    def __init__(self):
        pass

    def hello(self):
        print('Hello from TestModule')
```

の構文にすることで各セルにひとつだけタグをつけることができます。インポートする場合は

```python
import modcell as mods

mod = mods._import('test_module', tag='tagname')

x = mod.TestModule().hello()
```

とすることで`tagname`というタグがついたセルだけを読み込むことができます。

#### 複数のモジュールファイルへのコンパイル
`modcell`はデフォルトで`modcell.ModCell`クラスのインスタンスをひとつ生成しており、すべての関数はそのデフォルトインスタンスのメソッドとして実行されています。このデフォルトインスタンスは`modcell.default()`関数で取得することができますが、ユーザが操作することは非推奨です。

ユーザは必要に応じて複数の`ModCell`インスタンスを作成することが可能です。

```python
import modcell as mods

mod_1 = mods.ModCell()
mod_2 = mods.ModCell()
...
```

つまり以下のようなコードを実行することで、複数の`.ipynb`から複数の`.py`ファイルを生成できます。

```python
import modcell as mods

mod_debug = mods.ModCell()
nb1_debug = mod_debug._import('notebook_1', tag='debug')
nb2 = mod_debug._import('notebook_2')
nb3 = mod_debug._import('notebook_3')
with open('../module/mod_debug.py', mode='w') as f:
    mod_debug.compile(out=f)

mod_test = mods.ModCell()
nb1_test = mod_test._import('notebook_1', tag='test')
nb4 = mod_test._import('notebook_4')
with open('../module/mod_test.py', mode='w') as f:
    mod_test.compile(out=f)
```


# おまけ
もうひとつ思いついた方法として「IPython のカーネルにアタッチして他の notebook のコードセルの中身を引っ張ってくる」というものがあったが、どうもコードセルの中身を保持しているのはフロントエンドの JavaScript であって IPython のカーネルはパーサーにかけた後のコードしか保持していないらしいのでボツ案となった。

https://stackoverflow.com/questions/51121647/get-the-current-contents-of-the-entire-jupyter-notebook