【Python・Jupyter】ライブラリを自動インポートする方法・マジックコマンドの作り方

# はじめに
Jupyter使いのPythonistaの皆さん、一番上のセルに毎回同じインポート文を書くことに辟易していませんか？
それと、マジックコマンド作ってますか？

今回はJupyter生活を少し快適にする、この２点について紹介してみたいと思います。

# ライブラリを自動でインポートする方法
`~\.ipython\profile_default\startup`（`C:\Users\%USERNAME%\.ipython\profile_default\startup`(Windowsの場合)）下
のpyファイルはカーネル起動時に自動で実行されます。
本当に毎回使用するようなライブラリはこちらにインポート文を書き込んでおけば、自動的に読み込まれた状態でノートブックを開くことができます。

フォルダが見つからない場合は、コマンドラインもしくはセルから

```
# コマンドライン
ipython profile create

# セル
!ipython profile create
```
と実行すればファルダが作られると思います。


こちらにはもちろんインポート文以外も記載することが可能で、後述のマジックコマンドを定義しておけば任意のノートブックで使用可能になります。

# マジックコマンドの作り方

基本的にこちらの内容です。→ [Defining custom magics — IPython 7.27.0 documentation](https://ipython.readthedocs.io/en/stable/config/custommagics.html)

## マジックコマンドとは？
Jupyterにおけるマジックコマンドとは、`%`や`%%`から始まる特殊機能の呼び出しのことを指します。
`%time`や`%%timeit`、`%matplotlib notebook`などのコマンドは馴染み深い方も多いのではないでしょうか？

## 一番簡単なマジックコマンドの作り方
以下のように`@register_line_magic`, `@register_cell_magic`, `@register_line_cell_magic` のデコレータが付いた関数を定義することで、関数名と同名のマジックコマンドが使用可能になります。

それぞれ、
`@register_line_magic`は`%`から始まる、一行のみに対するマジックコマンド。
`@register_cell_magic`は`%%`から始まる、セル全体に対するマジックコマンド。
`@register_line_cell_magic`は`%`から始まると`%%`から始まるものの両方に対応したマジックコマンド。（例：`%time (%%time)`）
を定義することができます。

```python
from IPython.core.magic import (register_line_magic, register_cell_magic,
                                register_line_cell_magic)

@register_line_magic
def lmagic(line):
    "my line magic"
    return line

@register_cell_magic
def cmagic(line, cell):
    "my cell magic"
    return line, cell

@register_line_cell_magic
def lcmagic(line, cell=None):
    "Magic that works both as %lcmagic and as %%lcmagic"
    if cell is None:
        print("Called as line magic")
        return line
    else:
        print("Called as cell magic")
        return line, cell 
```
引数の`line`の部分にはマジックコマンドの横に書かれた文、
`cell`の部分には（マジックコマンドより下の）、セル全体の内容が渡されます。

## クラスの形でのマジックコマンド定義
こちらは恐らく、マジックコマンド内、マジックコマンド間で変数を定義・管理・共有する場合に名前空間を汚さないための方法。

クラスに`@magics_class`デコレータをつけて、`Magics`クラスを継承します。
他はデコレータの名前に`register_`が付いていない以外はほとんど同じです。
クラスの外で`get_ipython().register_magics(MyMagics)`のような形でマジックコマンドとして登録します。

```python 
from IPython.core.magic import (Magics, magics_class, line_magic,
                                cell_magic, line_cell_magic)

# The class MUST call this class decorator at creation time
@magics_class
class MyMagics(Magics):

    @line_magic
    def lmagic(self, line):
        "my line magic"
        print("Full access to the main IPython object:", self.shell)
        print("Variables in the user namespace:", list(self.shell.user_ns.keys()))
        return line

    @cell_magic
    def cmagic(self, line, cell):
        "my cell magic"
        return line, cell

    @line_cell_magic
    def lcmagic(self, line, cell=None):
        "Magic that works both as %lcmagic and as %%lcmagic"
        if cell is None:
            print("Called as line magic")
            return line
        else:
            print("Called as cell magic")
            return line, cell

get_ipython().register_magics(MyMagics)
```

## マジックコマンドでセルの内容を実行する方法
上記の定義で感じている方もいるかもしれませんが、マジックコマンドはほぼ「セルを関数に見立てたデコレータ」です。
それでいて、セルの内容はただの文字列として渡されてしまう不親切仕様です（）。

ではどうやってセルの内容を実行するのかといいますと、`get_ipython().run_cell()`や、`self.shell.run_cell()`を使います。
これにセルの内容の文字列を渡すことで、セルを実行したときと同様に内容を実行することができます。

## 使用例
指定したファイルに出力を自動で書き出すセル・マジックコマンドの例です。

```python 
from IPython.core.magic import register_cell_magic

@register_cell_magic
def file_out(line, cell):
    import sys
    from io import StringIO

    tmp_stdout = StringIO()
    stdout = sys.stdout
    sys.stdout = tmp_stdout

    get_ipython().run_cell(cell)

    sys.stdout = stdout
    print(tmp_stdout.getvalue())
    with open(line, 'a') as f:
        f.write(tmp_stdout.getvalue())
```

```python 
%%file_out output.text
for i in range(5):
    print(i)
```

```:out
0
1
2
3
4
5

```

```:./output.txt
0
1
2
3
4
5

```
## 毎回定義せずとも任意のノートブックで使用できるようにする方法
[前述](#ライブラリを自動でインポートする方法)のとおり、`~\.ipython\profile_default\startup`下のpyファイルに書き込むことで可能です。

# まとめ
## あわせ技
こんなマジックコマンドを用意しておけば、用途に応じたライブラリが１行で呼び出せます。

```python
from IPython.core.magic import Magics, magics_class, line_magic

@magics_class
class MyImport(Magics):

    @line_magic
    def imports(self, line):
        if line == 'math':
            self.import_math()
        elif line == 'table':
            self.import_table()
        elif line == 'web':
            self.import_web()

    def import_math(self):
        global math, np, plt, scipy, sympy
        import math
        import scipy
        import sympy
        import numpy as np
        import matplotlib.pyplot as plt

    def import_table(self):
        global pd, np, plt, sns
        import pandas as pd
        import numpy as np
        import matplotlib.pyplot as plt
        import seaborn as sns

    def import_web(self):
        global requests, BeautifulSoup, re, urllib
        import requests
        import re
        import urllib
        from bs4 import BeautifulSoup

get_ipython().register_magics(MyImport)
```

```python:usage_example
%imports table
```