Pythonメモ torinaブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Python、デコレータやメタクラスで自動登録

プログラミング関連 inspectモジュール importlibモジュール メタクラス 約16日前
2017年4月9日12:14
関数やクラスを定義し、それを自動で何処かに登録しておきたい、というケースがあります。

これは私が作っているエディタなのですが、下側は現在cmd.exeとなっており、DOSコマンドの入力ができます。



DOSコマンドは少々貧弱です。そこで、Linuxコマンドと互換性のあるコマンドや、独自のコマンドも使えるようにしています。



独自コマンドは、このような単純な関数として定義するだけです。そうすることで、関数名がそのままコマンドとして利用できます。
また、user_commands.pyというファイルに、ユーザーが自由に関数を追加できるようにもしています。



更に、定義した関数はエディタの右側で確認ができます。docstringも一緒に確認ができます。



このような処理はどう実装できるでしょうか。
シンプルな例ならば、あるモジュールにてユーザーに関数を定義してもらい、そのモジュール内の関数名、関数オブジェクト、docstringを辞書なんかに保存しておきます。
文字列が入力されたら、その文字列(関数名)をキーに辞書から値を取得する、という流れです。
実際に、それっぽい動作をするものを作成しましょう。

ユーザーが自由に追加できるファイルを、user_functions.pyとします。
実際にいくつか関数を定義してもらいました。
def display(string):
    """文字列を出力する関数

    引数:
        string: 出力する文字列
    """

    print(string)


def show(string):
    """displayとおなじ同じ"""

    print(string)


def print2(string):
    """print(string)を行う"""

    print(string)


def test(string):
    print(string)



Pythonで、シンプルなドキュメント生成プログラム
https://torina.top/detail/353/
で、inspectを使った関数の一覧取得や、関数のdocstring取得について書きました。

inspectを使ってモジュールから関数のリスト(関数名, 関数オブジェクト)を取得し、そこから関数のdocstringを取得し、最後に辞書に格納します。
]
import inspect
import user_functions

docstrings = {}

all_functions = inspect.getmembers(user_functions, inspect.isfunction)
for func_name, func_object in all_functions:
    func_doc = inspect.getdoc(func_object) or ''
    docstrings[func_name] = func_doc

print(docstrings)


実行すると、以下のように出力がされます。
user_functions.pyの関数が、きちんと取れてますね。
{'display': '文字列を出力する関数\n\n引数:\n string: 出力する文字列',
'print2': 'print(string)を行う',
'show': 'displayとおなじ同じ',
'test': ''}



しかし、この方法には問題があります。
それはuser_functions.pyに書いた関数が全て拾われるため、うかつにヘルパー関数も書けないことです。
処理を助けるための関数が20あり、実際に登録したい関数が5つしかなくても、登録されるのは25個です。これではいけません。
__regist_func__のような変数に登録したい関数を入れていく、という方法も思いつきましたが、これも煩雑になり管理が大変そうです。

恐らく一番楽なのは、以下のようなデコレータを利用することです。
一番下のtest関数にだけ、あえてデコレータをつけてないことを覚えておいてください。
from library import register


@register
def display(string):
    """文字列を出力する関数

    引数:
        string: 出力する文字列
    """

    print(string)


@register
def show(string):
    """displayとおなじ同じ"""

    print(string)


@register
def print2(string):
    """print(string)を行う"""

    print(string)


def test(string):
    print(string)



仕組みは非常に簡単です。関数を受け取るregister関数を作成し、docstrings辞書に関数名と関数docを格納し、関数をそのまま返します。
library.py
import inspect

docstrings = {}

def register(func):
    func_name = func.__name__
    func_doc = inspect.getdoc(func) or ''
    docstrings[func_name] = func_doc
    return func



これがmain.pyです。docstringsは、結果が格納されている辞書です。
import user_functionsで、一回対象のモジュールをロードしておく必要があります。
今回はuser_functionsという固定のモジュール名ですが、これが動的に変わる名前ならば、importlib.import_moduleなんかを使って、文字列でモジュールのインポートを行うとよいでしょう、
from pprint import pprint
from library import docstrings
import user_functions

pprint(docstrings)



結果も大丈夫ですね・・・!
{'display': '文字列を出力する関数\n\n引数:\n string: 出力する文字列',
'print2': 'print(string)を行う',
'show': 'displayとおなじ同じ'}



今回は単純な関数を定義し、グローバル変数のdocstringsという辞書に格納しました。
もう少し複雑な処理が必要であれば、クラスを使うのも手です。
Djangoのフィルターやタグの追加の処理を真似してみます。
user_functions.py
from library import Library
register = Library()  # 必須となる記述


@register.function  # このようにデコレータをつける
def display(string):
    """文字列を出力する関数

    引数:
        string: 出力する文字列
    """

    print(string)


@register.function
def show(string):
    """displayとおなじ同じ"""

    print(string)


@register.function
def print2(string):
    """print(string)を行う"""

    print(string)


def test(string):
    print(string)



この記述は必須となります。
これにより、登録用のモジュールそれぞれにregisterという名前でLibraryクラスのインスタンスを持つことができ、そのモジュール内の関数名・関数docなんかをregisterへ格納していく訳です。
そして、各モジュールのregister変数(Libraryオブジェクト)をあとで束ねていきます。
register = Library()



library.py
bundle_libraryは、各モジュールに作ったregister変数を束ね、最終的な結果を返します。
from importlib import import_module
import inspect


class Library:

    def __init__(self):
        self.docs = {}

    def function(self, func):
        func_name = func.__name__
        func_doc = inspect.getdoc(func) or ''
        self.docs[func_name] = func_doc
        return func


def bundle_library(module_names):
    docs = {}
    for name in module_names:
        module = import_module(name)
        module_func_docs = module.register.docs
        docs.update(module_func_docs)
    return docs



そしてmain.pyです。bundle_libraryにモジュール名を渡すだけになります。
from pprint import pprint
from library import bundle_library

user_files = ['user_functions']
docstrings = bundle_library(user_files)
pprint(docstrings)


関数については登録できるようになりました。
次はクラスに同様のことをしてみましょう。


まず、user_functions.pyにクラスを追加します。
以下のように、登録するクラスにはRegisterというクラスを継承させます。
from library import register, Register


class ClassA(Register):
    """ClassAです"""
    pass


class ClassB(Register):
    """ClassBです

    ほにゃらら
    ららら
    """
    pass


@register
def display(string):
    """文字列を出力する関数

    引数:
        string: 出力する文字列
    """

    print(string)


@register
def show(string):
    """displayとおなじ同じ"""

    print(string)


@register
def print2(string):
    """print(string)を行う"""

    print(string)


def test(string):
    print(string)



library.py
シンプルな最初のバージョンのlibrary.pyです。
メタクラスを利用します。パッと見で何となくわかると思いますが、__new__内でクラスを作り、return する前に名前とdocをdocstrings辞書に格納します。
import inspect

docstrings = {}


def register(func):
    func_name = func.__name__
    func_doc = inspect.getdoc(func) or ''
    docstrings[func_name] = func_doc
    return func


class Meta(type):
    def __new__(meta, name, bases, class_dict):
        cls = type.__new__(meta, name, bases, class_dict)
        docstrings[name] = inspect.getdoc(cls) or ''
        return cls


class Register(metaclass=Meta):
    pass


docstrings.pop('Register')



作成させるクラスに、metaclass=Metaと書かせるのはちょっと面倒そうです。なので、こちら側で作成しておき、ユーザーは継承するだけで済むようにさせます。
Registerクラスも当然docstirngsに追加されるので、popで削除しときます。
class Register(metaclass=Meta):
    pass


docstrings.pop('Register')



main.pyは、以前のシンプルなバージョンのままです。
from pprint import pprint
from library import docstrings
import user_functions

pprint(docstrings)



出力は大丈夫そうですね!
{'ClassA': 'ClassAです',
'ClassB': 'ClassBです\n\nほにゃらら\nららら',
'display': '文字列を出力する関数\n\n引数:\n string: 出力する文字列',
'print2': 'print(string)を行う',
'show': 'displayとおなじ同じ'}



python3.6からは、__init_subclass__を使うことでもっと楽にかけます。
import inspect

docstrings = {}


def register(func):
    func_name = func.__name__
    func_doc = inspect.getdoc(func) or ''
    docstrings[func_name] = func_doc
    return func


class Register:
    def __init_subclass__(cls, **kwargs):
        docstrings[cls.__name__] = inspect.getdoc(cls) or ''



実行すると、同じ出力が得られました!
{'ClassA': 'ClassAです',
'ClassB': 'ClassBです\n\nほにゃらら\nららら',
'display': '文字列を出力する関数\n\n引数:\n string: 出力する文字列',
'print2': 'print(string)を行う',
'show': 'displayとおなじ同じ'}