Skip to content

Instantly share code, notes, and snippets.

@Shinsuke-Abe
Created October 6, 2011 14:30
Show Gist options
  • Select an option

  • Save Shinsuke-Abe/1267529 to your computer and use it in GitHub Desktop.

Select an option

Save Shinsuke-Abe/1267529 to your computer and use it in GitHub Desktop.
pysec for Python3k with Japanese comment
# coding: utf-8
'''
PersecのPython移植版であるpysecのPython3kバージョンです。
勉強のための写経/移植なので、日本語でのコメント付きとなってます。
以下の記事の内容を参考に。
http://www.valuedlessons.com/2008/02/pysec-monadic-combinatoric-parsing-in.html
コメントを見る限りpublic domainだと明言してあるので問題ないとは思いますが。。。
Created on 2011/10/01
@author: mao
'''
# ベースのライブラリを便宜上含んでいる
def Record(*props):
"""
いろいろな物の基底クラスを作る。
ベースになるクラスはタプルの拡張で、
オブジェクト生成時に渡した引数の値が、
この関数に渡されたプロパティに紐づけられる。
"""
class cls(RecordBase):
pass
cls.setProps(props)
return cls
class RecordBase(tuple):
"""
いろんなクラスのベースになる。タプルの拡張。
Propで渡された順番通りに属性を追加していってるから、
オブジェクト生成時に渡した引数の順番が属性と紐づけられて、
プロパティとして取得できる。
"""
PROPS = () # クラス変数 -> タプル
def __new__(cls, *values):
"""
RecordBaseオブジェクトのコンストラクタ
"""
if cls.prepare != RecordBase.prepare:
# 継承クラスでprepareがオーバライドされている場合はそちらを使う
values = cls.prepare(*values)
return cls.fromValues(values)
@classmethod
def fromValues(cls, values):
"""
引数に渡された値から新たなタプルオブジェクトを返す。
"""
return tuple.__new__(cls, values)
def __repr__(self):
"""
オブジェクトの印字可能な文字列を示す。
"""
return self.__class__.__name__ + tuple.__repr__(self)
@classmethod
def prepare(cls, *args):
"""
オーバライドして使用する
"""
return args
@classmethod
def setProps(cls, props):
"""
クラス変数PROPSへのsetter。
引数に渡されたプロパティに従って、RecordBaseクラスにクラス変数とsetterおよびgetterを作成する。
(setterとgetterの作成は別メソッド)
クラス継承時に指定したプロパティ名の順に作成され、
コンストラクタの引数に指定してprepareで作成されるタプルの値とひもづく。
"""
for index, prop in enumerate(props): # enumerate -> シーケンスにインデックスつけて取り出す関数。地味に便利。
cls.setProp(index, prop)
cls.PROPS = props
@classmethod
def setProp(cls, index, prop):
"""
指定されたインデックスと内容でsetterとgetterを作成し、クラス属性としてセットする。
"""
getter_name = prop
setter_name = "set" + prop[0].upper() + prop[1:]
setattr(cls, getter_name, cls.makeGetter(index, prop)) # クラスに属性をセットする。 -> 関数やメソッドも属性
setattr(cls, setter_name, cls.makeSetter(index, prop))
@classmethod
def makeGetter(cls, index, prop):
"""
指定したインデックスの属性を取得するgetterを作成する
"""
# メソッドの引数に渡しているpropの意義は?
return property(fget = lambda self: self[index]) # getterにselfを引数に渡してself[index]を返す関数をセットする。
@classmethod
def makeSetter(cls, index, prop):
"""
setterメソッドを作成して返す
"""
# メソッドの引数に渡しているpropの意義は?
def setter(self, value):
values = (value if current_index == index else current_value
for current_index, current_value in enumerate(self))
return self.fromValues(values)
return setter
class ByteStream(Record("bytes", "index")):
"""
バイト列と現在のインデックスを持つRecordBaseの継承クラス。
ByteStreamってあるけど、Python3だと文字列とバイト列は明確に区別されるから、
StringStreamって変えた方がいいかもしれない。
"""
@classmethod
def prepare(cls, bytes, index=0):
"""
RecordBaseクラスのprepareメソッドのオーバライド。
引数で渡されたbytesとindexのタプルを返す。
"""
return (bytes, index)
def get(self, count):
"""
現在位置からcountで渡された文字数分だけ取得する。
戻り値は取得した文字列と、残りのバイトストリームのタプル。
ちなみに、countの結果すでに最後に達したり、
コンテキストでFalseになったりする場合はそのままのストリームを返す。
"""
start = self.index
end = start + count
bytes = self.bytes[start: end]
return bytes, (self.setIndex(end) if bytes else self)
def make_decorator(func, *dec_args):
"""
デコレータを作るための関数。具体的にはparser関数をデコレータにしてる。
make_decorator(parser)ってなるように使ってる。
parser関数の名前を持つdecorator関数を返す。
つまり、デコレートされる前の関数を引数にとってparser関数として機能する関数。
で、この関数を実行すると対象の関数の引数をとるdecorated関数を返すようになっている。
つまり、parserでデコレートした関数を実行すると、
parser(デコレートされた関数, 対象関数の引数(名前なし), 対象関数の引数(名前あり), make_docorator時に指定した引数)
を実行する関数が返ってくるようになる。
"""
def decorator(undecorated):
def decorated(*args, **kargs):
return func(undecorated, args, kargs, *dec_args)
decorated.__name__ = undecorated.__name__
return decorated
decorator.__name__ = func.__name__
return decorator
decorator = make_decorator
class Monad:
"""
モナドクラス。
プログラムの意味を中心に記載できるようにして、お約束的なコードを裏に隠すような実装にすること。
デザインパターンの一種っぽい。
jQueryの「.」とかS2JDBCとかの色々裏でやってることを隠そうね、ということ。
"""
def bind(self, func):
"""
必ずオーバライドして使うこと
"""
raise NotImplementedError
@classmethod
def unit(cls, val):
"""
必ずオーバライドして使うこと
"""
raise NotImplementedError
@classmethod
def lift(cls, func):
"""
lift引数に渡された関数を引数に持つunitメソッドを返す。
ちなみに、このunitメソッドの引数はvalで、これは引数に渡した関数。
つまり、unitメソッドの引数をliftの引数でラップしてから返す関数。
"""
return (lambda val : cls.unit(func(val)))
# 多分オーバライドしなくても使える
def __rshift__(self, bindee):
"""
右シフトをオーバーライドしている。
こうすることで、「>>」を使ってbindすることができる。
機能面というより、いわゆるモナド則ってやつへの対応?
"""
return self.bind(bindee)
def __and__(self, monad):
"""
ビット単位のandをオーバーライドしている。
これもrshiftと同じく機能面というよりかはmonadのパターンとしての拡張?
こうすることで、「&」を使ってshoveすることができる。
"""
return self.shove(monad)
# 使いやすかったり、もっと効率的なものがあるなら、オーバライドする
def shove(self, monad):
"""
shoveはputとかpushとかとおんなじ意味。
つまり、&でmonadを連結するための処理。
呼ばれたmonadに対して&で新たなmonadを連結する。
"""
return self.bind(lambda _ : monad)
class StateChanger(Record("changer", "bindees"), Monad):
"""
現在の状態を変更するためのクラス。
changerはparserデコレータをつけた変換関数で、bindeeはタプルとなる。
Monadを多重継承しているからbindとunitをオーバーライドしている。
つまり、「>>」でbind対象を追加し、「&」で新たなchangerをshoveできる。
※shoveの実態はchangerのmonadをbindしてるだけだから、
 changerをbindee要素として追加してるようなもん?
"""
@classmethod
def prepare(cls, changer, bindees = ()):
return (changer, bindees)
def bind(self, bindee):
"""
バインドする。
実行時ではないので速度的には遅いかも?
"""
return self.setBindees(self.bindees + (bindee,))
def __call__(self, state):
"""
StateChangerクラスを呼び出し可能にしている。
"""
return self.run(state)
def run(self, state0):
"""
オブジェクトがコールされたときの実行メソッド
"""
# changer属性がコールできる関数なら、引数のstate0を渡してコールする。
value, state = self.changer(state0) if callable(self.changer) else self.changer
state = state0 if state is None else state
# 配下のバインド属性全てに対してrunメソッドを再帰的に実行する。
for bindee in self.bindees:
value, state = bindee(value).run(state)
return (value, state)
@classmethod
def unit(cls, value):
"""
unitメソッドのオーバーライド。
valueにchangerをとって、bindeeが空のStateChangerクラスを作成して返す。
つまり、liftメソッドは引数をchangerとしたStateChangerクラスを呼ぶ関数を作って返すことになる。
StateChangerがcallableだからやれること。
"""
return cls((value, None))
### こっからはMonadを利用したパーサの実装 ###
class ParserState(Record("stream", "position")):
"""
対象ストリームと現在位置を保持するクラス。
streamはByteStreamクラスが想定。
"""
@classmethod
def prepare(cls, stream, position = 0):
"""
属性(ストリームと現在位置)のタプルを返す。
"""
return (stream, position)
def read(self, count):
"""
countで指定した数値分文字を読み込む。
実際には保持しているstream属性のgetメソッドが呼ばれる。
戻り値は、取得した文字列と位置を位置を変更したParserStateオブジェクト。
"""
collection, stream = self.stream.get(count)
return collection, self.fromValues((stream, self.position + count))
class Parser(StateChanger):
"""
パーサ本体。
StateChangerを継承している。だからこいつもMonad。
"""
def parseString(self, bytes):
"""
引数で渡された文字列をパースする。
ByteStreamが想定だけど、Python3では文字列とbyteは別物なので引数の名前は変えた方がいいかも?
"""
return self.parseStream(ByteStream(bytes))
def parseStream(self, stream):
"""
渡されたストリームをパースするんだ。
"""
# ストリームの状態をオブジェクト化する。
state = ParserState(stream)
# パースを実行する
# このとき、Paserに指定された変換関数が使われるのと、
# bindに追加された関数が再帰的に、順次適用される。
value, state = self.run(state)
return value
class ParseFailed(Exception):
"""
パース失敗を示す例外クラス。
"""
def __init__(self, message, state):
self.message = message
self.state = state
Exception.__init__(self, message)
@decorator
def parser(func, func_args, func_kargs):
"""
変換関数をParserオブジェクトにするためのデコレータ。
make_decorator関数のところで説明した通りparserでデコレートした関数を実行すると、
parser(デコレートされた関数, 対象関数の引数(名前なし), 対象関数の引数(名前あり), make_docorator時に指定した引数)
を実行する関数が返ってくるようになる。
中でchanger関数を定義してるけど、これは引数に渡した変換関数の名前に変えられてParserのchangerにセットされる。
changer関数は必ずstateを引数に持つ。
changerになる関数はstateと他の引数を持つが、
parserでデコレートしたタイミングでそれ以外をセットしてstateのみ指定して実行できるように入れ替えられる。
パーサーにとって変換する引数は変わらないけど状態は変わるわけだから、部分適用してるってこと?
だから、parserでデコレートした関数からパーサを作成するときは、
まずは状態以外の変換に必要な情報を与えてあげればいい。
そして、状態がはっきりした時に状態を与えて実行すれば、後は良きに計らってくれる。
"""
def changer(state):
return func(state, *func_args, **func_kargs)
changer.__name__ = func.__name__
return Parser(changer)
##### コンビネータ的な関数群 #########
# コンビネータってのは自由変数を含まないラムダ式らしい。。。
# シンプルなパーサを組み合わせてコンビネーションさせるためのもの、っぽい
@parser
def tokens(state0, count, process):
"""
トークンを一個区切るためのパーサを生成する。
指定した文字数分ParserStateのストリームから文字を取り出して、
processで渡した関数に取りだした文字を引数として渡す。
processはOKかどうかを示す値と処理した文字列を返すようになっていて、
tokensの戻り値は、処理した文字列とトークンを読みこんだ後のParserStateオブジェクト。
"""
tokens, state1 = state0.read(count)
passed, value = process(tokens)
if passed:
return (value, state1)
else:
raise ParseFailed(value, state0)
def read(count):
"""
countで指定した文字数読み込みをするようなtokens関数を返すんだ。
countとprocessを指定してる。
単純な読み込みだからprocessのpassedはTrueを返すようになってて、
処理した文字列も単に入力されたものをそのまま返すようになってる。
"""
return tokens(count, lambda values : (True, values))
@parser
def skip(state0, parser):
"""
値をスキップする
"""
# ストリームを引数に指定したパーサでパースする。
value, state1 = parser(state0)
# スキップするので、値は捨てて状態のみ返す。
return (None, state1)
@parser
def option(state, default_value, parser):
"""
デフォルト値付きのパースを行う。
パースできる場合はそのままパースして、
パースに失敗した場合は、初期値と現在の状態を返す。
"""
try:
return parser(state)
except ParseFailed as failure:
if failure.state == state:
# 状態に変化がないかを確認
# 変化がなければデフォルト値と現在の状態を返す。
return (default_value, state)
else:
raise
@parser
def choice(state, parsers):
"""
複数のパーサでパースを行う。
どれかに引っかかった時点でその時のパース結果を返す。
"""
for parser in parsers:
try:
return parser(state)
except ParseFailed as failure:
# パース失敗のときは、状態チェックして、
# 変わっていたら処理を止める。
# 変わっていなかったら、次のパーサ。
if failure.state != state:
raise failure
# 当然、ループを抜けてしまった場合は何にもパースできなかったんだから、
# パースエラーをスローすることにする。
raise ParseFailed("no choices were found", state)
@parser
def match(state0, expected):
"""
一致するかどうかを調べる。
"""
actual, state1 = read(len(expected))(state0)
if actual == expected:
return actual, state1
else:
raise ParseFailed("expected %r" % (expected,), state0)
def between(before, inner, after):
"""
beforeとafterで挟まれたinnerをマッチさせるためのパーサを作る。
復習:「&」はshoveで「>>」はbind
まず最初に開始のパーサーに対してinnerを返す関数をバインドする。
そうやってできたパーサに対して、
afterに対してあるパーサをバインドした関数をバインドする。
つまり、
before.bindee=((lamdba _: inner), (lambda value : after.bindee=((lamdba _:Parser(value, None)))))
見たいなのが出来上がる。
"""
return before & inner >> (lambda value: after & Parser.unit(value))
def quoted(before, inner, after):
"""
beforeとafterでくくられたinnerをマッチさせるためのパーサを作る。
beforeとafterを区切り文字としてマッチするParserを作ってbetweenに渡してるだけ。
"""
return between(match(before), inner, match(after))
def quoted_collection(start, space, inner, joiner, end):
"""
全体をクオートしたコレクションをマッチさせるためのパーサを作る。
startとendがクオートするための文字列。
その中身はspaceとinnerとjoinerで構成される。
innerは対象となる値でjoinerはCSVの「,」に相当する区切り文字。
spaceはその前後にある空白文字のこと。
で、それをbetweenに渡している。
"""
return quoted(start, space & sep_end_by(inner, joiner), end)
@parser
def many(state, parser, min_count = 0):
"""
おんなじパーサに対していくつマッチするか確認するためのパーサを作る。
min_countを指定して、いくつ以上マッチしなきゃいけない、っていう指定もできる。
"""
values = []
try:
while True:
value, state = parser(state)
values.append(value)
except ParseFailed:
if len(values) < min_count:
raise
return values, state
@parser
def group(state, parsers):
"""
複数のパーサグループに対してパースできるかどうか確認するためのパーサを作る。
parsersはパーサオブジェクトのシーケンスで、それを順繰りにパースしていくだけ。
"""
values = []
for parser in parsers:
value, state = parser(state)
values.append(value)
return values, state
def pair(parser1, parser2):
"""
parser1.bindee=(parser2.bindee=(Parser((value1, value2))) の順で実行されるパーサを作る。
"""
# return group((parser1, parser2))
return parser1 >> (lambda value1 : parser2 >> (lambda value2 : Parser.unit((value1, value2))))
@parser
def skip_many(state, parser):
"""
パーサがマッチしなくなるまでスキップする。
"""
try:
while True:
value, state = parser(state)
except ParseFailed:
return (None, state)
def skip_before(before, parser):
"""
先にスキップしてから、指定したパーサでパースするパーサを作る。
"""
return skip(before) & parser
@parser
def skip_after(state0, parser, after):
"""
指定したパーサでパースした後にスキップするパーサを作る。
"""
value, state1 = parser(state0)
_, state2 = after(state1)
return value, state2
@parser
def option_many(state0, first, repeated, min_count = 0):
"""
firstを初期値としてパースして、パースエラーが発生しなければrepeatedでパースするパーサを作る。
"""
try:
first_value, state1 = first(state0)
except ParseFailed:
if min_count > 0:
raise
else:
return [], state0
else:
values, state2 = many(repeated, min_count - 1)(state1)
values.insert(0, first_value) # 初期値をリストの最初に入れる。
return values, state2
### 以下の違いがよくわからない。。。
# parser separated and ended by sep
def end_by(parser, sep_parser, min_count = 0):
"""
sep_parserで区切られるパターン(sep_parserで終わる)のパーサを作る。
manyなので、最小個数を設定可能。
"""
return many(skip_after(parser, sep_parser), min_count)
# parser separated by sep
def sep_by(parser, sep_parser, min_count = 0):
"""
sep_parserで区切られるパターンのパーサを作る。
"""
return option_many(parser, skip_before(sep_parser, parser), min_count)
# parser separated and optionally ended by sep
def sep_end_by(parser, sep_parser, min_count = 0):
"""
sep_parserで区切られるパターン(sep_parserで終わる)のパーサを作る。
"""
return skip_after(sep_by(parser, sep_parser, min_count), option(None, sep_parser))
### char-specific parsing ###
def satisfy(name, passes):
"""
passesで指定された判定を満足するトークンパーサを作成する。
"""
return tokens(1, lambda char: (True, char) if passes(char) else (False, "not " + name))
def one_of(chars):
"""
引数で指定した文字列の1文字にマッチするかどうかを確認するパーサを返す。
"""
char_set = frozenset(chars)
return satisfy("one of %r" % chars, lambda char: char in char_set)
def none_of(chars):
"""
引数で指定した文字列のどの文字にもマッチしないことを確認するパーサを返す。
"""
char_set = frozenset(chars)
return satisfy("not one of %r" % chars, lambda char: char and char not in char_set)
def maybe_match_parser(parser):
"""
パーサが文字列だったらmatchパーサを返し、文字列でなければ引数のパーサをそのまま返す。
"""
return match(parser) if isinstance(parser, str) else parser
def maybe_match_parsers(parsers):
"""
maybe_match_parserのシーケンス版。
戻り値はタプルで返す。
"""
return tuple(maybe_match_parser(parser) for parser in parsers)
def many_chars(parser, min_count = 0):
"""
同じ文字のパターンが複数個続くことを示すパターン。
"""
return join_chars(many(parser, min_count))
def option_chars(parsers):
"""
空文字をデフォルト値に持つgroup_charsパーサを返す。
"""
return option("", group_chars(parsers))
def group_chars(parsers):
"""
parsersを全て適用するパターンを返す。
"""
return join_chars(group(maybe_match_parsers(parsers)))
#return join_chars(group(parsers))
def join_chars(parser):
"""
指定したパーサにパース結果をジョインするパーサを追加する。
"""
return parser >> Parser.lift("".join)
def while_one_of(chars, min_count = 0):
"""
指定した文字の中が幾つか続くパターン。(正規表現の[abc]みたいなイメージ)
"""
return many_chars(one_of(chars), min_count)
def until_one_of(chars, min_count = 0):
"""
指定した文字以外の文字がいくつか続くパターン。(正規表現の[!abc]みたいなイメージ)
"""
return many_chars(none_of(chars), min_count)
def char_range(begin, end):
"""
beginとendを整数に変換して、その間の整数を文字に変換して連結する関数。
"""
return "".join(chr(num) for num in range(ord(begin), ord(end)))
def quoted_chars(start, end):
"""
startとendでくくられたパターンのパーサを作る。
"""
assert len(end) == 1, "end string must be exactly 1 character"
return quoted(start, many_chars(none_of(end)), end)
# 1桁の数値を示すパターン
digit = one_of(char_range("0", "9"))
# 1桁以上の数字の列を示すパターン
digits = many_chars(digit, min_count=1)
# 空白文字として認知される文字のパターン
space = one_of(" \v\f\t\r\n")
# 複数文字続く空白文字のパターン
spaces = skip_many(space)
############# simplified JSON ########################
# HACK: json_choicesは相互に再帰的。
# jsonの値はテキストや数値やマップやコレクションのうちの一つで、それを後から定義できる。
json_choices = []
json = choice(json_choices)
# テキストはクオートされたいくつかの文字。
text = quoted_chars("'", "'")
# 正規表現「-?[0-9]+(\.[0-9]+)?」で表現されるもののソート。
# monadを見慣れないあなたのために。
# "parser >> Parser.lift(func)" というのは、
# "funcにパースを通った値を入れてくれ、でも新しいパーサを返してくれ"という意味。
number = group_chars([option_chars(["-"]), digits, option_chars([".", digits])]) >> Parser.lift(float)
# quoted_collection(start, space, inner, joiner, end)は、
# "startとendで囲まれているjoinerで区切られているinnerのリスト"を意味する。
# JSONは任意のホワイトスペースをたくさんおくことを許可しているから、
# たいてい、たくさんのスペースをおいてしまう。
joiner = between(spaces, match(","), spaces)
mapping_pair = pair(text, spaces & match(":") & spaces & json)
collection = quoted_collection("[", spaces, json, joiner, "]") >> Parser.lift(list)
mapping = quoted_collection("{", spaces, mapping_pair, joiner, "}") >> Parser.lift(dict)
# HACK: 相互に再帰的な終了。
json_choices.extend([text, number, mapping, collection]) # jsonパーサのbindeeにセット
############# simplified CSV ########################
def line(cell):
return sep_end_by(cell, match(","))
def csv(cell):
return sep_end_by(line(cell), match("\n"))
############# テスト ####################
print(json.parseString("{'a' : -1.0, 'b' : 2.0, 'z' : {'c' : [1.0, [2.0, [3.0]]]}}"))
print(csv(number).parseString("1,2,3\n4,5,6"))
print(csv(json).parseString("{'a' : 'A'},[1, 2, 3],'zzz'\n-1.0,2.0,-3.0"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment