※図式化と言っても、ノードツリーを描画したりとか大層なことはしてません。「入れ子の枠」で表現するだけです。
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
- 例
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
という名前で使用することができるようになります。
import sys
sys.path.append(r'C:\Users\argius\PyCharm\MyModule1')
import container2html as c2h
モジュールの再読み込み
モジュールは一度読み込まれたら、再読み込みするまでコードの更新は反映されません。
今回のような場合は試行錯誤するので、モジュールを修正するたびに更新を反映させたいのですが...
そういう場合、つまり修正したモジュールの再評価を行うには、importlib
モジュールのreload
関数を使います。
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
例えば、collections
モジュールのCounter
は、辞書に似ていますが、辞書と同様に解釈できるかどうかを知るために、下記のように確認しました。
>>> from collections import Counter
>>> Counter.mro()
[<class 'collections.Counter'>, <class 'dict'>, <class 'object'>]
実装
モジュール container2html.py
"""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('&', '&').replace('"', '"').replace('<', '<').replace('>', '>')
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))
スタイル
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;
}
実行結果
事前準備をします。
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
をショートカットとして定義します。
def v(code, **kwargs):
display(HTML(c2h.convert(eval(code), label=code, **kwargs)))
あとは実行するだけです。
実行コード
オプションでshowtypeinfo=True
を付けると、型の名前も合わせて表示されます。
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
は赤枠で表示しています。
おわりに
実際これを活用するかどうかは分かりませんが、作る過程で調べたことの方が有意義だった気がします。
実は今回は、PythonよりもCSSのほうで苦労しました。
何よりも、こういう風に試行錯誤して何かを作るって楽しいですね!