LoginSignup
17
17

More than 1 year has passed since last update.

Pythonのオブジェクトからplantumlのオブジェクト図を生成出力する

Last updated at Posted at 2020-12-16

オブジェクト図

Pythonでは、値、変数、関数、クラス、メソッド、モジュールなど、すべてがオブジェクトで、メモリ上に存在するものです。
メモリ上にどんなオブジェクトが存在しているのか把握することで、Pythonの動作を理解したり、実行状態を把握することができます。
実際、他の方のPython記事に対して、変数辞書からのオブジェクト図を示してコメントすることがあります。

そこで、指定したオブジェクトからplantumlのオブジェクト図(テキスト形式)を生成出力するプログラムを作ってみました。
なお、plantumlから画像に変換するには別の作業が必要です。Qiitaであれば、出力されたマークダウンテキストを投稿欄やコメント欄にコピー&ペーストすれば図に変換して表示してくれます。

  • コメントの例: https://qiita.com/risuoku/items/c71e4fdb7ea9d9800e20#comment-7b342ec242f0debc8e41

  • 自分で書いたオブジェクト図の例
    image.png

  • 指定したオブジェクトからplantumlを生成出力して作成した図の例(上の図を90度回転したような配置):
    image.png

    色の説明:

    • ピンク: 出力指定したオブジェクト
    • 紫: 関数
    • 緑: クラス
    • 青: インスタンス
    • オレンジ: 変数辞書

プログラム

plantumlオブジェクト図生成出力スクリプト

objectuml.py
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が生成出力されます。

plantuml生成出力コマンド実行
$ python3 objectuml sample

サンプルコード

sample.py
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モジュールのオブジェクト図

image.png

ユーザプログラムから呼び出す

利用者のプログラムの中から本ライブラリを呼び出してplantumlを出力することもできます。

import objectuml

objectuml.plantuml(オブジェクト図出力対象変数名)

オセロゲームのオブジェクト図

以下のソースコードのmain関数に本ライブラリを呼び出す処理を追加して実行してみた例を示します。

参考資料

17
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
17