srcgenが何かに使えそうだと思っているのだけれど。どうしたら便利なのかという答えは出ていない

srcgenが何かに使えそうだと思っているのだけれど。どうしたら便利なのかという答えは出ていない

srcgenというパッケージがあって。これが個人的には結構面白い機能を持った ライブラリだと思っているのだけれど。良い使い方というのが見えていない。

srcgenとは

srcgen はコード生成のためのフレー ムワークという括りに入るものらしい。

jinja2やmakoなどの一般的なテキスト型のテンプレートエンジンとは種類が異 なり。どちらかと言うとhtmlのdomを手書きするような使用感に近いんだろう か。コンテキストマネージャーを特徴的に使うことによりpython上から異なる 言語のコードを生成するためのコードを出力する事ができる。

出力先の言語として以下の言語に対応している。

今回はpythonを使ってみる。

使い方

以下の様にして使う。

from srcgen.python import PythonModule
m = PythonModule()

with m.def_("sum", "xs"):
    m.stmt("n = 0")
    with m.for_("x", "xs"):
        m.stmt("n += x")
    m.return_("n")

print(m)

これは以下のようなコードが生成される。

def sum(xs):
    n = 0
    for x in xs:
        n += x
    return n

どのような場合に使えるだろうか?一部のコードの最適化のために使えるよう な気がしている。すごく単純にしてしまったが、sum1からsum4までの関数が並 んでいる。汎用的なコードを書いた際のパフォーマンス際にループなどの利用 がパフォーマンス上のボトルネックになってしまうような時などがあるかもし れない*1。 そのような場合に似たような挙動をするを一度に生成するというようなイメー ジ。

for i in range(1, 5):
    with m.def_("sum{}".format(i), ", ".join("x{}".format(j) for j in range(i))):
        m.return_(" + ".join("x{}".format(j) for j in range(i)))

print(m)

"""
def sum1(x0):
    return x0

def sum2(x0, x1):
    return x0 + x1

def sum3(x0, x1, x2):
    return x0 + x1 + x2

def sum4(x0, x1, x2, x3):
    return x0 + x1 + x2 + x3
"""

srcgenの仕組み(contextmanager)

srcgenの肝となっているcontext mangerを使った部分の定義は以下の様になっている。

class PythonModule(BaseModule):

# .. skip

    @contextmanager
    def suite(self, headline, *args):
        self.stmt(headline, *args)
        prev = self._curr
        self._curr = []
        prev.append(self._curr)
        yield
        self._curr = prev

    def for_(self, var, expr):
    return self.suite("for %s in %s:" % (var, expr))

# .. def,if,while,try,exceptなどの定義が続く

基本的にはstackを使った木の走査のコードと同様。scopeとなる空リストを、 withが付加された段階でpushする(これによって以降の行はインデントされる ようになる)。yieldで内部のblockを実行した後、context managerの __exit__ によってpopされる。

forの使い方

以下のようなコードはどのようなコードが生成されるか。

with m.def_("foo", "xs0", "xs1", "xs2"):
    m.stmt("r = []")
    for i in range(3):
        with m.for_("x{}".format(i), "xs{}".format(i)):
            m.stmt("r.append(x{})".format(i))

print(m)

以下の様なコードが出力される。

def foo(xs0, xs1, xs2):
    r = []
    for x0 in xs0:
        r.append(x0)
    for x1 in xs1:
        r.append(x1)
    for x2 in xs2:
        r.append(x2)

ネストの浅いループが3回連なるというような形になる。この3つの変数につい てネストの深いループを書くことはできないだろうか?具体的には与えられた 引数のリストらの直積を返すような関数 cross3 を定義したい。

もちろん、手で直接定義することは可能。

with m.def_("cross3", "xs0", "xs1", "xs2"):
    m.stmt("r = []")
    with m.for_("x0", "xs0"):
        with m.for_("x1", "xs1"):
            with m.for_("x2", "xs2"):
                m.stmt("r.append((x0, x1, x2))")
    m.return_("r")

以下の様な出力結果になる。

def cross3(xs0, xs1, xs2):
    r = []
    for x0 in xs0:
        for x1 in xs1:
            for x2 in xs2:
                r.append((x0, x1, x2))
    return r

これを以前にsum0からsum4から定義した時と同様にパラメータを指定して一気 に生成する事はできないだろうか?ループによって生成することは不可能だと いうことはわかっている。肝はwithのコンテキストが終了したタイミングで内 部のstackがpopされるということ。したがってコンテキストが終了しない内に 次のループの記述に移行すれば良い。再帰を使えばそれが書ける。

for n in range(1, 5):
    def rec(i):
        if i >= n:
            m.stmt("r.append(({}))".format(", ".join("x{}".format(j) for j in range(i))))
        else:
            with m.for_("x{}".format(i), "xs{}".format(i)):
                rec(i + 1)
    with m.def_("cross{}".format(n), *["xs{}".format(i) for i in range(n)]):
        m.stmt("r = []")
        rec(0)
        m.return_("r")

以下の様な出力になる。

def cross1(xs0):
    r = []
    for x0 in xs0:
        r.append((x0))
    return r

def cross2(xs0, xs1):
    r = []
    for x0 in xs0:
        for x1 in xs1:
            r.append((x0, x1))
    return r

def cross3(xs0, xs1, xs2):
    r = []
    for x0 in xs0:
        for x1 in xs1:
            for x2 in xs2:
                r.append((x0, x1, x2))
    return r

def cross4(xs0, xs1, xs2, xs3):
    r = []
    for x0 in xs0:
        for x1 in xs1:
            for x2 in xs2:
                for x3 in xs3:
                    r.append((x0, x1, x2, x3))
    return r

execを使って実際に実行できるコードを生成する。

namedtupleに近いがmutableなクラスを定義するような関数namedobjectを定義してみよう。 関数によって生成された文字列をexecして利用可能な関数を取り出すデコレータも一緒に定義しておく。

def as_python_code(fn):
    def wrapper(name, *args, **kwargs):
        m = PythonModule()
        fn(m, name, *args, **kwargs)
        logger.debug(m)
        # activate python code
        env = {}
        exec(str(m), env)
        return env[name]
    return wrapper


@as_python_code
def namedobject(m, name, fields):
    args = fields.replace(",", " ").split(" ")
    with m.class_(name):
        with m.def_("__init__", "self", *args):
            for x in args:
                m.stmt("self.{x} = {x}".format(x=x))

        m.stmt("__slot__ = {!r}".format(args))

        with m.def_("__repr__", "self"):
            m.return_("self.__class__.__name__ + '({})'.format(self=self)".format(
                ", ".join("{x}={{self.{x}}}".format(x=x) for x in args)
            ))

個人的には直接文字列テンプレートを使ってコードを生成している collections.namedtupleの方式寄りは綺麗で内容を把握しやすいのではないか と思っている。

ここで生成したクラスは以下の様にして使う。

Pair = namedobject("Pair", "x y")

print(Pair.__slot__)  # => ["x", "y"]
print(Pair(2, 3))  # => Pair(x=2, y=3)
print(Pair(2, 3) == Pair(2, 3))  # => False

以下の様にすると、値が同じなら同一となるようなvalueobject?というクラス生成関数も作る事ができる。

@as_python_code
def valueobject(m, name, fields):
    args = fields.replace(",", " ").split(" ")
    with m.class_(name):
        with m.def_("__init__", "self", *args):
            for x in args:
                m.stmt("self.{x} = {x}".format(x=x))

        m.stmt("__slot__ = {!r}".format(args))

        with m.def_("__repr__", "self"):
            m.return_("self.__class__.__name__ + '({})'.format(self=self)".format(
                ", ".join("{x}={{self.{x}}}".format(x=x) for x in args)
            ))

        with m.def_("__hash__", "self"):
            m.return_("hash(repr(self))")

        with m.def_("__eq__", "self", "other"):
            with m.if_("not isinstance(other, self.__class__)"):
                m.return_("False")
            m.return_("repr(self) == repr(other)")

        with m.def_("__ne__", "self", "other"):
            with m.if_("not isinstance(other, self.__class__)"):
                m.return_("True")
            m.return_("repr(self) != repr(other)")

以下の様にして使う。

P = valueobject("P", "x y")

print(P.__slot__)  # => ["x", "y"]
print(P(2, 3))  # => P(x=2, y=3)
print(P(2, 3) == P(2, 3))  # => True

*1:本当にそのような状況があるのか疑問ではあるけれど。大抵の場合ボ トルネックは他の箇所に出てきそうではあるし。このような箇所がボトルネッ クになる分野においてpythonを直接使うというのは間違った選択なような気が している。出力先を他の言語というケースにマッピングしてもらえると幸い