Jupyterでコンテナーを入れ子の枠で表現（図式化）、とそれを作る過程で調べたこと

※図式化と言っても、ノードツリーを描画したりとか大層なことはしてません。「入れ子の枠」で表現するだけです。

Pythonで手軽にコンテナーを図式化するにはどうすれば？と思ったのが発端で、そこから、Jupyter notebookを使って実現したものと、それを実現する過程で間接的に便利だと思った点をあわせてメモしたものです。



## はじめに

Pythonに限ったことではないですが、コンテナーが多重に入れ子になっているデータを（特にプログラミング初心者向けに）説明する際に、視覚的に分かりやすい図か何か手軽に用意できないか、と思ったのがこれを作ったきっかけです。

それも、いくつかのサンプルを画像として用意しておくだけでは不満なので、コードをそのまま図式化できないかを考えてみました。


最初はコンテナーをトラバースして画像を作るとか？考えましたが、入れ子を表現する部分の計算って大変そう...

でも良く考えたら、入れ子を表現するものと言えば、身近にありました。
そう、HTMLで書いてWebブラウザーで表示するのが簡単ですね。

ちょうどJupyterを触り始めたところだったので、Jupyter上で表示できればより良し。
で、あとは試行錯誤をするために変換処理はモジュールにして...

というふうに色々と考えた結果、「HTMLで入れ子の枠として表示させる」になりました。


参考にしたサイトのリンクは、直接文中に貼っています。


## 開発環境

- Python 3.5.1 | Anaconda 4.0.0 (64-bit)
- Windows 7 64bit

Windows7+Anaconda(Python3)で開発していますが、
Python 3.5.1とJupyter notebookが使えれば何でも良いです。


## 実現方式

基本的には、コンテナーをトラバースして、HTMLを作るだけです。
Pythonで分からないこともまだ多くあったので、試行錯誤しながら作ることになりました。
その過程で知ったことも合わせていくつかメモしておきます。


### JupyterでHTMLをそのまま出力する

IPythonの`display`モジュールを使うと、Jupyterの結果に直接HTMLを出力できます。

Module: display — IPython 4.2.0 documentation
[http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html](http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html)

- 例

```python
from IPython.display import display, HTML

display(HTML('<p>Hello.</p>'))
```


### コンテナー等のオブジェクトをHTMLに変換する

コンテナーをトラバースして、HTMLに変換します。
再帰的に変換するので、結果として入れ子になります。

HTMLで出力しただけでは見た目が分かりにくいので、線の種類や色で型の種類をいくつか区別できるようにしています。
なのでタグにスタイル（クラス）を付けておきます。

これらのコンテナーをHTMLに変換する処理を、`container2html`という名前でモジュールとして作成します。

スタイル定義を適用するために、`<style>`タグをHTMLとしてあらかじめ出力しておきます。


### 一時的にローカル環境のモジュールを使う

一時的にローカル環境で書いたモジュールを使うには、モジュールパスにディレクトリーを追加します。
プロジェクトのディレクトリーをモジュールパスに追加すれば、一般のモジュールと同様にインポートできるようになります。

例えば、今、モジュールをWindows上のPyCharmで書いていて、
プロジェクトディレクトリー`C:\Users\argius\PyCharm\MyModule1`があって、
そして、その下に`container2html.py`というファイルを作り、それをモジュールにしたいとします。

その場合、下記のようにすることで、モジュールを`c2h`という名前で使用することができるようになります。

```python
import sys
sys.path.append(r'C:\Users\argius\PyCharm\MyModule1')

import container2html as c2h
```


### モジュールの再読み込み

モジュールは一度読み込まれたら、再読み込みするまでコードの更新は反映されません。
今回のような場合は試行錯誤するので、モジュールを修正するたびに更新を反映させたいのですが...

そういう場合、つまり修正したモジュールの再評価を行うには、`importlib`モジュールの`reload`関数を使います。

```python
import importlib
importlib.reload(c2h)
```

2016-07-21: 古い`imp`モジュールを使っていたので、`importlib`に修正しました。


### 型の親子関係を調べる

未知の型にもある程度対応できるように、基本の型などの親子関係を調べたかったのですが、最初はその方法が分かりませんでした。

下記のサイトにより、`mro`メソッドを使うとそれを調べることができることが分かりました。
オブジェクト`o`に対して`type(o).mro()`とすれば、祖先をたどることができます。
`list.mro()`のように型名を直接指定することもできます。

Python Tips：クラスの継承関係をチェックしたい - Life with Python
[http://www.lifewithpython.com/2014/08/python-check-class-inheritance-relationship.html](http://www.lifewithpython.com/2014/08/python-check-class-inheritance-relationship.html)


例えば、`collections`モジュールの`Counter`は、辞書に似ていますが、辞書と同様に解釈できるかどうかを知るために、下記のように確認しました。

```python
>>> from collections import Counter
>>> Counter.mro()
[<class 'collections.Counter'>, <class 'dict'>, <class 'object'>]
```


## 実装

### モジュール `container2html.py`

```python
"""Container to HTML"""


def convert(o, **kwargs):
    label = kwargs.get('label', None)
    labelhtml = '' if label is None else '<p class="label">%s</p>' % label
    return '<div class="c2h">%s %s</div>' % (labelhtml, HtmlConverter(kwargs).object_to_html(o))


def escape(o):
    return str(o).replace('&', '&amp;').replace('"', '&quot;').replace('<', '&lt;').replace('>', '&gt;')


class HtmlConverter:
    def __init__(self, kwargs):
        self.options = kwargs
        self.showtypeinfo = self.options.get('showtypeinfo', False)

    def object_to_html(self, o):
        def f():
            if isinstance(o, dict):
                return self.dict_to_html(o, 'dict')
            t = type(o)
            if t is tuple:
                return self.seq_to_html(o, 'tuple')
            if t is str:
                return '<span class="value">%s</span>' % repr(o)
            if t is list or hasattr(o, '__iter__'):
                return self.seq_to_html(o, 'list')
            return '<span class="value">%s</span>' % str(o)

        if self.showtypeinfo:
            return '<div class="typeinfo"><div>[%s]</div>%s</div>' % (escape(type(o).__name__), f())
        return f()

    o2html = object_to_html

    def seq_to_html(self, seq, style_name=None):
        a = list(seq)
        if len(a) == 0:
            return '<div style="nil">%s(empty)</div>' % ''
        items = ['<li>%s</li>' % self.o2html(x) for x in a]
        return '<ul class="%s">%s</ul>' % (style_name, ''.join(items))

    def dict_to_html(self, d, style_name=None):
        if len(d) == 0:
            return '<ul class="%s"><li>(empty)</li></ul>' % style_name
        items = ['<ul><li>%s</li><li>%s</li></ul>' % (self.o2html(k), self.o2html(v)) for k, v in d.items()]
        return '<ul class="%s"><li>%s</li></ul>' % (style_name, ''.join(items))
```

### スタイル

```css
div.c2h {
  font-family: consolas, monospace;
  border: 1px solid black;
  margin: 0px 1px 3px 0px;
  padding: 3px 1em;
}
div.c2h .label {
  font-size: 100%;
  color: blue;
  text-decoration: underline;
  padding-bottom: 1ex;
}
div.c2h span.value {
  background-color: #fff;
  padding: 1px 4px;
}
div.c2h div.typeinfo {
  border: 1px dashed grey;
}
div.c2h ul {
  border: 4px solid black;
  display: table;
  margin: 1ex;
  padding-left: 0;
  list-style: none;
  text-align: center;
}
div.c2h ul li {
  display:table-cell;
  vertical-align: middle;
  text-align:center;
  padding: 1ex;
  border-style: solid;
  border-right-width: 4px;
  border-top-width: 0px;
  border-bottom-width: 0px;
}
div.c2h ul.list {
  border-color: #cc3311;
}
div.c2h ul.list li {
  border-color: #cc3311;
}
div.c2h ul.tuple {
  border-color: #33bb33;
  border-radius:10px;
}
div.c2h ul.tuple > li {
  border-color: #33bb33;
}
div.c2h ul.dict {
  border-color: #3333cc;
}
div.c2h ul.dict > li {
  border-color: #3333cc;
}
div.c2h ul.dict > li ul {
  border-color: #3333cc;
  border-style: inset;
  border-radius:10px;
}
div.c2h ul.dict > li ul li {
  border-color: #3333ff;
  border-width: 1px;
}
```


## 実行結果

事前準備をします。

```python
from IPython.display import display, HTML

import sys
sys.path.append(r'C:\Users\argius\PyCharm\MyModule1')

import container2html as c2h

display(HTML('''
<style>
（省略）
</style>
'''))
```

これで、`c2h.convert`関数の結果を`display(HTML(...))`すれば表示できるようになりましたが、
毎回これを書くのは面倒なので、
コード（式）の文字列を`eval`してHTML変換したものを`display`で出力する関数`v`をショートカットとして定義します。

```python
def v(code, **kwargs):
    display(HTML(c2h.convert(eval(code), label=code, **kwargs)))
```

あとは実行するだけです。


### 実行コード

オプションで`showtypeinfo=True`を付けると、型の名前も合わせて表示されます。

```python
v('[1, 2, (3, 4, 5)]')
v('[1, 2, (3, 4, 5)]', showtypeinfo=True)
v('zip([1, 2, 3, 4], [1, 2, 3, 4], [5, 6, 7, 8])', showtypeinfo=True)
v('dict(one=1, two=2)')
v('range(5)')

from collections import Counter
v("Counter(['a', 'a', 'b', 'c'])", showtypeinfo=True)

import numpy as np
v('np.arange(9).reshape(3, 3)', showtypeinfo=True)
```


### 結果

`dict`は青枠、`tuple`は緑枠、それ以外の`list`などの`iterable`は赤枠で表示しています。

![_qiita.png](https://qiita-image-store.s3.amazonaws.com/0/99734/01561862-ce79-9ba6-e505-618662a29849.png)

## おわりに

実際これを活用するかどうかは分かりませんが、作る過程で調べたことの方が有意義だった気がします。
実は今回は、PythonよりもCSSのほうで苦労しました。

何よりも、こういう風に試行錯誤して何かを作るって楽しいですね！