srcgenが何かに使えそうだと思っているのだけれど。どうしたら便利なのかという答えは出ていない
srcgenが何かに使えそうだと思っているのだけれど。どうしたら便利なのかという答えは出ていない
srcgenというパッケージがあって。これが個人的には結構面白い機能を持った ライブラリだと思っているのだけれど。良い使い方というのが見えていない。
srcgenとは
srcgen はコード生成のためのフレー ムワークという括りに入るものらしい。
jinja2やmakoなどの一般的なテキスト型のテンプレートエンジンとは種類が異 なり。どちらかと言うとhtmlのdomを手書きするような使用感に近いんだろう か。コンテキストマネージャーを特徴的に使うことによりpython上から異なる 言語のコードを生成するためのコードを出力する事ができる。
出力先の言語として以下の言語に対応している。
- c
- js
- html
- 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