概要
以前、投稿した下記の記事は%%scriptを使用したcell magicを使って、C/C++ソースをコンパイルできるようにした。これはほとんどプログラミングしないで簡単に実現できたが、機能が限られたものだった。
今回はPythonを用いてCustom Magicを実装して機能を増やしてみることにします。
実行環境
> sw_vers
ProductName: macOS
ProductVersion: 15.4.1
BuildVersion: 24E263
> python --version
Python 3.12.7
> jupyter --version
Selected Jupyter core packages...
IPython : 8.27.0
ipykernel : 6.28.0
ipywidgets : 7.8.1
jupyter_client : 8.6.0
jupyter_core : 5.7.2
jupyter_server : 2.14.1
jupyterlab : 4.2.5
nbclient : 0.8.0
nbconvert : 7.16.4
nbformat : 5.10.4
notebook : 7.2.2
qtconsole : 5.5.1
traitlets : 5.14.3
Custom Magicの作成
Custom Magicのサンプル
サンプルは上記のサイトのものを一部変更して使います。
クラスデコレーションによってmagic classを設定して、関数デコレーションによってcell magic, line magicを区別する。
import sys
from IPython.core.magic import (Magics, magics_class, line_magic,
cell_magic, line_cell_magic)
@magics_class
class MyMagics(Magics):
@line_magic
def lmagic(self, line):
"my line magic"
print("my line magic example")
return line
@cell_magic
def cmagic(self, line, cell):
"my cell magic"
print(cell, end='')
print(line, file=sys.stderr)
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
def load_ipython_extension(ipython):
ipython.register_magics(MyMagics)
if __name__ == '__main__':
load_ipython_extension(get_ipython())
上記のコードでは最後2行はサンプルには載っていないと思いますが、Kernelが立ち上がった時に自動登録するために使います。
それではテストしてみます。Custom Magicを使うためには%load_ext mymagic
を使い、コードを変更して再ロードするには%reload_ext mymagic
を使います。.py
を付けなように。
print(???, file=sys.stderr)
で出力するとピンク背景で表示されます。
return
で返した値は入力のcell番号と同じ番号で表示されます。
今回は特に必要ないのでreturn
を下記のように削除します。
import sys
from IPython.core.magic import (Magics, magics_class, line_magic,
cell_magic, line_cell_magic)
@magics_class
class MyMagics(Magics):
@line_magic
def lmagic(self, line):
"my line magic"
print("my line magic example")
@cell_magic
def cmagic(self, line, cell):
"my cell magic"
print(cell, end='')
print(line, file=sys.stderr)
@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")
else:
print("Called as cell magic")
def load_ipython_extension(ipython):
ipython.register_magics(MyMagics)
if __name__ == '__main__':
load_ipython_extension(get_ipython())
前と同じテストをします。return
で返していた値が表示されなくなったのが確認できます。
gcc cell magic
簡単なgcc cell magic
まずは簡単なgcc用のCustom Magicを作成します。
import os
import sys
import subprocess
from IPython.core.magic import (Magics, magics_class, line_magic,
cell_magic, line_cell_magic)
OBJFILE = "./a.out"
COMPILE_EXEC_CMD = "gcc -xc - -o {0} && {0}".format(OBJFILE)
@magics_class
class GCCMagics(Magics):
@cell_magic
def gcc(self, line, cell):
"""
gccでコンパイルした後にバイナリの実行
gcc -xc - -o ./a.out && ./a.out
"""
rc = self.compile_and_exec(cell)
print(rc.stdout, end="")
if rc.stderr != "":
print(rc.stderr, file=sys.stderr, end="")
def compile_and_exec(self, src):
rc = subprocess.run(COMPILE_EXEC_CMD, text=True, capture_output=True, shell=True, input=src)
if os.path.isfile(OBJFILE):
os.remove(OBJFILE)
return rc
def load_ipython_extension(ipython):
ipython.register_magics(GCCMagics)
if __name__ == "__main__":
load_ipython_extension(get_ipython())
Magic名を%%gcc
としてcellのソースをそのままgccでコンパイルして、エラーがなければ実行する。
コンパイルと実行にはsubprocess.run
を使う。コンパイルソースはinput=
に指定するとsubprocess.run
のコマンドの標準入力に渡される。 そしてsubprocess.run
の戻り値には標準出力(stdout)、標準エラー(stderr)があり、それをJupyter上に表示します。そして、生成されたバイナリがあればそれを削除します。
gccのオプション-xc
はコンパイルをC言語で、-
はソースを標準入力からの指定です。
C++でコンパイルする場合はg++ -xc++
とします。
print
文でend=""
としているのは改行を抑制しています。
テストをします。%%gcc?
をするとmagic関数のDocstringが表示されます。コンパイルエラーもエラーなしでの実行結果もちゃんと表示されました。
もっと機能をつけたgcc cell magic
import os
import sys
import subprocess
from IPython.core.magic import (Magics, magics_class, line_magic,
cell_magic, line_cell_magic)
OBJFILE = "./a.out"
COMPILE_EXEC_CMD = "gcc -xc - -o {0} && {0}".format(OBJFILE)
COMPILE_CMD = "gcc -xc - "
ALL_SOURCE = """
#include <stdio.h>
#include <stdlib.h>
{0}
int main(int argc, char *argv[]) {{
{1}
return 0;
}}
"""
@magics_class
class GCCMagics(Magics):
main_source = {} # main関数内のソース
other_source = {} # main関数外のソース
@cell_magic
def gcc(self, line, cell):
"""
%%gcc コメント
"""
rc = self.compile_and_exec(cell)
print(rc.stdout, end="")
if rc.stderr != "":
print(rc.stderr, file=sys.stderr, end="")
@cell_magic
def gmain(self, line, cell):
"""
%%gmain コメント
"""
src_key = line.strip()
if src_key != "":
self.main_source[src_key] = cell
rc = self.compile_and_exec(self.make_source())
else:
rc = self.compile_and_exec(ALL_SOURCE.format("", cell))
print(rc.stdout, end="")
if rc.stderr != "":
print(rc.stderr, file=sys.stderr, end="")
@cell_magic
def gfunc(self, line, cell):
"""
%%gfunc コメント
"""
src_key = line.strip()
if src_key == "":
print("lineパラメータがありません", file=sys.stderr)
return
else:
self.other_source[src_key] = cell
rc = self.compile_and_exec(self.make_source(), exec=False)
print(rc.stdout, end="")
if rc.stderr != "":
print(rc.stderr, file=sys.stderr, end="")
@line_magic
def glist(self, line):
"""
%glist コメント
"""
print(self.make_source(), end="")
@line_magic
def grun(self, line):
"""
%grun コメント
"""
rc = self.compile_and_exec(self.make_source())
print(rc.stdout, end="")
if rc.stderr != "":
print(rc.stderr, file=sys.stderr, end="")
def make_source(self, src_key=None):
other_src = ""
main_src = ""
for key in sorted(self.other_source):
other_src += self.other_source[key]
for key in sorted(self.main_source):
main_src += self.main_source[key]
src = ALL_SOURCE.format(other_src, main_src)
return src
def compile_and_exec(self, src, exec=True):
if exec:
rc = subprocess.run(COMPILE_EXEC_CMD, text=True, capture_output=True, shell=True, input=src)
else:
rc = subprocess.run(COMPILE_CMD, text=True, capture_output=True, shell=True, input=src)
if os.path.isfile(OBJFILE):
os.remove(OBJFILE)
return rc
def load_ipython_extension(ipython):
ipython.register_magics(GCCMagics)
if __name__ == "__main__":
load_ipython_extension(get_ipython())
- %%gcc -- cellのソースをそのままコンパイル&実行
- %%gmain -- main関数が不要でmain関数を自動で補うので、cellのソースはmain関数内に挿入
- lineパラメータがない場合はcellのソースだけmain関数を付加してコンパイル&実行
- lineパラメータがある場合はそれをkeyとしてソースを辞書に登録し、%%gfuncで登録した辞書とここで登録した辞書をkeyのソート順に挿入したフルソースをコンパイル&実行
- %%gfunc -- main関数の外に挿入。関数やグローバル変数の記述に使う
- lineパラメータがない場合はエラー
- lineパラメータがある場合はそれをkeyとしてソースを辞書に登録し、%%gmainで登録した辞書とここで登録した辞書をkeyのソート順に挿入したフルソースをコンパイルのみ
- %glist -- %%gmainや%%funcで登録したコードにmain関数を付加したフルコードを表示
- %grun -- %%gmainや%%funcで登録したコードにmain関数を付加したフルコードをコンパイル&実行
テストします。
最初から%reload_ext
でも良いようです。
%load_extを使わないでCustom Magicの自動登録
以下のディレクトリにgccmagic.py
を移します。startup
ディレクトリにREADMEがあるのでそれを参考にscript名の前に番号をつけるのも良いと思います。
~/.ipython/profile_default/startup/
This is the IPython startup directory
.py and .ipy files in this directory will be run *prior* to any code or files specified
via the exec_lines or exec_files configurables whenever you load this profile.
Files will be run in lexicographical order, so you can control the execution order of files
with a prefix, e.g.::
00-first.py
50-middle.py
99-last.ipy
終わりに
すこし改良の余地があります。
- %%gmain key で実行するとき、このcellに標準出力が合った場合それだけ出力させる。つまりPythonのようにそのcellだけが実行されたように見せる
-
rc.stdout
を編集すれば出来そう
-
- includeやコンパイル時のライブラリの指定
- includeは%%gfuncでも代用可
- main関数のテンプレートをcellから変える
- コンパイルするフルソースを一緒に表示するかのon,off
-
self.main_source
、self.other_source
のクリアや特定keyの削除 - ハイライトなどがPythonなので、これをC/C++にしたい
- たぶんCustom Magicでは出来ないと思う
- 専用kernelなら可能かも