オブジェクト図
Pythonでは、値、変数、関数、クラス、メソッド、モジュールなど、すべてがオブジェクトで、メモリ上に存在するものです。
メモリ上にどんなオブジェクトが存在しているのか把握することで、Pythonの動作を理解したり、実行状態を把握することができます。
実際、他の方のPython記事に対して、変数辞書からのオブジェクト図を示してコメントすることがあります。
そこで、指定したオブジェクトからplantumlのオブジェクト図(テキスト形式)を生成出力するプログラムを作ってみました。
なお、plantumlから画像に変換するには別の作業が必要です。Qiitaであれば、出力されたマークダウンテキストを投稿欄やコメント欄にコピー&ペーストすれば図に変換して表示してくれます。
-
コメントの例: https://qiita.com/risuoku/items/c71e4fdb7ea9d9800e20#comment-7b342ec242f0debc8e41
-
指定したオブジェクトからplantumlを生成出力して作成した図の例(上の図を90度回転したような配置):
色の説明:
- ピンク: 出力指定したオブジェクト
- 紫: 関数
- 緑: クラス
- 青: インスタンス
- オレンジ: 変数辞書
プログラム
plantumlオブジェクト図生成出力スクリプト
import sys
import argparse
import importlib
from abc import ABCMeta
_making = []
_exists = {}
def exists(obj):
if id(obj) in _exists:
return True
_exists[id(obj)] = obj # Keep obj to prevent reassignment of the same id
return False
def empty_function():
pass
class EmptyClass:
pass
FUNCTION = type(empty_function)
LINK_TYPES = {
'int', 'float', 'complex', 'function',
'type', 'list', 'tuple', 'dict', 'set', 'mappingproxy', 'ABCMeta',
}
NOLINK_TYPES = {
'object', 'NoneType', 'bool', 'module', 'property', 'abstractproperty',
'builtin_function_or_method', 'method', 'staticmethod', 'classmethod',
'abstractmethod', 'abstractstaticmethod', 'abstractclassmethod',
'member_descriptor', 'method_descriptor', 'getset_descriptor',
'wrapper_descriptor', 'weakref', 'WeakSet',
}
MINIMIZE_TYPES = {
'int', 'float', 'complex', 'str', 'bytes', 'function', 'module'
}
BUILTIN_TYPES = {
*LINK_TYPES,
*NOLINK_TYPES,
*{name for name, obj in vars(__builtins__).items() if type(obj) == type}
}
BUILTIN_OBJECTS = (empty_function, EmptyClass, ABCMeta('', (), {}))
BUILTIN_MEMBERS = {name
for names in map(dir, BUILTIN_OBJECTS)
for name in names
if name not in ('__new__', '__init__', '__del__')}
BUILTIN_MEMBERS.update(globals())
COLOR_START = '#ffc6ff' # pink
COLOR_VARS = '#ffe2c6' # orange
COLOR_FUNCTION = '#c6c6ff' # purple
COLOR_CLASS = '#c6ffc6' # green
COLOR_OBJECT = '#c6ffff' # blue
def link(base, from_, arrow, to_):
if to_ is None or to_ == {}:
return f'{from_} => {repr(to_)}'
if options.minimize and type(to_).__name__ in MINIMIZE_TYPES:
return f'{from_} => <{type(to_).__name__}> {repr(to_)}'
if type(to_).__name__ in NOLINK_TYPES:
return f'{from_} => {repr(to_)}'
try:
if to_.__name__ in NOLINK_TYPES:
return f'{from_} => {to_.__name__}'
except:
pass
dump(object_diagram(to_, color=COLOR_FUNCTION if isinstance(to_, FUNCTION) else COLOR_OBJECT))
if to_ in _making:
return f'{from_} => cycle link to {id(to_)}'
# return f"{arrow} {id(to_)} /' {type(to_)}:{repr(to_)} '/" # for DEBUG
return f'{from_} {arrow} {id(to_)}'
def number_vars(obj):
yield f'real => {obj.real}'
if isinstance(obj, complex):
yield f'imag => {obj.imag}'
def repr_vars(obj):
yield f'~__repr__() => {repr(obj)}'
def dict_vars(obj):
for name, member in obj.items():
if name in BUILTIN_MEMBERS:
continue
yield link(id(obj), f'[{repr(name)}]', '*-->', member)
def set_vars(obj):
for i, item in enumerate(obj, 1):
yield link(id(obj), f'~#{i}', '*-->', item)
def enumerate_vars(obj):
for i, item in enumerate(obj):
yield link(id(obj), f'[{i}]', '*-->', item)
def name_vars(obj):
yield f'~__name__ => {repr(obj.__name__)}'
def object_vars(obj):
try:
variables = vars(obj)
except:
return
if variables:
dump(object_diagram(variables, color=COLOR_VARS))
yield f'~__dict__ *--> {id(variables)}'
else:
yield '~__dict__ => {}'
OBJECT_VARS = {
'int': number_vars,
'float': number_vars,
'complex': number_vars,
'str': repr_vars,
'bytes': repr_vars,
'list': enumerate_vars,
'tuple': enumerate_vars,
'dict': dict_vars,
'mappingproxy': dict_vars,
'set': set_vars,
'function': name_vars,
'module': name_vars,
}
def class_diagram(cls, color=COLOR_CLASS):
if exists(cls):
return
name = f'{id(cls)}'
base = cls.__base__
_making.append(cls)
yield ''
yield f'map {name} {color} {{'
yield f'~__class__ => {type(cls).__name__}'
yield f'~__name__ => {repr(cls.__name__)}'
if base.__name__ in BUILTIN_TYPES:
yield f'~__base__ => {base.__name__}'
else:
dump(class_diagram(base))
yield link(id(cls), f'~__base__', '*->', base)
yield from object_vars(cls)
yield f'}}'
_making.remove(cls)
def object_diagram(obj, color=COLOR_OBJECT):
if isinstance(obj, type):
yield from class_diagram(obj)
return
if exists(obj):
return
name = f'{id(obj)}'
cls = type(obj)
yield ''
_making.append(obj)
yield f'map {name} {color} {{'
if cls.__name__ in BUILTIN_TYPES:
yield f'~__class__ => {type(obj).__name__}'
else:
yield f'~__class__ *-> {id(type(obj))}'
if hasattr(obj, '__len__'):
yield f'~__len__() => {len(obj)}'
if cls.__name__ in OBJECT_VARS:
yield from OBJECT_VARS[cls.__name__](obj)
if cls.__name__ not in BUILTIN_TYPES:
yield from object_vars(obj)
yield f'}}'
if cls.__name__ not in BUILTIN_TYPES:
dump(class_diagram(cls))
_making.remove(obj)
def dump(lines):
print(*lines, sep='\n')
def plantuml(obj):
print('```plantuml')
print('@startuml')
dump(object_diagram(obj, color=COLOR_START))
print()
print('@enduml')
print('```')
_making.clear()
_exists.clear()
def parse_args():
parser = argparse.ArgumentParser(description='create object diagrams in plantuml')
parser.add_argument('-m', '--minimize', action='store_true',
help='minimize links')
parser.add_argument('module_name', metavar='MODULE_NAME', type=str,
help='a module name')
return parser.parse_args()
if __name__ == '__main__':
options = parse_args()
sys.path.append('.')
plantuml(vars(importlib.import_module(options.module_name)))
工夫点
- ジェネレータ関数でplantuml要素を出力
- オブジェクトリンクがあれば、リンク先をジェネレートして先にprint出力
制限事項
オブジェクトの相互参照(循環参照)があるとplantumlから図を生成できません。
オブジェクトが相互参照(循環参照)しているところは、「cycle link to ~」という表示にして、リンク線を描かないようにしてあります。
使用方法
コマンド実行
最初に示したオブジェクト図のソースコード sample.py
を以下に示します。
sample.py
のモジュール名は .py
を取り除いた sample
ですので、次ようにコマンド実行することでplantumlが生成出力されます。
$ python3 objectuml sample
サンプルコード
class A:
prop1 = 123
prop2 = prop1
def hoge(self):
return 'superhoge'
fuga = hoge
class SubA(A):
prop1 = 777
def hoge(self):
return 'hoge'
a = SubA()
生成出力されたplantuml
```plantuml
@startuml
map 15703550080 #c6ffff {
~__class__ => int
real => 123
}
map 123145298897216 #c6c6ff {
~__class__ => function
~__name__ => 'hoge'
}
map 123145300468016 #ffe2c6 {
~__class__ => mappingproxy
~__len__() => 8
['prop1'] *--> 15703550080
['prop2'] *--> 15703550080
['hoge'] *--> 123145298897216
['fuga'] *--> 123145298897216
}
map 34361615648 #c6ffc6 {
~__class__ => 'type'
~__name__ => 'A'
~__base__ => object
~__dict__ *--> 123145300468016
}
map 123145298640944 #c6ffff {
~__class__ => int
real => 777
}
map 123145298897072 #c6c6ff {
~__class__ => function
~__name__ => 'hoge'
}
map 123145300508832 #ffe2c6 {
~__class__ => mappingproxy
~__len__() => 4
['prop1'] *--> 123145298640944
['hoge'] *--> 123145298897072
}
map 34361616592 #c6ffc6 {
~__class__ => 'type'
~__name__ => 'SubA'
~__base__ *-> 34361615648
~__dict__ *--> 123145300508832
}
map 123145298931824 #c6ffff {
~__class__ *-> 34361616592
~__dict__ => {}
}
map 123145298889664 #ffc6ff {
~__class__ => dict
~__len__() => 11
['A'] *--> 34361615648
['SubA'] *--> 34361616592
['a'] *--> 123145298931824
}
@enduml
`` `
標準モジュールのオブジェクト図
標準ライブラリのオブジェクト図を出力することも可能です。
構造が複雑なライブラリはリンクが正しく出力されないので、BUILTIN_TYPES定義の追加が必要そうですが。 出力されるようになったと思います。
$ python3 objectuml math
mathモジュールのオブジェクト図
ユーザプログラムから呼び出す
利用者のプログラムの中から本ライブラリを呼び出してplantumlを出力することもできます。
import objectuml
objectuml.plantuml(オブジェクト図出力対象変数名)
オセロゲームのオブジェクト図
以下のソースコードのmain関数に本ライブラリを呼び出す処理を追加して実行してみた例を示します。
- ソースコード: https://gist.github.com/shiracamus/492201c280cf937beb114224c871a5b5
- 生成出力されたplantumlから変換した画像:
参考資料
- https://plantuml.com/ja/
-
https://plantuml.com/ja/object-diagram
このサイトの各図にあるEdit online
ボタンを押して、上記の plantuml コードをコピー&ペーストしてSubmit
ボタンを押すと図が生成されます。