なんか、趣味では最近はC言語ばっかりだったりするわけですが。

さて、関数型言語系をカジった人なら誰しも取り付かれる、モノ、それがパターンマッチ。パターンマッチが使えると、とにかく直感的にコードをかけますよね。

つーわけで、Pythonでパターンマッチを実装してみました。機能的には

  • リスト,タプルに対するパターンマッチ
  • パターン変数への束縛
  • ガード条件
  • 任意のオブジェクトに対するパターンマッチ
  • 部分パターンの束縛(Ocamlのas)

あたりを実装してみました。これだけあれば、かなり便利にコードをかけます。できるだけ、手軽に書けるように工夫してみました。こんな感じです。

変数束縛とガード。 getattr でごにょごにょしてるので簡単にかけます。

1m = Match([1,2,3]) 
2if m.when([1,2,m.var]) and m.var > 2:
3  print m.var
4# >> 3

こう使えば、Pythonに念願のswitchが!

1m = Match(10)
2if m(9):
3  print 1
4elif m(10):
5  print 2
6else:
7  False
8# >> 2

部分パターンを束縛してみます。 [1,2,m.var] 全体を all というパターン変数に束縛します。

 1m = Match([1,2,3]) 
 2if m.when([1,2,m.var]) and m.var > 5:
 3  False
 4elif m.when(m._as_all([1,2,m.var])):
 5  print m.all
 6  print m.var
 7else:
 8  raise StandardError("")
 9# >> [1, 2, 3]
10# >> 3

任意のオブジェクトにも使えます。いわゆるレコードに対するマッチも簡単にできるということです。

 1class Test(object):
 2  def __init__(self, v1, v2):
 3    self.v1 = v1
 4    self.v2 = v2
 5  def __repr__(self):
 6    return "Test(%s, %s)"%(repr(self.v1), repr(self.v2))
 7m = Match([1, Test(2, 3)])
 8if m.when([1, m._class(Test, {"v1":2, "v2": m.v2})]):
 9  print m.v2
10else:
11  False
12# >> 3

オブジェクトに対するパターンマッチは __match__ メソッドを定義するとカスタマイズできます。ここらのアイデアはScalaからいただきました。

1class Test2(Test):
2  def __match__(self):
3    return {"value": self.v1 + self.v2}
4m = Match([1, 2, Test2(3,4)])
5if m.when([1,2, m._class(Test2, {"value": m.var})]):
6  m.var
7else:
8  False
9# >> 7

結構いい感じな気がします。

ダウンロード

patternmatch.py

実装のお話

ソースコードはこんな感じ。

 1class Match(object):
 2  class _var(str): pass
 3  class _class(object):
 4    def __init__(self, klass, attrs):
 5      self.klass= klass
 6      self.attrs= sorted(attrs.iteritems())
 7    def match(self, m, obj):
 8      props = getattr(obj, "__match__", lambda: obj.__dict__)()
 9      return issubclass(obj.__class__, self.klass) and \
10            m.when(self.attrs, sorted(props.iteritems()))
11  class _as(object):
12    def __init__(self, name, pattern = None):
13      self.name = name
14      self.pattern = pattern
15    def __call__(self, pattern):
16      self.pattern = pattern
17      return self
18
19  def __init__(self, obj):
20    self.obj = obj
21    self.bind = {}
22
23  def __getitem__(self, key):
24    if not self.bind.has_key(key):
25      if key.startswith("_as_"):
26        return self._as(self._var(key[4:]))
27      return self._var(key)
28    return self.bind[key]
29  __getattr__ = __getitem__
30  __call__ = lambda self, *a, **k : self.when(*a, **k)
31
32  def when(self, pattern, obj = None):
33    if not obj: obj = self.obj
34    if isinstance(pattern, (self._var, self._class, self._as)):
35      if isinstance(obj, (list, tuple)):
36        pattern = [pattern]
37        obj     = [obj]
38
39    if not isinstance(obj, (list, tuple)) and \
40      not isinstance(pattern, (list, tuple)) :
41      obj = [obj]
42      pattern = [pattern]
43
44    if not isinstance(obj, (list, tuple)) or  \
45      not isinstance(pattern, (list, tuple)) :
46      self.bind = {}
47      return False
48
49    if len(obj) != len(pattern):
50      if not ((pattern[-1].__class__ == self._var) and pattern[-1].startswith("__")):
51        self.bind = {}
52        return False
53
54    for i, (value, pat) in enumerate(zip(obj, pattern)):
55      if value == pat:
56        continue
57      elif pat.__class__ == self._var and pat.startswith("__"): 
58        self.bind[str(pat)] = obj[i:]
59        return True
60      elif pat.__class__ == self._var:
61        self.bind[str(pat)] = value
62      elif pat.__class__ == self._class:
63        if not pat.match(self, value):
64          self.bind ={}
65          return False
66      elif pat.__class__ == self._as:
67        if not self.when(pat.pattern, value):
68          self.bind ={}
69          return False
70        self.bind[str(pat.name)] = value
71      elif isinstance(value, (list, tuple)) and isinstance(pat, (list,tuple)):
72        if not self.when(pat, value):
73          self.bind = {}
74          return False
75      else:
76        self.bind = {}
77        return False
78
79    return True

まぁわりかしシンプルですね。

 

  今年も終わりが近づいてまいりました。年をとると時間がすぎるのが速いナァ・・・と痛感しております。


Python2.6きましたね。ということで、自分用にも主な変更点メモ。なぐり書きなのでミス多いかも。個人的な注目部分は

  • with文
  • multiprocessing
  • itertoolsへのメソッド追加
  • ABCの導入
  • クラスデコレータの導入
  • ネットワーク系ライブラリ(http,ftp,telnet..etc)でタイムアウトが設定できるようになった。

あたりですかね。ではどうぞ。

Python 3.0由来の変更点

  • 複素数へオブジェクトを変換する __complex__ メソッド。
  • 例外補足のためのもう一つ書き方: except TypeError as exc
  • build-inの reduce() に加え、 functools.reduce の追加。(3.0では reduce はfunctools経由でしか使えない)

3.0では他にもbuild-in関数に変更がある。3.0互換のコードを書きたいなら必要に応じて from future_builtins import hex,map のようにimportすること。

また、 -3 コマンドラインスイッチにより 3.0 で削除される機能を使っている場合、警告を出せる。

PEP 343: ‘with’ statement

1with open('/etc/passwd', 'r') as f:
2    for line in f:
3        print line
4        ... more processing code ...
  • withに入るとオブジェクトの __enter__() 、抜けるときに __exit__(type, value, traceback) が呼ばれる。例外発生しなかった場合、 type, value,traceback はNone。
  • contextlib を使うと簡単に書ける。

PEP 399: メインモジュールからの明示的相対的import

Pythonでは -m スイッチでモジュールをスクリプトとして動かせる。が、パッケージ内部のモジュールを動かそうとした場合、相対的importが上手く働かない。本修正で、 __package__ 属性がモジュールに追加された。この属性がある場合、 __name__ の代わりにこの値からの相対importを行う。

PEP302スタイルのインポータは __package__ を必要に応じてセットできる。 runpy モジュールの -m スイッチはこれを行っているので、パッケージ内からスクリプトを実行しても相対的importは正しく動く。

PEP 371: multiprocessing module

プロセス間通信を実装したモジュール。並列計算などを実装できる。詳しくは Atsushiさんの記事 参照

PEP3101: さらに高度な文字列フォーマット

3.0では % オペレータはさらに高機能になる。2.6ではこの %str.format メソッドで実装している。

 1"User ID: {0}".format("root") -> "User ID: root"
 2
 3'User ID: {uid}   Last seen: {last_login}'.format(
 4      uid='root',
 5      last_login = '5 Mar 2008 07:20') ->
 6  'User ID: root   Last seen: 5 Mar 2008 07:20'
 7
 8'Platform: {0.platform}\nPython version: {0.version}'.format(sys) ->
 9    'Platform: darwin\n
10    Python version: 2.6a1+ (trunk:61261M, Mar  5 2008, 20:29:41) \n
11    [GCC 4.0.1 (Apple Computer, Inc. build 5367)]'
12
13fmt = '{0:15} ${1:>6}'
14fmt.format('Registration', 35) ->
15  'Registration    $    35'

くわしくは PEP3101 参照。

PEP3105: 関数としての print

3.0で print 文は print 関数になる。2.6では __future__ に定義されている。

1from __future__ import print_function
2print('# of entries', len(dictionary), file=sys.stderr)
3
4def print(*args, sep=' ', end='\n', file=None)

PEP 3112: Byte リテラル

3.0` では文字列はデフォルトでUnicodeになり、8bitリテラルは別に用意される。 b'string'byte コンストラクタだ。2.6 ではstrとおなじ bytes 型を定義し、 b'' 記法をサポートする。

また、 __future__ で文字列リテラルを全てUnicodeにすることができる。

1from __future__ import unicode_literals
2
3s = ('\u751f\u3080\u304e\u3000\u751f\u3054'
4    '\u3081\u3000\u751f\u305f\u307e\u3054')
5
6print len(s)               # 12 Unicode characters

PEP 3116: 新しいI/Oライブラリ

Pythonではダックタイピングにより、ファイル-likeオブジェクトは read() , write() が定義されていればよかった。ただ、文字列を扱う場合 readline() が定義されていたほうがよい。このようなケースに応じた基底クラスがPython3.0では io モジュールに用意される。

  • RawIOBase
  • BufferedIOBase
  • TextIOBase

など。

くわしくは PEP 3116 参照。

PEP 3119: Abstract Base Classes

Abstract Base Class(ABC)はJAVAのインターフェースのようなもの。ABCのサポートは ABCMeta というメタクラスを含む abc モジュールで構成される。 ABCMetaisinstanceissubclass のbuilt-in関数で特別に扱われる。

ABCは通常の継承のように

1import collections
2class Storage(collections.MutableMapping):
3  ...

ともABCの register() メソッドをつかって

1import collections
2class Storage:
3    ...
4collections.MutableMapping.register(Storage)

ともできる。 register を使えば、組み込み型や他者の書いたクラスを ABC とすることができる。

ABCはJAVAと違い、Pythonが自動でそのインターフェースをチェックしてくれるわけではない。チェックするためには

1if not isinstance(d, collections.MutableMapping):
2    raise ValueError("Mapping object expected, not %r" % d)

のように書く。

独自のABCは以下のように定義する

 1from abc import ABCMeta, abstractmethod, abstractproperty
 2
 3class Drawable():
 4    __metaclass__ = ABCMeta
 5
 6    @abstractmethod
 7    def draw(self, x, y, scale=1.0):
 8        pass
 9
10    @abstractproperty
11    def readonly(self):
12      return self._x
13
14    def draw_doubled(self, x, y):
15        self.draw(x, y, scale=2.0)

これでDrawableから派生したクラスでdrawメソッド、readonlyプロパティが定義されていなければ TypeError が出るようになる。

PEP 3127: 数値リテラル及び文法

3.0では8進数は0開始でなく 0o or 0O 開始になり、2進数リテラル 0b or 0B がサポートされる。2.6では0開始8進数表記も使えるが 0o or 0O 開始も使えるし、 oct() 関数は0開始も文字列を返す。ただし、 future_builtins.oct は0o開始の文字列を返す。また、 int , long 関数は 0o0b 開始文字列も扱えるようになった。

PEP 3129: クラスデコレータ

1@foo
2@bar
3class A:
4  pass

1class A:
2  pass
3
4A = foo(bar(A))

と同義になる。

PEP 3141: 数値の型階層

3.0ではSchemeに習った数値の型階層が導入される。これらの型は numbers モジュールを通して 2.6 にバックポートされた。

  • Number
    • Complex
      • Real : floor()trunc() を定義
        • Rational : numeratordenominator を定義。実装は fractions モジュールで。
          • Integral

また、 math.trunc() も3.0からバックポートされた。fractionはこのように使う。

 1>>> from fractions import Fraction
 2>>> a = Fraction(2, 3)
 3>>> b = Fraction(2, 5)
 4>>> float(a), float(b)
 5(0.66666666666666663, 0.40000000000000002)
 6>>> a+b
 7Fraction(16, 15)
 8>>> a/b
 9Fraction(5, 3)
10>>> (2.5) .as_integer_ratio()
11(5, 2)
12>>> (3.1415) .as_integer_ratio()
13(7074029114692207L, 2251799813685248L)
14>>> (1./3) .as_integer_ratio()
15(6004799503160661L, 18014398509481984L)

その他の言語の変更

  • **kw 引数にPythonの辞書以外の UserDict のようなどんなマッピングでも使えるようになった。
  • next(iterator, [default]) メソッド追加。見ての通りiteratorの次の要素を、なければdefaultを返す。
  • タプルに index()count() が追加された(listと同じ)。
  • 組み込み型のsliceのサポートが向上。
  • プロパティが getter , setter , deleter というデコレータをサポート。
 1class C(object):
 2    @property
 3    def x(self):
 4        return self._x
 5
 6    @x.setter
 7    def x(self, value):
 8        self._x = value
 9
10    @x.deleter
11    def x(self):
12        del self._x
13
14class D(C):
15    @C.x.getter
16    def x(self):
17        return self._x * 2
18
19    @x.setter
20    def x(self, value):
21        self._x = value / 2
  • setintersection(), intersection_update(), union(), update(), difference(), difference_update() が引数に複数のiterableを取れるようになった。
  • 浮動小数点に関する様々な改良が追加された。
  • 親クラスから __hash__() を継承したクラスで __hash__ = None とすることでhashableでないことを示せる。
  • Exception のインタフェースが変更。 message 属性がdeprecated。
  • GeneratorExit の親クラスが Exception から BaseException に変更
  • Generatorオブジェクトに gi_code 属性を追加。
  • compile() 関数でキーワード引数が取れるようになった。
  • complex コンストラクタでカッコにかこわれている数値がOKになった。 complex('(3+4j)')
  • string.translate() に変換テーブルとして None を渡せるようになった。 None は恒等変換として扱われる。
  • dir() 組み込み関数が、引数に渡されたオブジェクトの __dir__() メソッドを見るようになった。 __dir__() は文字列のlistを返す必要がある。これにより __getattr__()__getattribute__() で、擬似属性みたいなのを実現しているクラスでもそのリストが返せる。
  • instance methodオブジェクトで im_self__self__im_func__func__ でも参照できるようになった。
  • class文内で locals() を使った場合、自由変数(クラスの属性でない変数)は含まれなくなった。

新規、改善、非推奨モジュール

  • 3.0警告モードで以下のモジュールは非推奨。

    • audiodev, bgenlocations, buildtools, bundlebuilder, Canvas, compiler, dircache, dl, fpformat, gensuitemodule, ihooks, imageop, imgfile, linuxaudiodev, mhlib, mimetools, multifile, new, pure, statvfs, sunaudiodev, test.testall, and toaiff.
  • asyncore , asynchat でメンテ再開。多くのパッチやバグフィックスが含まれた。

  • cgi でクエリ文字列付きPOSTリクエストにおいてクエリ文字列が解釈されるようになった。(/add.py?category=1へのPOSTなど)

  • parse_qsparse_qslcgi モジュールから urlparse モジュールへ。 cgi でもまだ使えるが PendingDeprecationWarning が出る。

  • cmathモジュールが改良された。

  • collections.namedtuple が新しく定義。

 1>>> var_type = collections.namedtuple('variable',
 2...             'id name type size')
 3# Names are separated by spaces or commas.
 4# 'id, name, type, size' would also work.
 5>>> var_type._fields
 6('id', 'name', 'type', 'size')
 7
 8>>> var = var_type(1, 'frequency', 'int', 4)
 9>>> print var[0], var.id           # Equivalent
101 1
11>>> print var[2], var.type          # Equivalent
12int int
13>>> var._asdict()
14{'size': 4, 'type': 'int', 'id': 1, 'name': 'frequency'}
15>>> v2 = var._replace(name='amplitude')
16>>> v2
17variable(id=1, name='amplitude', type='int', size=4)
  • collections.dequemaxlen 引数を取れるようになった。maxlenを超えて要素を追加すると自動で先頭が破棄される。

  • datetime モジュールの strftime() で’%f’が扱えるようになった。

  • decimal モジュールで exp() , log10() が追加、また as_tuple()sign,digits,exponent をフィールドとするnamed tupleを得られる。

  • difflib.SequenceMathcer クラスが a,b,size 属性をもったnamed tupleを返すようになった。

  • ftplib でタイムアウトが設定できるようになった。

  • glob.glob() がunicodeファイル名を返すようになった。

  • gopherlib 削除。

  • heapq モジュールに merge(iter1, iter2, ...)heappushpop(heap, item) が追加。

  • httplib.HTTPConnection でタイムアウトが設定できるようになった。

  • inspect モジュールの関数の多くがnamed tupleを返すようになった。

  • itertools に多数の関数を追加。

    • izip_longest(iter1, iter2, …[, fillvalue])
    • product(iter1, iter2, …, [repeat=N])
    • combinations(iterable, r)
    • permutations(iter[, r])
    • chain(*iterables) は新しく itertools.chain.from_iterable(iterable) とも書けるように。
  • math モジュールに関数を追加。

    • isinf() ,isnan(): 無限とNaNを判別。
    • copysign() : IEEE 754のサインビットをコピー。 math.copysign(1, -0.0)-1.0
    • factorial() :階乗を計算。
    • fsum() : iterableの数値を足すが、出来る限り精度を失うことを避ける。
    • acosh(), asinh() ,atanh()
    • log1p()
    • trunc() : 0の方向へ丸める。
  • MmeWritermimify モジュールが非推奨に。 email パッケージを使うこと。

  • md5 モジュールが非推奨に。 hashlib を使うこと。

  • mmap オブジェクトで文字列検索を行う rfind(),find() メソッドを追加。

  • operator モジュールに methodcaller() 関数追加。

1>>> # Equivalent to lambda s: s.replace('old', 'new')
2>>> replacer = operator.methodcaller('replace', 'old', 'new')
3>>> replacer('old wine in old bottles')
4'new wine in new bottles'
  • operator.attrgetter でドットを含む名前が取れるように。
1>>> inst_name = operator.attrgetter(
2...        '__class__.__name__')
3>>> inst_name('')
4'str'
5>>> inst_name(help)
6'_Helper'
  • os.walk()followlinks=False パラメータ追加。 True にセットされるとシンボリックリンクをたどる。無限ループ注意。
  • os.path.splitext() でドット始まりのファイル名の扱いが変更に。 ('.ipython', '') から ('', '.ipython')
  • os.path.relpath(path, start='.') が追加。 start からの相対パスを返す。
  • pdb に新しいコマンド run を追加。
  • posixfile モジュールが非推奨に。 fcntl.lockf() を変わりに使うこと。
  • pickletoolsoptimize() 関数追加。
  • popen2 モジュールが非推奨に。 subprocess モジュールを使うこと。
  • random モジュールの Random オブジェクトが32-bitと64-bit環境間でpickleできるようになった。また triangular(low,hight,mode) 関数追加。
  • re モジュールで長い正規表現を行うとき、シグナルをチェックするようにしたので、時間のかかる検索に割り込まれても大丈夫になった。また、正規表現は正規表現専用の仮想マシンコードの変換されるがベリファイアがなかったので、不正なコードを実行される可能性があった。今回ベリファイアを追加。
  • rgmimg モジュール削除。
  • sched.scheduler インスタンスに queue 属性追加。この属性は (time,priority,action,argument) から構成されるnamed tupleのリストを返す。
  • select モジュールがepoll,kqueueシステムコールのラッパ関数を導入。
  • sets モジュールが非推奨に。組み込み set, frozenset を使うこと。
  • sha モジュール非推奨。 hashlib モジュールを使うこと。
  • shutil.copytree()ignore=callable 引数追加。 ignore でコピーしないファイルリストを返すことが出来る。また ignore_patterns 関数を使い、以下のようにもできる
1shutil.copytree('Doc/library', '/tmp/library',
2                ignore=shutil.ignore_patterns('*~', '.svn'))
  • smtplib でSMTP over SSL, LMTPをサポート。

  • subprocess.Popen オブジェクトに terminate(), kill(), send_signal() 追加。

  • sys に様々な追加。

    • float.h 由来の情報を持った float_info オブジェクト。
    • .pyc.pyo を作るか制御する dont_write_bytecode
    • オブジェクトの使用メモリを取得する sys.getsizeof() 。組み込みオブジェクトは正しい数値を返す。
    • sys.getprofile() , sys.gettrace()
  • telnetlib でタイムアウトが設定できるようになった。

  • tempfile.NamedTemporaryFiledelete=False を指定することで自動削除しなくできる。

  • tempfile.SpooledTemporaryFile を追加。メモリ上に確保し、メモリにのらなくなるとファイルに書き出す。

  • test.test_supportwith 文を使った関数を提供する。

  • textwrap モジュールで空白文字を保持するため drop_whitespace=False を指定できるようになった。

  • timeit モジュールで文字列だけでなくcallableも受け取れるようになった。

  • タートルグラフィックを扱う turtle モジュールにかなりの改良。

  • urllib でタイムアウトを設定できるようになった。

  • zipfile.ZipFileextract()extractall(), open(), read() を追加。

  • ASTを扱う ast モジュール追加

  • JSONを扱う json モジュール追加

  • Property Listを扱う plistlib モジュール追加。

  • OpenSSL の上に構築された、 ssl モジュール追加。 socket モジュールのSSLサポートはまだ使えるが、3.0では削除される予定。このモジュールを使うためには、通常通りTCPコネクションを張り、 ssl.wrap_socket() 関数に渡す。

  • 3.0由来の組み込み関数を含む future_builtins モジュールを追加。

    • ascii : repr() と同義。3.0では repr() はunicode, ascii() はASCII byte文字列を返す。
    • filter,map : 2.xではリストを, 3.0ではイテレータを返す。
    • hex, oct : __hex__()__oct__() ではなく、 __index__() をよびその結果を変換する。

Python2.6への移行

  • ハッシュできないクラスは __hash__=None を行うべき。
  • collection.deque.__init__() は、iterableの要素を追加する前に自身の内容をクリアする。これは list.__init__() と同じ。
1>>> from collections.deque
2>>> a = deque([1,2,3])
3>>> deque.__init__(a, [4,5,6])
4>>> a
5# python2.6 => deque([4,5,6])
6# python2.5 => deque([1,2,3,4,5,6])
  • object.__init__() はこれまで任意の引数を渡すことができ、これらの引数は無視していた。2.6では余計な引数を渡すと TypeError がでる。
  • Decimal コンストラクタは前後に空白があっても大丈夫に。以前はInvalidOperation例外がでていた。一方、 Context.create_decimal() は余分な空白があると ConversionSyntax 例外が出るようになった。
  • __import__ で間違ってファイルパスを渡すと、指定したファイルをimportしていたが、これは決してそのように意図したわけではない。今はこのようなケースをチェックし、 ImportError を出すようになった。
  • C API: PyImport_ImportPyImport_ImportModule のデフォルトが相対importでなく完全importになった。これは他のモジュールをimportするC拡張に影響する。
  • socket.error 例外は IOError を継承するようになったので、 StandardError のサブクラスになった。
  • xmlrpclibdatetime.datedatetime.time を自動的に xmlrpclib.DateTime に変換しなくなった。なぜならこの挙動は全てのアプリケーションにとって必ずしも有用とは限らないからだ。 xmlrpclib を使っているコードは datetime インスタンスを変換すべきである。
  • 3.0警告モード: Exception クラスにスライシングおよびインデックスアクセスすると警告。
  • 3.0警告モード: 2つの辞書や、比較メソッドを実装していないオブジェクト間で比較演算子を適応すると警告。ただし、 dict1 == dict2 は動く。

前のエントリー で簡易S式パーサを re.Scanner で作ったのですが、まぁ個人的にまとめておいたほうが後々使えるだろう、ということでライブラリにまとめました。ダンパもついているので、S式の読み込みの他、PythonオブジェクトをS式で出力することができます。

実装には引き続き re.Scanner を活用しています。おかげで短い行数でキレイにかけているのではないかと。

ダウンロード

simplesexp.py

ソースはこんな感じ(テストのぞく)。

  1import re, sys
  2from unicodedata import east_asian_width
  3
  4try:
  5  from re import Scanner
  6except ImportError:
  7  from sre import Scanner
  8
  9class ParseError(StandardError): pass
 10
 11class Ident(unicode):
 12  def __repr__(self):
 13    return "Ident(%s)"%unicode.__repr__(self)
 14
 15class Symbol(unicode):
 16  def __repr__(self):
 17    return "Symbol(%s)"%unicode.__repr__(self)
 18
 19class Pair(list): 
 20  def __repr__(self):
 21    return "Pair(%s)"%list.__repr__(self)
 22
 23class Token(object):
 24  def __init__(self, value, pos):
 25    self.value = value
 26    self.pos   = pos
 27  def __repr__(self):
 28    return repr(self.value)
 29
 30class Binding(object):
 31  def __init__(self, dct):
 32    self.dct = dict(((k, k.__class__), v) for k,v in dct.iteritems())
 33  __contains__ = lambda self, key: (key, key.__class__) in self.dct
 34  __getitem__  = lambda self,key:  self.dct[(key, key.__class__)]
 35
 36default_binding = {"#t":True, "true":True, "#f":False, "false":False, "nil":None, "dict":Ident(u'alist->hash-table')}
 37
 38class Reader(object):
 39  PAREN = {"]":"[", ")":"("}
 40  def __init__(self, binding=None, symbol_marker="'", use_dict=True):
 41    self.binding = binding or default_binding
 42    self.symbol_marker = symbol_marker
 43    self.use_dict = use_dict
 44
 45  def read(self, value):
 46    self.result = []
 47    self.paren_stack = []
 48    self.source = value
 49    self.pos = 0
 50    self.scanner = Scanner([
 51      (r"\s+", self("skip")),
 52      (r";[^\n]*\n", self("skip")),
 53      (r""""(((?<=\\)")|[^"])*((?<!\\)")""", self("str")),
 54      (r"(\(|\[)", self("open")),
 55      (r"(\)|\])", self("close")),
 56      (r"(([\d]+|(((\d+)?\.[\d]+)|([\d]+\.)))e[\+\-]?[\d]+)|(((\d+)?\.[\d]+)|([\d]+\.))", self("number")),
 57      (r"\-?((0x[\da-f]+)|(0[0-7]+)|([1-9][\d]*)|0)[l]?", self("number")),
 58      (r"""%s([^\(\[\)\]\s"]+)"""%self.symbol_marker, self("symbol")),
 59      (r"""([^\(\[\)\]\s"]+)""", self("ident")),
 60      (r"""".*""", self("unterm_str")),
 61      (r".*", self("unknown_token"))
 62    ], re.M|re.S|re.I)
 63    self.scanner.scan(self.source)
 64    if self.paren_stack:
 65      self.raise_error("missing closing parenthesis.")
 66    return self.parse(self.result)
 67
 68  def append(self, v):
 69    self.last().append(Token(v, self.pos))
 70
 71  def __call__(self, name):
 72    def _(scanner, s):
 73      self.pos += len(s)
 74      return getattr(self, name)(s)
 75    return _
 76
 77  def unknown_token(self,s): self.raise_error("unknown token: %s"%s)
 78  def skip(self, _): pass
 79  def open(self, s):
 80      new_lst = []
 81      self.last().append(new_lst)
 82      self.paren_stack.append([s, new_lst])
 83  def close(self, s):
 84      if not self.paren_stack:
 85        self.raise_error("missing opening parenthesis.")
 86      if self.PAREN[s] != self.paren_stack.pop()[0]:
 87        self.raise_error("missing closing parenthesis.")
 88  def str(self, s): self.append(eval('u""'+s+'""'))
 89  def unterm_str(self, s): self.raise_error("unterminated string literal.")
 90  def number(self, s): self.append(eval(s))
 91  def symbol(self, s): self.append(Symbol(s[1:]))
 92  def ident(self, s): 
 93    if s in self.binding:
 94      self.append(self.binding[s])
 95    else:
 96      self.append(Ident(s))
 97
 98  def last(self):
 99    if self.paren_stack:
100      return self.paren_stack[-1][1]
101    else:
102      return self.result
103
104  def parse(self, rs):
105    def is_ident(value, expected):
106      return getattr(value,"value", None) == Ident(expected)
107    def is_pair(rs):
108      return getattr(rs, "__len__", lambda :0)()==3 and is_ident(rs[1], u".")
109
110    if isinstance(rs, list):
111      if not len(rs):
112        return []
113      elif self.use_dict and is_ident(rs[0], u"alist->hash-table"):
114        if len(rs) != 2:
115          self.raise_error("alist->hash-table: expected 1 arguments, got %d."%(len(rs)-1), rs[0].pos)
116        if not all(is_pair(a) for a in rs[1]):
117          self.raise_error("alist->hash-table: aruguments must be alist", rs[0].pos)
118        return dict((self.parse(i[0]), self.parse(i[2])) for i in rs[1])
119      elif len(rs)!=3 and any(is_ident(t, u".") for t in rs):
120        self.raise_error('illegal use of "."', rs[0].pos)
121      elif is_pair(rs):
122        parsed = self.parse(rs[2])
123        if not isinstance(rs[2], list):
124          return Pair([rs[0].value, parsed])
125        if isinstance(parsed, Pair):
126          return Pair([rs[0].value, parsed])
127        elif isinstance(parsed, list):
128          return [rs[0].value]+parsed
129        else:
130          return [rs[0].value, parsed]
131      else:
132        return map(self.parse, rs)
133    else:
134      return rs.value
135
136  def raise_error(self, msg="parse error", pos=None, range=3):
137    pos = pos or self.pos
138    lines = self.source.split("\n")
139    curline = self.source[:pos].count("\n")
140    linepos = pos - len("\n".join(lines[:curline]))
141    buf = ["\n"]
142    for i in xrange(max(0, curline-range), curline+1):
143      buf.append("% 5d: %s"%(i+1, lines[i]))
144    width = 7 + sum(east_asian_width(c) == 'W' and 2 or 1 for c in unicode(lines[i]))
145    buf.append("%s~"%(" "*width))
146    buf.append("line %d, %d: %s"%(curline+1,linepos, msg))
147    raise ParseError(("\n".join(buf)).encode(sys.stderr.encoding))
148
149class Dumper(object):
150  def __init__(self, binding=None ,symbol_marker="'"):
151    binding = binding or default_binding
152    self.binding = Binding(dict(zip(binding.values(), binding)))
153    self.symbol_marker = symbol_marker
154
155  def dump(self, obj):
156    result = self.to_sexp(obj, [])
157    if isinstance(result, list) and len(result) and result[0]=="(":
158      result = result[1:-1]
159    return u" ".join(result)
160
161  def to_sexp(self, obj, result):
162    ap = result.append
163    tos = lambda v: self.to_sexp(v, result)
164    if isinstance(obj, Pair):
165      ap("(")
166      tos(obj[0])
167      ap(" . ")
168      tos(obj[1])
169      ap(")")
170    elif isinstance(obj, (tuple, list)):
171      ap("(")
172      map(tos, obj)
173      ap(")")
174    else:
175      if isinstance(obj, dict):
176        ap("( alist->hash-table ")
177        tos([(k, Ident(u"."), v) for k,v in obj.items()])
178        ap(" ) ")
179      elif obj in self.binding:
180        ap(unicode(Ident(self.binding[obj])))
181      elif isinstance(obj, Symbol):
182        ap(u"'%s"%unicode(obj))
183      elif isinstance(obj, (Ident,int, float, long)):
184        ap(unicode(obj))
185      else:
186        s = unicode(repr(obj)).decode("unicode_escape")
187        m = re.match(r"""^[u|r]?["|'](.*)["|']$""", s, re.M|re.S)
188        if m:
189          s = m.group(1)
190        ap("\"%s\""%s.replace('"','\\"').replace("\\'","'"))
191    return result
192
193dumper = Dumper()
194read = Reader().read
195dump = dumper.dump

概要

特徴は

  • 辞書を定義できる
  • ドット対に対応
  • 識別子に対して、任意のバインディングを指定できる
  • わりとちゃんとエラー表示される
  • シンボル表記、数値表記(python表記)に対応

といった当たりでしょうか。具体的にはテストコードを見てもらうと分かるかと。

 1(あああ hoge->fuga123 (1 . (2 . 3)) "hoge\\"hoge" ;comment2 
 2foo "aaa" #t <= 'foo 
 3"hogehoge
 4foo
 5" (5 . (6 .()))
 6)
 7(dict (
 8  ("いいい" .
 9    (alist->hash-table (
10      ("a-1" . "vvv")
11      ("a-2" . (
12        hoge foo bar 
13      ))
14    )))
15))
16(10 1L -45 010 0x10 -10 -0x10 3.14 10. .001 1e100 3.14e-10 0e0)
17; comment3 ()(
18
19""")

という感じのS式が

1[
2  [Ident(u'あああ'), Ident(u'hoge->fuga123'), Pair([1, Pair([2, 3])]), u'hoge"hoge',
3  Ident(u'foo'), u'aaa', True, Ident(u'<='), Symbol(u'foo'),
4  u'hogehoge\nfoo\n', [5,6]],
5  {u'いいい': 
6    {u'a-1': u'vvv', 
7    u'a-2': [Ident(u'hoge'), Ident(u'foo'), Ident(u'bar')]}},
8  [10, 1L, -45, 010, 0x10, -10, -0x10, 3.14, 10., .001, 1e100, 3.14e-10, 0e0]
9]

となります。 IdentSymbol はunicodeのサブクラス、 Pair はリストのサブクラスになっているので、違和感なく使えると思います。

また、 alist->hash-table で辞書が作れます。デフォルトで dictalist->hash-table をバインドしてますので、 dict でも辞書が作れます。この機能はオンオフ切り替えも可能です。

その他、 #tTrue などSchemeっぽくデフォルトバインディングが用意してあります。もちろん、バインディングは変更可能ですのでCLっぽくもできます。


と、こんな感じです。一番便利なのはやっぱり辞書ですかねえ。なので、YAML,JSONで書いてる設定ファイルをS式で置き換え・・・なんてことができるかもしれません。