Python とマクロ、代数的データ型

前回はマクロの概要と Python でマクロを実装するための仕組みについて説明しました。

Python とマクロ、インポートフックと抽象構文木 - forest book

動作原理を理解した上で実際にマクロでどういったことができるのか、MacroPy というライブラリで提供されている機能をみながら考察してみます。

MacroPy の概要

Python でのマクロ実装の1つです。インポートフックでモジュール内のマクロ機能を AST 変換することで動作します。MacroPy で提供されているマクロ機能は以下のデコレーターを使って実装されています。

  • @macros.expr
  • @macros.block
  • @macros.decorator
  • @macros.expose_unhygienic

これらの仕組みを使って自分でマクロを実装することもできます。それにより Python の意味論 (semantics) の拡張を簡単にします。

リポジトリには python3 ブランチがあり、Python 3 対応が試みられているようですが、正常に動作しない機能もあるため、Python 2.7 を使う方が無難だと思います。

MacroPy が提供するマクロや機能は多岐に渡ります。またドキュメントもしっかりしているので学習にはとても良さそうです。ただ、全てを README に記載しているので目を通すだけでもなかなか大変です。以下は MacroPy の README から目次を抜き出したものです。

機能

マクロ

ユーザー定義マクロ

リファレンス

項目がたくさんあるので興味のあるところから読み進めると良いと思います。

今回は README の前半部によく登場する Case クラスという機能を提供するマクロとその背景について解説します。

Case クラス

Scala から Case クラスという機能を提供するマクロです。Case クラス について MacroPy のドキュメントで以下が引用されています。

Case Classes Are Cool - Code Commit

私が Scala をよく知らないため、この記事を読んでもよく分からなくて最初からつまづきました。Case クラスについて調べていると Effetive Scala に Case クラスについて説明があるのを見つけました。

代数的データ型としてのケースクラス

ケースクラス (case class) は、代数的データ型 (algebraic data type) をエンコードする: ケースクラスは数多くのデータ構造をモデリングするのに役に立ち、強力な不変式を簡潔なコードとして提供する。ケースクラスは、パターンマッチと共に利用すると特に有用だ。パターンマッチの解析器は、さらに強力な静的保証を提供する包括的解析 (exhaustivity analysis) を実装している。

Effective Scala 関数型プログラミング-代数的データ型としてのケースクラス

( ゜Д゜)

説明が簡潔過ぎてもっと分からなくなってしまいました。

代数的データ型 (Algebraic data type)

wikipedia:代数的データ型 という用語が新たに出てきました。Case クラスを理解する前にこの型が何なのかを調べることにしましょう。

余談ですが、以前、Python と型ヒント について調べていたときに Alex Gaynor 氏が Python の型システムについての懸念を表明し、その中で代数的データ型がないといったことも挙げられていました。

Python's type system isn't very good. It lacks many features of more powerful systems such as algebraic data types, interfaces, and parametric polymorphism. Despite this, it works pretty well because of Python's dynamic typing. I strongly believe that attempting to enforce the existing type system would be a real shame.

[Python-ideas] Proposal: Use mypy syntax for function annotations

そういったやり取りも記憶に残っていて代数的データ型に興味がありました。大雑把にいまの自分の理解で要約しますが、厳密な定義は原典を参照してください。

代数的データ型とは、一般的に関数型言語にみられるデータ型で、具体的には直積型、直和型、列挙型や再帰型といったデータ型を指します。関数型言語では、これらのデータ型を使ったプログラミングが一般的であり、パターンマッチングと共にそのデータ型の表現や操作について簡潔、且つ強力に扱えます。

代数的データ型と簡潔な表現

例えば、なぜ次に学ぶ言語は関数型であるべきか - YAMAGUCHI::weblog の記事で紹介されている Boolean 式を表す型と、それらの式を評価する関数の定義が以下になります。

  • OCamlでの表現型と評価器
type 'a expr = | True 
               | False 
               | And  of  'a expr * 'a  expr 
               | Or   of  'a expr * 'a  expr 
               | Not  of  'a expr 
               | Base of  'a  
 
let  rec eval eval_base expr  = 
   let  eval' x = eval eval_base x in 
   match expr with 
   | True  -> true 
   | False -> false 
   | Base base  -> eval_base base 
   | And  (x,y) -> eval' x && eval' y  
   | Or  (x,y)  -> eval' x || eval' y 
   | Not  x     -> not (eval' x) 

これと同等のことを Python で実装してみたのが以下の記事になります。

代数的データ型とオブジェクト指向プログラミングと

代数的データ型の直和型 (後述) に相当するものは、オブジェクト指向言語においてもクラスの継承や列挙型 (Enum) で表現できます。以下は列挙型を使って表現したコードです。

# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod

from extenum import ConstantSpecificEnum

class Evaluator(metaclass=ABCMeta):
    @abstractmethod
    def evaluate(self, value): pass

class MyEvaluator(Evaluator):
    def evaluate(self, value):
        return bool(value)

class Expr(ConstantSpecificEnum):

    TRUE = 1
    FALSE = 2
    BASE = 3
    AND = 4
    OR = 5
    NOT = 6

    @overload(TRUE)
    def eval(self, evaluator, *args):
        return True

    @overload(FALSE)
    def eval(self, evaluator, *args):
        return False

    @overload(BASE)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0])

    @overload(AND)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0]) and evaluator.evaluate(args[1])

    @overload(OR)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0]) or evaluator.evaluate(args[1])

    @overload(NOT)
    def eval(self, evaluator, *args):
        return not evaluator.evaluate(args[0])

ぱっと見た直感で随分とコード量が増えて、コードの見た目 (表現) が冗長になってしまっているというのに気付くと思います。

関数型言語で代数的データ型の定義とそれを扱う処理はとても簡潔に書ける (表現できる) のに対して、代数的データ型をサポートしないオブジェクト指向言語でそういった処理を実装するのは冗長で複雑になりがちであるというのを実感する例です。

私が調べた中では、代数的データ型について語るときに言語機能としてそういったデータ型を簡潔に強力に表現できるかどうかということが論点の1つとして語られているように思います。

余談ですが、直和型を簡潔に表現するために Python 標準の enum モジュールだと機能不足だったので extenum というパッケージを作りました。extenum の機能と用途について簡単にまとめた記事が以下になります。

enum を拡張する extenum パッケージを作りました

標準の enum モジュールにはない以下の機能を提供しています。

  • 定数固定メソッド実装
  • 暗黙の列挙型メンバー
  • EnumSet

代数的データ型とパターンマッチング

代数的データ型の文脈で使われるデータ型がいくつかあります。Python におけるそれらを考察したのが以下の記事になります。

代数的データ型とパターンマッチングと

  • 直積型
  • 直和型
    • 前節の Ocaml のコードのように関数型言語では簡潔に表現できる
    • Python ではサポートしていない、継承や enum で代替できるが簡潔ではない
  • 列挙型
    • Python 3.4 から標準ライブラリとして提供されている
  • 再帰
    • データ型を定義するときに再帰的に扱う、前方参照 (forward reference) が必要

さらに代数的データ型とパターンマッチングは表裏一体な機能と言っても良さそうなので、一緒に考察することでその利点がより分かりやすくなります。

上記の記事で直積型は namedtuple に相当すると説明しています。

ここでようやく元の話に戻ってきましたが、この直積型に相当するものが Case クラスです。MacroPy では以下のように定義します。

@case
class Point(x, y): pass

MacroPy では Case クラスは以下の機能をもつと説明されています。Case クラスは @macros.decorator で実装されています。

ざっくり言うと、様々な特殊メソッド (機能) をもつクラスを自動生成してくれます。こういった何かの機能を自動生成するものをボイラープレート (boilerplate) と呼んだりするようです。詳細は README にあるサンプルコードを参照してください。

先の記事を書いた後で id:podhmoPythonのnamedtupleについて見過ごしてきたこと で namedtuple はタプルであって型ではないと言及しているのに気付きました。

これは結局、namedtupleは名前の通りtupleでしかないせいです。tupleなので型名を持っていません。したがって、同じ順序で同じ値が渡されていたものは比較でTrueになるというわけです。

先の記事で namedtuple が直積型に相当すると書いたのは厳密には間違っていて、Case クラスの __eq__ ではクラスのチェックも実装されているため、Case クラスが直積型に相当すると言った方が適切でしょう。

    def __eq__(self, other):
        try:
            return self.__class__ == other.__class__ \
                and all(getattr(self, x) == getattr(other, x) for x in self.__class__._fields)
        except AttributeError:
            return False

さて、前述した OCamlでの表現型と評価器を、MacroPy の Case クラスとパターンマッチングを使って実装してみます。

# -*- coding: utf-8 -*-
from macropy.case_classes import macros, case
from macropy.experimental.pattern import macros, ClassMatcher, _matching, switch

@case
class Expr:  # Algebraic data type
    class True_: pass
    class False_: pass
    class Base(value): pass
    class And(expr1, expr2): pass
    class Or(expr1, expr2): pass
    class Not(expr): pass

def eval_(expr):  # Pattern Matching
    with switch(expr):
        if Expr.True_():
            return True
        elif Expr.False_():
            return False
        elif Expr.Base(value):
            return bool(value)
        elif Expr.And(expr1, expr2):
            return eval_(expr1) and eval_(expr2)
        elif Expr.Or(expr1, expr2):
            return eval_(expr1) or eval_(expr2)
        elif Expr.Not(expr):
            return not eval_(expr)

def test():
    True_, False_ = Expr.True_, Expr.False_
    Base, And, Or, Not = Expr.Base, Expr.And, Expr.Or, Expr.Not
    assert eval_(Not(True_())) is False
    assert eval_(Or(And(Base(3), False_()), Not(False_()))) is True

列挙型で実装したコードよりも随分とすっきりしましたね。

Case クラスの継承 によると、@case で生成したクラスでは、継承関係をネストした内部クラスとして表現できるようです。この例では、Expr の内部クラスは Case クラスを継承することになります。

そして eval_() 関数が MacroPy の パターンマッチング のマクロで実装されています。こういった with 文と一緒に使うマクロは @macros.block で実装されています。

もはや Python の意味論ではないので何ともコメントが難しいですが、OCaml のコードによく似た表現になっていることが伺えます。そして、それでもまだ冗長であるのも否めない気はします。

感覚的なものですが、構文を変えずに意味論だけの拡張で他言語の概念を取り入れるというものの限界というのか境界というのか、そういったものが見え隠れしている気がします。

代数的データ型の補足

Python のようなオブジェクト指向言語ではほぼ馴染みがないため、最初から関数型言語で学習する方が良いとは思いますが、PyAlgebraicDataTypes というライブラリが代数的データ型とパターンマッチングの学習向けに分かりやすいと思います。以下に簡単なチュートリアルを書きました。

代数的データ型と FizzBuzz と

他にも代数的データ型そのものの概要やそれに関するデータ構造について以下の記事が参考になりました。

The Algebra of Data, and the Calculus of Mutation

代数表現とデータ型の表現の概念から始まり、直積型と直和型、再帰型の説明、後半に wikipedia:en:Zipper_(data_structure) や One-Hole Contexts といった話題も出てきます。

まとめ

MacroPy のマクロ機能と関数型言語における代数的データ型の概念について紹介しました。

  • MacroPy
    • Case クラス
    • パターンマッチング
  • 代数的データ型
    • 直積型
    • 直和型
    • 列挙型
    • 再帰

MacroPy を使って代数的データ型を表現してみました。

ボイラープレートとしての Case クラス、Case クラスを型とみなしたパターンマッチングにより、関数型言語のそれに近い表現で実装することはできました。マクロを使うことで Python の意味論を拡張できるというのを実感するには分かりやすい例でした。

Python とマクロ、インポートフックと抽象構文木

どちらがきっかけだったのか忘れてしまいましたが、wikipedia:メタプログラミングwikipedia:抽象構文木 について調べているうちに マクロ が出てきました。

私の中では、マクロと聞くと、C 言語の、プリプロセッサ (コンパイルの前処理) でコードに置き換えるものを漠然とイメージします。改めてマクロって何だったっけ?何が嬉しいのだっけ?と考えてみると、基本的なことが分かっていないことに気付いたのでマクロについて調べ直してみました。

マクロとは

wikipedia からマクロの定義を引用します。

A macro (short for "macroinstruction", from Greek μακρο- 'long') in computer science is a rule or pattern that specifies how a certain input sequence (often a sequence of characters) should be mapped to a replacement output sequence (also often a sequence of characters) according to a defined procedure. The mapping process that instantiates (transforms) a macro use into a specific sequence is known as macro expansion. A facility for writing macros may be provided as part of a software application or as a part of a programming language. In the former case, macros are used to make tasks using the application less repetitive. In the latter case, they are a tool that allows a programmer to enable code reuse or even to design domain-specific languages.

wikipedia:en:Macro_(computer_science)

ざっくり意訳すると、

コンピューターサイエンスで言うところのマクロ ("マクロ命令" の省略形) とは、定義された手続きに従い、特定の入力シーケンス (文字列のシーケンス) が出力シーケンス (文字列のシーケンス) に置き換えられる方法を指定するルールやパターンのことです。特定のシーケンス内にインスタンス化する (変換する) 対応付け処理は、マクロ展開として知られています。マクロを書くための機構 (facility) は、ソフトウェアアプリケーションの一部、もしくはプログラミング言語の一部として提供されるかもしれません。前者は簡潔な表現でそのアプリケーションを使うタスクを作るために使われます。後者はプログラマーにとってコードを再利用させたり、ドメイン特化言語を設計することさえ可能にするといったツールになります。

大雑把に要約すると、既定のコードを置き換えるルールやパターンを作ることで簡潔な表現やコードの再利用性をもたらすといったことが嬉しそうですね。

また、マクロと言えば Lisp 系の言語の特徴的な機能のように私はよく見聞きしていました。

S式は言ってみれば言語の構文木そのものです。普通の言語では、処理系のフロントエンドにある構文解析器が、「人間に優しい」文法を「機械が理解しやすい」 構文木に変換します。

Lisp:S式の理由

wikipedia:S式 という表現方法、プログラムのコードそのものをデータとして扱えるという特性により、普通の言語 *1 で必要な構文解析や抽象構文木を操作するといった処理が簡単になり、その結果としてマクロがより身近で強力なものになるのではないかと推測します。

参考までに Clojure でのマクロを使う動機付けについて書かれた記事を紹介します。


閑話休題。前置きが長くなってしまいました。先の wikipedia の続きの説明によると、Python も立派にマクロをサポートしています。

While syntactic macros are often found in Lisp-like languages, they are also available in other languages such as Prolog, Dylan, Scala, Nemerle, Rust, Haxe,[5] and Python.[6]

Syntactic macros

あれ!?そうだったっけ?と思う方もいるかもしれません。普通に Python でマクロを書いたりすることはないのでイメージできないかもしれません。

その根拠として Python でマクロを提供するライブラリとして MacroPy が紹介されています。このライブラリは様々な機能がマクロとして実装されていて、Python でマクロをどう実装するかの参照実装の1つとして良いと思います。念のため、初学者向けに断っておくと、Python における一般的なアプリ開発の用途でマクロを使う必要性は全くありません。本稿ではマクロという概念そのものを学ぶことが目的です。またマクロはその特性上、その言語におけるメタプログラミングを提供する仕組みとも密接な関係があります。そのため、マクロを学ぶことは Python におけるメタプログラミングを学ぶ上で良い題材とも言えるでしょう。

Python におけるマクロの概説

MacroPy の概要説明に分かりやすい図があるのでそこから引用します。


MacroPy は大まかに次のように動作します。

1. モジュールのインポートをフックする (インターセプトする)
2. モジュールのソースコード構文解析して AST (抽象構文木) に変換する
3. AST をトラバースして、見つけたマクロを展開する
4. 改変した AST をコンパイルして、モジュールの読み込みを再開する

(注) あるモジュール内でマクロが使われているとき、そのモジュールを直接実行することはできない (マクロが有効にならない) 。

MacroPy 30,000ft Overview

横文字がたくさん出てきました。まず用語が分からないとそれだけで嫌になってきます。それぞれの用語を1つずつ整理しながら意図している内容を噛み砕いていきましょう。

Python のモジュール

通常 モジュール は、Python のプログラム (ソースコード) を記述した xxx.py というファイルです。ここでは Python でインポートできる最小単位がモジュールであることを覚えておいてください。モジュールも Python の世界の中で扱えるオブジェクトの1つです。

歴史的に Python のインポートの API が貧弱だったことからいまの状況はやや混沌としています。私はよく知らないので簡単な紹介しかできませんが、いまは3つの方法があります。

1. __import__() 関数

import 文で呼ばれる組み込み API といったものでしょうか。インポートを制御する低レベルのインターフェースです。昔からのライブラリなどで動的にモジュールをインポートするプログラムでしばしば見かけたりします。昔は __import__ を使うしかなかったのですが、いまは importlib.import_module() を使うようにとドキュメントで推奨されています。

2. imp ライブラリ

このライブラリがいつからあるのか、どういった変遷を経たのか私はよく知りませんが、PEP 302 の仕組みを提供するライブラリの1つです。但し、ドキュメントによると 3.4 で撤廃とあるので今後は importlib へ移行されていくようです。

PEP 302 の Python バージョンが 2.3 (2002年) と明記されています。それなりに昔からある仕組みのようです。この PEP には後述するインポートフックの仕様についても記述されています。

3. importlib ライブラリ

Python 3.1 から導入されたインポートを扱う新たなライブラリです。一部 2.7 にもバックポートされています。

What’s New In Python 3.1 を眺めていて1つ気付くのは、importlib はインポート文の pure Python な参照実装だとあります。imp の C 拡張 (CPython) なところを取り除いていって、インタープリター間の移植性を高めたいといったところも狙いのようです。

と、考察した後になってから importlib の冒頭にその目的が書いてあることに気付きました。

The purpose of the importlib package is two-fold.
One is to provide the implementation of the import statement (and thus, by extension, the __import__() function) in Python source code.
(...snip...)
Two, the components to implement import are exposed in this package, making it easier for users to create their own custom objects (known generically as an importer) to participate in the import process.

importlib 31.5.1 はじめに

また後述するインポートフックのところでも出てきますが、以下の内容も頭の片隅に入れておいてください。

モジュールには、関数定義に加えて実行文を入れることができます。これらの実行文はモジュールを初期化するためのものです。これらの実行文は、インポート文の中で 最初に モジュール名が見つかったときにだけ実行されます。

6.1. モジュールについてもうすこし
インポートフック

Python のモジュールをインポートするときの処理に割り込んでごにょごにょするといったことをインポートフックと呼びます。

先ほどの PEP 302 で導入された仕組みによると、sys.meta_pathfinder オブジェクトを登録することにより、デフォルトの sys.path よりも先にその finder.find_module() が呼ばれます。そして、finder.find_module()loader オブジェクトを返し、loader.load_module() がモジュールオブジェクトを返します。

実際に試してみましょう。

$ vi run.py 
# -*- coding: utf-8 -*-
def main():
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

適当なパッケージとモジュールを作り、

$ tree .
.
├── run.py
└── test
    ├── __init__.py
    ├── t1.py
    ├── t2.py
    ├── t3.py

$ head test/*.py
==> test/__init__.py <==

==> test/t1.py <==
print('I am t1')

==> test/t2.py <==
print('I am t2')

==> test/t3.py <==
print('I am t3')

インポート時に標準出力するだけのプログラムを用意します。

$ python run.py 
I am t1
I am t2
I am t3

インポートフックを実装するための finder/loader の両方の機能をもつ ImportHook クラスを定義し、インポート前に sys.meta_path に登録します。ImportHookインスタンスfind_module() が loader としての自分自身を返し、load_module() が呼ばれます。

# -*- coding: utf-8 -*-
import imp
import sys

class ImportHook:

    def find_module(self, mod_name, path=None):
        if mod_name == 'test.t2':
            print('find_module:', mod_name, path)
            return self

    def load_module(self, mod_name):
        print('load_module:', mod_name)
        path = mod_name.split('.')
        mod = imp.load_module(mod_name, *imp.find_module(path[-1], path[0:-1]))
        return mod

def main():
    sys.meta_path.insert(0, ImportHook())
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

このプログラムを実行すると以下の出力になります。

$ python3.4 run.py  # python2.7 でも実行可
I am t1
find_module: test.t2 ['/Users/t2y/work/external-repo/python/learn/import-hook/test']
load_module: test.t2
I am t2
I am t3

インポートフックが呼ばれてモジュールの検索と読み込みが行われているのが確認できました。

余談ですが、ドキュメントを見ていて sys.path_hooks というのもあるようです。この例では sys.meta_path に finder オブジェクトを登録しましたが、その finder オブジェクトを生成する呼び出し可能オブジェクトのリストを登録するようです。さらにもう1つ前の段階でもフックできるようですね。

リファレンス:

抽象構文木 (Abstract Syntax Tree)

抽象構文木構文解析構文木とデータ構造の中間的なものとして使用される。さらにコンパイラインタプリタなど(プログラミング言語処理系)でのプログラムの中間表現として使われ、コンパイラ最適化やコード生成はその上で行われる。抽象構文木のとりうる構造は抽象構文で記述されている。

wikipedia:抽象構文木

Pythonソースコード構文解析して、抽象構文木 (以下 AST) を扱うために ast モジュールという標準ライブラリがあります。ast モジュールのヘルパー関数を使うと、簡単にソースコードを AST のノードオブジェクトに変換できます。

ソースコードを見た方が分かりやすいので簡単なサンプルを紹介します。

# -*- coding: utf-8 -*-
import ast
import inspect
import sys

def func():
    for i in range(3):
        if i == 2:
            print('Value is {}'.format(i))

source = inspect.getsource(func)
tree = ast.parse(source)
print(ast.dump(tree))

実行すると以下のような AST のノードオブジェクトの dump が出力されます。手で整形するのも難しかったのでちょっと見辛いですが、どういったオブジェクト表現かという雰囲気は掴めます。後ほど、マクロ展開について考察するときのために Python の AST 表現は (知らない人には) 訳が分からない程度に覚えておいてください。

$ python3.4 ast_sample.py 
Module(
body=[FunctionDef(name='func',
           args=arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
  body=[For(target=Name(id='i', ctx=Store()),
             iter=Call(func=Name(id='range', ctx=Load()),
             args=[Num(n=3)], keywords=[], starargs=None, kwargs=None),
    body=[If(test=Compare(left=Name(id='i', ctx=Load()), ops=[Eq()], comparators=[Num(n=2)]), 
      body=[Expr(value=Call(func=Name(id='print', ctx=Load()),
                 args=[Call(func=Attribute(value=Str(s='Value is {}'), attr='format', ctx=Load()),
                   args=[Name(id='i', ctx=Load())],
                   keywords=[], starargs=None, kwargs=None)],
                 keywords=[], starargs=None, kwargs=None))],
    orelse=[])],
  orelse=[])],
decorator_list=[], returns=None)]
)

実際に ast モジュールを使ったことがなかったのと公式ドキュメントでは使い方がよく分からなかったので以下に簡単な入門記事を書きました。

ast モジュールの使いどころとしては、Python コードを処理するテンプレート、コード解析、マクロ、そういった類のツールなどが一般的です。分かりやすいサンプルとして、以下にデコレートされている関数やメソッドを調べるといったことをやってみました。

ソースコードの AST 変換とモジュール読み込み

ここまででインポートフックと AST について分かりました。

前々節のインポートフックのソースコードを修正して、あるモジュールのソースコードを読み込んで AST に変換・改変した後、その AST をコンパイルして新規にモジュールを作成するのをやってみましょう。

AST のノードオブジェクトに変更を加えるには ast モジュールの NodeTransformer を使います。ast.parse() でファイルのソースコードを AST オブジェクト (Module ノード) に変換し、NodeTransformer を継承したクラスを設けてトラバースするのが簡単です。ここでは文字列のノードオブジェクトの値を書き換えています。

class StringTransformer(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'I am StringTransformer'
        return node

...
    def handle_ast(self, file_, mod_name):
        source = file_.read()
        tree = ast.parse(source)
        transformed_tree = StringTransformer().visit(tree)
        code = compile(transformed_tree, mod_name, 'exec')
        return code

NodeTransformer は Visitor パターンで処理を実装します。Visitor パターンって何だったっけ?という方は以下の記事で復習しましょう。

そして、NodeTransformer で変更を行った AST の Module ノードオブジェクトを compile() することでコードオブジェクトを取得します。

次に置き換え用のモジュールオブジェクトを新規に生成し、先ほど変更を加えてコンパイルしておいたコードオブジェクトをそのモジュールの名前空間exec() します。これはモジュールのところでインポート時に実行して初期化されるという処理に相当します。

    def create_module(self, mod_name, file_name, code):
        mod = imp.new_module(file_name)
        sys.modules[mod_name] = mod
        exec(code, mod.__dict__)
        return mod

Python でインポートしたときに行われる処理、sys.modules への登録やコード実行などを行っています。本来は __file____package__ といった属性にも適切な値を設定すべきですが、単純なサンプルなのでここでは省きます。

前々節のインポートフックのソースコードに修正を施したものが以下になります。

# -*- coding: utf-8 -*-
import ast
import imp
import sys

class StringTransformer(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'I am StringTransformer'
        return node

class ImportHook:
    def find_module(self, mod_name, path=None):
        if mod_name == 'test.t2':
            print('find_module:', mod_name, path)
            return self

    def load_module(self, mod_name):
        print('load_module:', mod_name)
        package_name, file_name = mod_name.split('.')
        file_, pathname, desc = imp.find_module(file_name, [package_name])

        # read source and transform ast
        code = self.handle_ast(file_, mod_name)

        # create new module and exec it
        mod = self.create_module(mod_name, file_name, code)
        return mod

    def handle_ast(self, file_, mod_name):
        source = file_.read()
        tree = ast.parse(source)
        print('AST:', tree)
        transformed_tree = StringTransformer().visit(tree)
        code = compile(transformed_tree, mod_name, 'exec')
        print('compiled:', code)
        return code

    def create_module(self, mod_name, file_name, code):
        mod = imp.new_module(file_name)
        sys.modules[mod_name] = mod
        exec(code, mod.__dict__)
        return mod

def main():
    sys.meta_path.insert(0, ImportHook())
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

実行結果です。

$ python3.4 run.py 
I am t1
find_module: test.t2 ['/Users/t2y/work/external-repo/python/learn/import-hook/test']
load_module: test.t2
AST: <_ast.Module object at 0x1053c0518>
compiled: <code object <module> at 0x105313ae0, file "test.t2", line 1>
I am StringTransformer
I am t3

"I am t2" の文字列出力を AST のレイヤーで書き換えて実行することができました。

ここまでのサンプルコードではマクロ展開以外の動作、つまり MacroPy の概要説明にあるモジュール読み込みのワークフローの流れを確認しました。

マクロ展開について少し

前節のサンプルコードでは、実際にマクロを定義したわけではありませんが、StringTransformer で AST のノードオブジェクトの文字列の値を直接変更しました。冒頭で紹介した、マクロが既定コードの置き換えを目的としているといった内容を思い出してください。マクロ展開というのは、マクロというルールやパターンから AST のレイヤーで変更を行うことに相当します。つまり、マクロで実現したいことは AST で実現できるということであり、AST で実現できないことはマクロで実現できないということでもあります。

ここで Python の構文とその AST のオブジェクト表現を見比べてみます。

>>> import ast
>>> ast.dump(ast.parse("2 + 2"))
'Module(body=[Expr(value=BinOp(left=Num(n=2), op=Add(), right=Num(n=2)))])'

ある記事の言葉を引用すると、

この結果にはかなりがっかりした。Lisp なら、これと同等の構文木は (+ 2 2) である。この構文木なら誰にでも使えるが、 Python のこんな構文木をいじれるのは本当のエキスパートだけだろう。

Python にはマクロがない。

Python の構文と AST の表現が全く違うことから、マクロを書く・展開する・適用する = AST を操作するということが誰にでも実装できない、ひいては普通にマクロを書くことはないということにつながるように思います。

Karnickel

いきなり MacroPy を読み進めようとすると、複雑過ぎて挫折してしまうかもしれません。私は挫折しました。

学習向けにもっと簡単なものとして Karnickel という小さいライブラリがあります。これは Sphinx の作者としても有名な Georg Brandl 氏 *2ast モジュールとインポートフックのデモとして作ったライブラリのようです。

MacroPy の概要説明の図を使いたかったために紹介する順序があべこべになってしまいましたが、KarnickelMacroPy 同様のモジュール読み込みフローをもちます。インポートフック、NodeTransformer によるマクロ展開といった一通りのサンプルを含み、1ファイル300行ちょっとのコード量です。私は最初に Karnickel のライブラリを読み進めました。

Karnickel のマクロ

マクロ実装のデモ向けライブラリのため、マクロとして提供されている機能そのものにあまり意味はありません。

また使い方の説明が分かりにくいため (インポートフックを知っている前提)、マクロ展開のフローが分かっていないと、試しに実行するところから戸惑うかもしれません。簡単に使い方を紹介します。ちなみに KarnickelPython 3 では動きません (Python 2.6+) 。

example.macros にサンプルのマクロが定義されています。マクロは @macro デレコーターで定義するようです。

from karnickel import macro

@macro
def add(i, j):
    i + j

@macro
def assign(n, v):
    n = v

@macro
def custom_loop(i):
    for __x in range(i):
        print __x
        if __x < i-1:
            __body__

実行するときはこのマクロ定義モジュール以外に、インポートフックがある実行用ファイル (run.py) とマクロをインポートして使うファイル (example/test.py) の2ファイルが必要です (たぶん) *3

# -*- coding: utf-8 -*-
import karnickel
karnickel.install_hook()

from example.test import usage_expr, usage_block, usage_3

print 'usage_expr():', usage_expr()
print 'usage_block():', usage_block()
print 'usage_3():'
usage_3()

実行結果。

$ python run.py 
usage_expr(): 22
usage_block(): 1
usage_3():
0
loop continues...
1
loop continues...
2
loop continues...
3
loop continues...
4
loop continues...
5
loop continues...
6
loop continues...
7
loop continues...
8
loop continues...
9

詳細は追いませんが、NodeTransformer を使ってマクロ展開を実装しているのが karnickel.py で確認できます。

class Expander(ast.NodeTransformer):
    """
    AST visitor that expands macros.
    """

    def __init__(self, module, macro_definitions=None, debug=False):
        ...

まとめ

Python におけるマクロを提供する仕組み、言わばメタプログラミングについて紹介しました。

  • Python におけるマクロ
    • インポートフック
    • 抽象構文木
    • AST 変換 (マクロ展開)

インポートフックを実装してみると、インポート周りの API が分かりにくいと思うかもしれません。良く言えば癖がある、悪く言えば使いにくいといった感じでしょうか。歴史的経緯や互換性もあるでしょうし、メタプログラミングが本質的に難しいということなのかもしれません。以前からインポート周りは Python の良くないところの1つに挙げられている発表を私はいくつか見たことがあります。importlib の冒頭にも書かれているよう、もっと使いやすくなるように今後も改善されていくと思います。

次回は MacroPy で提供されているマクロの機能についても見ていきましょう。

*1:この文脈では一般的な手続き型プログラミング言語を指していると思われます

*2:基調講演者 - PyCon APAC 2013

*3:これらを1ファイルにして実行するとエラーになったのでそういう作りなんだと思います

Pyramid ベースの軽量 CMS フレームワーク Kotti の紹介

ここ最近 Kotti アドオンの開発を行っていました。

Kotti は、Pyramid の上位でアプリ開発を支援するフレームワークです。いまのところ、軽量 CMS として開発が進められています。

Kotti を紹介するウェブサイトも構築されています。まだ概要程度の情報量ですが、日本語化もされているのでざっとご覧ください。

個人的にちょっと使ってみた感想としては、Kotti の CMS 機能もその完成度もまだまだベータ開発中といったところです。実用的なレベルまではもう少し時間がかかりそうですが、開発が活発に行われているので将来が楽しみです。私の場合、Pyramid を使ったアプリを開発してみたかったことと、普通の Web アプリにユーザー管理やコンテンツ管理の機能を簡単に追加したかったという動機で Kotti を利用してみました。

開発していると Pyramid の知識も必要になりますが、Node/Content という Kotti がもつリソースモデル、pyramid_tm によるリクエスト単位のトランザクション管理など、Pyramid 流の開発のプラクティスとして、フレームワークでどんなことをやっているのかを学ぶことが多かったです。また、後述する Kotti アドオンの開発も容易でした。

私が開発したものは Amazon Elastic MapReduce (Amazon EMR) を操作する Kotti アドオンです。

現時点の EMR の GUI と比較して、

  • EMR 設定をリソースとして保存・再利用
  • サービス単位にジョブフローをまとめる
  • Keep Alive を有効にしたインスタンスへ追加ステップを実行

といった特徴があります。但し、まだまだ開発ステータスがアルファレベルであり、機能や UI は最低限のものですし、運用面の考慮も全くされていません。実験中といった感じです。

Kotti アドオンを開発する上で慣れが必要なのは、複数プロダクトの仕組みやドキュメントを調べないといけないことです。主なライブラリのドキュメントは以下です。

例えば、INI ファイル の設定においても、

kotti.site_title = Kotti with mapreduce
kotti.secret = qwerty

などの kotti で始まる設定は Kotti 本体の設定ですが、

pyramid.default_locale_name = en
pyramid.includes =
    pyramid_debugtoolbar

などの pyramid で始まる設定は Pyramid のドキュメントを調べないといけません。
また、

mail.default_sender = yourname@yourhost

は、pyramid_mailer の設定であり、これは Message オブジェクトの sender 属性 (envelope-from) に利用されます。

開発していて気になることや不具合があったときに Kotti 本体だけ調べても分からないこともよくあります。そんなとき、この機能を実際に提供しているのはどのプロダクトか、それはどういった仕組みかを理解するために、それぞれのプロダクトについての理解を深め、全体像を把握しないと解決策が分からないこともあります。

最初の取っ掛かりとしては、既に PyPI 上にも10個以上の Kotti アドオンが公開されているので、実際に動くサンプルとしてコードを眺めるのも参考になります。

まだまだ荒削りなフレームワークですが、その分、自分で開発したり、改善したりする余地がたくさんあります。Pyramid アプリ入門として Kotti アドオンを作るのでも良いように思います。もう少し経って Kotti の開発が落ち着いたら、ドキュメント翻訳にも挑戦してみたいところです。

ローカル環境で複数の Sphinx ドキュメントを参照する

最近の Python パッケージは、Sphinx でドキュメントが書かれていることが多いです。

Sphinx でドキュメントが書かれているなら、そのパッケージのソースをクローンしてきて、ローカルでドキュメントをビルドするのも簡単です。私の場合、そのときの開発に用いるフレームワーク・ライブラリ・ツール等は、リポジトリからソースをクローンしてきて、最新版ソースを開発環境へインストールして、自分のアプリケーションを開発する上でデバッグしやすい環境を構築しています。また、そのドキュメントもローカルでビルドしています。

例えば、KottiPyramidフレームワークのドキュメントをローカルでビルドしてみます。

$ cd /path/to/Kotti/
$ python setup.py build_sphinx

$ cd /path/to/pyramid/docs/
$ make html

このとき、前者は Kotti/build/sphinx/html/ に、後者は pyramid/docs/_build/html/ に HTML ドキュメントが生成されます。

ローカルで HTML ドキュメントを参照する際に Sphinx の検索機能を使いたいので HTTP サーバーを介して参照できると便利です。とはいえ、そのためにわざわざ HTTP サーバーを設定・構築するのもちょっと面倒です。前置きが長くなりました、そんな用途に「ちょっとだけ」便利な HTTP サーバーを作ってみました。

その名の表す通り、標準ライブラリの SimpleHTTPServer をちょっとだけカスタマイズしたものです。

先ず SimpleHTTPServer を使って、複数のドキュメントを見ようとすると次のようになります。

$ cd /path/to/Kotti/build/sphinx/html/
$ python -m SimpleHTTPServer 8001
Serving HTTP on 0.0.0.0 port 8001 ...

$ cd /path/to/pyramid/docs/_build/html/
$ python -m SimpleHTTPServer 8002
Serving HTTP on 0.0.0.0 port 8002 ...

SimpleHTTPServer は、カレントディレクトリ配下のみを公開ディレクトリとして扱います。2-3のドキュメントならこれでも良いですが、10個のドキュメントをこの方法で見ようとすると、コマンドラインでサーバー起動するのが面倒だったり、どのドキュメントがどのポート番号か分からなくなったりします。

この煩わしさを解決するのが LittleHTTPServer です。次のようにコマンドラインオプションで指定します。

$ littlehttpserver -p 8003 -d /path/to/pyramid/docs/_build/html/ -d /path/to/Kotti/build/sphinx/html/
INFO: Serving HTTP/1.0 on 0.0.0.0, port: 8003 ...

http://localhost:8003 へアクセスすると、次のように表示されます。

これならローカルで見たいドキュメントが複数になっても煩わしくないですね。

2012/8/22 追記

コメントをもらったので -m オプションで実行できるように修正して再リリースしました。

$ python -m littlehttpserver -i path/to/top -d path/to/pkg1

コマンドラインオプション付きの which コマンドを実装してみた

というツイートを見かけました。

以前、勉強がてらに

を作ったことがありましたが、もうあれから3年も経つんだなとコードを見返しながら感慨に浸っていました。

そんなとき、ふと which コマンドを実行したところ、コマンドラインオプションがあることに気付きました!

$ which
usage: which [-as] program ...

$ man which
WHICH(1)                  BSD General Commands Manual                 WHICH(1)

NAME
     which -- locate a program file in the user's path

SYNOPSIS
     which [-as] program ...

DESCRIPTION
     The which utility takes a list of command names and searches the path for each 
     executable file that would be run had these commands actually been invoked.

     The following options are available:

     -a      List all instances of executables found (instead of just the first one of each).

     -s      No output, just return 0 if any of the executables are found, or 1 if none are found.

     Some shells may provide a builtin which command which is similar or identical
     to this utility.  Consult the builtin(1) manual page.

これは、、、

。。。

過去の私の実装に足りないものを発見してしまいました、もう再実装するしかない! (> <)

ということで、コマンドラインオプションを追加するだけなのですが、結構難しくて悩んでしまいました。

# -*- coding: utf-8 -*-
import glob
import os
import sys
import argparse
from itertools import chain
from os.path import join as pathjoin
from operator import itemgetter

def search(cmd, paths, is_all=False):
    for path in paths:
        for match in glob.glob(pathjoin(path, cmd)):
            if os.access(match, os.X_OK):
                yield match
                if not is_all:
                    raise StopIteration

def parse_argument(args=None):
    parser = argparse.ArgumentParser()
    parser.set_defaults(is_all=False, is_silent=False, commands=[])
    parser.add_argument("-a", dest="is_all", action="store_true",
        help="List all instances of executables found "
             "(instead of just the first one of each).")
    parser.add_argument("-s", dest="is_silent", action="store_true",
        help="No output, just return 0 if any of the executables are found, "
             "or 1 if none are found.")
    parser.add_argument("commands", nargs="*")
    args = parser.parse_args(args or sys.argv[1:])
    return args

def main(cmd_args=None):
    args = parse_argument(cmd_args)
    env_paths = os.environ['PATH'].split(':')
    result = []
    for cmd in args.commands:
        founds = list(search(cmd, env_paths, args.is_all))
        result.append((0, founds) if founds else (1, [cmd]))

    status_code = max(map(itemgetter(0), result))
    if not args.is_silent:
        cmd_paths = [paths for ret_val, paths in result if ret_val == 0]
        for cmd_path in chain.from_iterable(cmd_paths):
            print cmd_path
    return status_code

if __name__ == '__main__':
    sys.exit(main())

実行結果はこんな感じです。

$ python which.py -a ls vi unknown
/bin/ls
/opt/local/bin/vi
/usr/bin/vi

$ python which.py -s ls vi unknown; echo $?
1

ついでに本当に which コマンドと動作が一致しているか、簡単なテストを書いてみました。

# -*- coding: utf-8 -*-
import sys
from subprocess import Popen, PIPE

import pytest
import which

FILESYSTEM_ENCODING = sys.getfilesystemencoding()

def get_system_which_result(args):
    cmds = ["which"]
    cmds.extend(args)
    p = Popen(" ".join(cmds), stdout=PIPE, stderr=PIPE, shell=True)
    out, err = p.communicate()
    return p.returncode, out, err

@pytest.mark.parametrize("args", [
    ["ls"],
    ["cd", "pwd"],
    ["non_existence"],
    ["-a", "vi"],
    ["-a", "ls", "vi"],
    ["-s", "non_existence"],
    ["-a", "-s", "ls", "vi"],
    ["-a", "-s", "ls", "vi", "non_existence"],
])
def test_which_command(args, capsys):
    my_ret = which.main(args)
    my_out, my_err = capsys.readouterr()
    sys_ret, sys_out, sys_err = get_system_which_result(args)
    assert sys_ret == my_ret
    assert sys_out == my_out.encode(FILESYSTEM_ENCODING)
    assert sys_err == my_err.encode(FILESYSTEM_ENCODING)

ソースは以下にあります。

こういったシンプルなツールを作るのはおもしろいですね。

PyCon Taiwan 2012 イベントレポート Day 2

6月9-10日に開催された PyCon Taiwan 2012 に参加してきました。

前回の記事からちょっと間があきましたが、2日目 (10日) のカンファレンスレポートです。私は基調講演について執筆しました。

fanstatic による Python パッケージを使った静的リソース管理

最近 Kotti で Web アプリを作ってみようと調査しています。但し、今日は Kotti のお話ではなく、たまたま更新差分を見ていたら、fanstatic という静的リソース管理ツールが新規に使われているのを見つけました。ちょっと調べてみると、とても良さそうに見えたので紹介します。

fanstatic って何?

特にドキュメントでパッケージ名の由来を見つけられなかったのですが、fan + static と区切ってみると名前を覚えやすいです。個人的に fan- という係りが fancy, fantasy, fantasista などを連想させて言葉の響きが良いですね。static リソースの管理の煩雑さを解消してくれる夢のようなツールを連想します。

さて、fanstatic は、スマートな静的リソースパブリッシャーとあります。

テンプレートに静的リソースの記述をインジェクションしてくれるので、パブリッシャーという表現をしているのでしょうが、それよりも javascriptcss といった静的リソースを Python パッケージとして管理できる仕組みに私は驚きました。

パッケージ、ドキュメント、リポジトリはそれぞれ以下になります。

fanstatic で jquery を使ってみよう

fanstatic を使うと、どんなことができるか、クイックスタート を参考にしながら見てみましょう。

まずは fanstatic と js.jquery というパッケージをインストールします。

(fanstatic)$ pip install fanstatic js.jquery
(fanstatic)$ pip freeze
Paste==1.7.5.1
WebOb==1.2
distribute==0.6.24
fanstatic==0.11.4
js.jquery==1.7.1
wsgiref==0.1.2

簡単なサンプルコードから紹介します。

(fanstatic)$ vi quick.py 
# -*- coding: utf-8 -*-
from fanstatic import Fanstatic
from js.jquery import jquery

def app(environ, start_response):
    start_response('200 OK', [])
    jquery.need()
    return ['<html><head></head><body>Hello World</body></html>']

if __name__ == "__main__":
    from wsgiref.simple_server import make_server
    fanstatic_app = Fanstatic(app)
    server = make_server('0.0.0.0', 8080, fanstatic_app)
    server.serve_forever()

通常の WSGI アプリを作成して、2行だけ追加します。jquery.need() は、HTML の <head> セクションに <script> タグを埋め込みます。あとは FanstaticWSGI アプリをラップするだけです。

...
    jquery.need()
...
    fanstatic_app = Fanstatic(app)
...

実行結果を見てみましょう。

(fanstatic)$ python quick.py 

別のターミナルで
(fanstatic)$ telnet localhost 8080 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /

<html><head>
    <script type="text/javascript" src="/fanstatic/jquery/jquery.js"></script>
</head><body>Hello World</body></html>

ちゃんと <head> セクションに jquery の <script> タグが埋め込まれていますね。

作業前に js.jquery を pip でインストールしただけで使えるのが便利です。jquery の最新版をダウンロードしてきて、どこそこに展開して、パス設定はどうしようかな、、、と悩まなくて済むのが凄いところです。また、jquery のような、汎用的なライブラリは、みんなで共有してバージョンアップもパッケージ管理ツール (今回は pip) で行えた方が楽で良いですね。

任意のアプリから静的リソースを管理する

もう少し現実的なアプリケーションについて、リソースライブラリの作成 から見てみましょう。

ここでは静的リソースを含む Python パッケージを作成してみます。まずパッケージディレクトリとひな形を作成します。

(fanstatic)$ mkdir myapp
(fanstatic)$ cd myapp
(fanstatic)$ mkdir -p foo/static
(fanstatic)$ touch foo/__init__.py
(fanstatic)$ touch foo/static/base.css
(fanstatic)$ touch foo/static/base.js

次にパッケージの setup.py を定義します。

(fanstatic)$ vi setup.py
# -*- coding: utf-8 -*-
from setuptools import setup

setup(
    name="myapp",
    version="0.1",
    install_requires=["fanstatic", "js.jquery"],
    entry_points={
        "fanstatic.libraries": [
            "foo = foo.static:lib_foo",
        ],
    },
)

install_requires に fanstatic と使いたい js ライブラリを記述します。ここでは js.jquery のみを記述します。fanstatic がパッケージ内の静的リソースの場所を見つけられるように entry_points を使って定義します。ここが1つのポイントです。

では、先ほど作成した static ディレクトリを fanstatic から見つけられるように static.py に定義します。サンプルコードから紹介します。

(fanstatic)$ vi foo/static.py
from fanstatic import Group, Library, Resource
from js.jquery import jquery

_resources = [jquery]

lib_foo = Library("foo", "static")
foo_js = Resource(lib_foo, "base.js", depends=[jquery], bottom=True)
_resources.append(foo_js)

foo_css = Resource(lib_foo, "base.css", bottom=True)
_resources.append(foo_css)

resources = Group(_resources)

大体見た感じで雰囲気は掴めますが、詳細に見て行きます。

lib_foo = Library("foo", "static")

Library オブジェクトは、名前と静的リソースの置き場所へのパスを引数に取ります。ここで lib_foo は、setup.py の entry_points で定義した名前を使う必要があるのに注意してください。

foo_js = Resource(lib_foo, "base.js", depends=[jquery], bottom=True)

Resource オブジェクトは、実際の静的リソース (js/css) を定義します。base.js は jquery を使うスクリプトなので depends=[jquery] を定義することにより、base.js より前に jquery.js が読み込まれるようにインジェクションされます。

resources = Group(_resources)

Group オブジェクトは、複数のリソースをまとめます。あとで紹介しますが、複数リソースのインジェクションを resources.need() のように実行できて便利です。

主要な点は紹介しました。実際に動かせるように残りのファイルも紹介します。

main プログラムです。せっかくなので css/js が適用されていることを確認できるように html を少し変更します。

(fanstatic)$ vi foo/main.py 
# -*- coding: utf-8 -*-
from fanstatic import Fanstatic
from static import resources as static_resources

def app(environ, start_response):
    start_response('200 OK', [])
    static_resources.need()
    html = """
    <html>
      <head></head>
      <body>
        <button type="button" id="sample_btn">Click Me!</button>
      </body>
    </html>
    """
    return [html]

if __name__ == "__main__":
    from wsgiref.simple_server import make_server
    fanstatic_app = Fanstatic(app)
    server = make_server('0.0.0.0', 8080, fanstatic_app)
    server.serve_forever()

base.css と base.js は、それぞれ以下の通りです。

(fanstatic)$ vi foo/static/base.css 
button {
    border-color: #666666;
    background-color: #E8E6E1;
    font-size: large;
    padding: 10px;
}
(fanstatic)$ vi foo/static/base.js 
$(function() {

    $("#sample_btn").click(function() {
        alert("Hello World");
    });

});

最終的なパッケージの構成です。

(fanstatic)$ tree myapp/
myapp/
├── foo
│&#160;&#160; ├── __init__.py
│&#160;&#160; ├── main.py
│&#160;&#160; ├── static
│&#160;&#160; │&#160;&#160; ├── base.css
│&#160;&#160; │&#160;&#160; └── base.js
│&#160;&#160; └── static.py
└── setup.py

2 directories, 6 files

それでは、パッケージをインストールして、実行してみましょう。

(fanstatic)$ python setup.py develop
(fanstatic)$ python foo/main.py 

ブラウザでアクセスして、ボタンをクリックすると、次のような画面を確認できます。

まとめ

fanstatic の強力さと利便性が分かる簡単なチュートリアルを紹介しました。

ここで紹介したソースは以下に置いてあります。

fanstatic リポジトリ を見ると、js. の名前空間で始まるライブラリがたくさんあります。ここに無ければ、自分でパッケージングして公開するのも良いですね。パッケージ管理の仕組みを、こんな用途にも使えるんだと再発見しました。おもしろいですね。