左再帰的な文法PEG:5のPythonのパーサシリーズの父

オリジナルタイトル |左再帰PEG文法

著者 |グイド・ヴァンロッサム(Pythonの父)

翻訳 |次のエンドウ猫(「Pythonの猫」のパブリック著者)

免責事項 |この翻訳はに基づく学習の交換の目的である4.0-NC-SA BY CCライセンス。読みやすくするために、わずかな変化の内容。

私はつまずきで、それはそれを解決する時間です左再帰を何度か言及しています。スタックオーバーフローに起因する再帰下降パーサを使用して、左再帰と終了するプログラムが発生します。基本的な質問です。

[これは、このシリーズの私のPEGパート5です。その他の記事はを参照して、このディレクトリ ]

次の構文規則を考慮してください。

expr: expr '+' term | term

我々は単純断片再帰下降構文解析に翻訳する場合は、次のようになります。

def expr():
    if expr() and expect('+') and term():
        return True
    if term():
        return True
    return False

つまりexpr()呼び出すためにexpr()も呼び出しを開始、expr()始まった、というように......これが唯一のスタックオーバーフローで終了することができ、例外がスローされますがRecursionError

伝統的な救済策は、文法を書き換えることです。前回の記事で、私はそうしてきました。私たちは、このように書き換えた場合、実際には、上記の構文は、識別することができます。

expr: term '+' expr | term

私たちはパースツリーを生成するためにそれを使用する場合は、構文解析ツリーの形状は、このような私たちは、構文に追加するときのように壊滅的な結果につながることができ、異なるものになる'-'際にオペレータ(ので、同じではありません)。a - (b - c)(a - b) - c

これは、多くの場合、このようなパケットや反復などのPEGを解決するために、より強力な機能、することができ、我々は上記のルールを書き換えることができます:

expr: term ('+' term)*

実際には、これは現在PGENパーサジェネレータ(PGENと同じ問題を抱えている左再帰ルール)にPythonの構文で記述されています。

しかし、それはまだいくつかの問題があります:好きなので'+''-'、この演算子が(Pythonで)基本的に、我々はバイナリとして解決する場合a + b + c、このようなことが、私たちは分析の結果を通過しなければならない(基本的にリスト[「A」 、 '+'、 'B'を 、 '+'、 'C'])、 左再帰と同様ツリー(構文解析するように構成された[[ 'A'を、 '+ '、 'B']、 '+'、 ' C「])。

元の文法は、再帰テーブルvを残している。必要な関連性を、それゆえ、私たちはそのフォームで直接パーサを生成することができれば、それが良いだろう。私たちはすることができます!一つのファンはまた、実装が容易、数学的な証明が付属して、私には非常に優れた技術を指摘しました。私はここでそれを説明しようとするでしょう。

私たちは、入力を考えてみましょうfoo + bar + baz例として。私たちは、木が対応する解析を解析します(foo + bar)+ bazこれは、必要とexpr()「+」演算子(すなわち、最初の)の内側に対応する;及び第二の選択肢は「+」演算子(すなわち、秒)の頂部に対応する:3回再帰呼び出しを残し二つの選択肢(つまりterm)。

私はASCIIのスキルのデモンストレーションとしてこれを使用しますので、実際のチャートを描画するためにコンピュータを使用して得意おりませんので。

expr------------+------+
  |              \      \
expr--+------+   '+'   term
  |    \      \          |
expr   '+'   term        |
  |            |         |
term           |         |
  |            |         |
'foo'        'bar'     'baz'

アイデア(exprの中で願うことです)関数は、「オラクル」(翻訳:翻訳ないの背後にある予言、神託を)持っている、それは最初の選択肢(つまり、再帰呼び出しexprの())のいずれかを使用することを教えてくれる、いずれかの第二(すなわち、コール用語())。最初の呼び出しexprの()、「オラクル」にtrueを返す必要があり、2番目の(再帰)呼び出しで、それはまた、trueを返す必要がありますが、私たちが呼び出すことができるように、第三の呼び出しで、それは、falseを返す必要があります用語()。

コードでは、それは次のようにする必要があります:

def expr():
    if oracle() and expr() and expect('+') and term():
        return True
    if term():
        return True
    return False

どのように我々はそれが、この「オラクル」書くことができますか?それを試してみる......我々は、コールスタック式exprに記録されるように試みることができる()(再帰左)コール数、および以下の式と比較演算子の「+」の数。事業者の数は、コールスタックの深さよりも大きい場合、それはfalseを返す必要があります。

私はほとんどしたいsys._getframe()、それを達成するために、より良い方法があります:私たちは、コールスタックを逆にしてみましょう!

ここでの考え方は、私たちがコールでオラクルからfalseを返し、結果を保存することです。それは持っていますexpr()->term()->'foo'(これは、元返す必要がありますtermすなわち、解析木を'foo'上記のコードは単純にTrueを返します。しかし、このシリーズの第二の記事で、私はパースツリーを返す方法を実証しました。)簡単に達成するために、Oracleを書くために、それが必要最初の呼び出しが戻るとfalse--とき、前方スタックを確認するか、振り返る必要はありません。

その後、我々は再び呼ばれるexpr())、今回のOracleはtrueを返しますが、我々は(exprをしていない再帰呼び出しを残したが、結果は前に一度呼び出されたときに保存置き換えます。ナ見て、期待される'+'演算子とそれに続くtermそこには、私たちは次のようになりますfoo + bar

私たちは、このプロセスを繰り返し、その後、物事は非常に明確に見える:今回はパースツリーの完全な表現を得るでしょうし、それが正しい左再帰((FOO +バー)+バズ)です。

私たちは、再びプロセスを繰り返し、この時点では、Oracleはtrueを返し、結果が利用可能になる前に、次のステップは、「+」演算子ではなく、選択肢の最初の失敗最後の呼び出しで保存しました。だから我々はちょうど最初の項(「foo」を)見つけ、それが成功する、2番目のオプションを用意してみてください。前の呼び出しと比較すると、これは悪い結果であるので、私たちはここで停止して(つまり、(fooの+バー)+バズ)を解決するために、最長のままにしておきます。

実際の作業コードに変換するために、私は、最初のコードわずかOracleへの()の呼び出しと左再帰exprの()を呼び出し組み合わせを書き換えたいです。我々はそれを呼び出しますoracle_expr()コード:

def expr():
    if oracle_expr() and expect('+') and term():
        return True
    if term():
        return True
    return False

次に、我々は、上記のロジックを実現するためにデコレータを書き込みます。それは(私は後でそれを取り除くよ、心配しないでください)グローバル変数を使用しています。oracle_expr()この関数は、グローバル変数を読んで操作し、それが飾られます。

saved_result = None
def oracle_expr():
    if saved_result is None:
        return False
    return saved_result
def expr_wrapper():
    global saved_result
    saved_result = None
    parsed_length = 0
    while True:
        new_result = expr()
        if not new_result:
            break
        new_parsed_length = <calculate size of new_result>
        if new_parsed_length <= parsed_length:
            break
        saved_result = new_result
        parsed_length = new_parsed_length
    return saved_result

もちろん、このプロセスは悲しいですが、それはポイントのコードを示し、それでは、それは我々が誇りに思っている何かに開発することができ、それを試してみましょう。

我々は、代わりにグローバル変数のキャッシュメモリを使用して、次の呼び出しに結果を保存し、その後、私たちは余分な必要がないことを決定的な洞察は、(私が最初に考えたのではないかもしれないが、これは、私自身である)oracle_expr()機能を-我々は関係なく、それが再帰的に残っているかどうかのexprに標準コール()、場所を生成することができます。

これを行うために、我々は唯一の左再帰的なルールのため、別々の@memoize_left_recデコレータを必要としています。)(oracle_expr()関数の役割として動作するキャッシュメモリで保存された値から、それを削除し、それがサイクルが長い前部長く、それぞれの新しい結果、繰り返し式exprを呼んでカバーよりも呼ばれている含まれてい。

もちろん、メモリキャッシュの場所ので、キャッシュを処理するために、各分析法の入力に応じて、それがバックトラックまたは複数の再帰的なルールに影響を与えていない(例えば、おもちゃの文法で、私はexprの用語を使用している左再帰されています) 。

私は最初の3件の記事で作成した別の素晴らしい特性インフラストラクチャは、新しい結果が古い結果よりも長いチェックすることが容易であるということである:マーク()メソッドは、タグ配列入力にインデックスを返すので、我々はそれを使用することができ、むしろparsed_length上記以外。

私はなぜこのアルゴリズムは常に有効どんなに狂っこの構文は、証明ませんする必要はありません。私は実際に証明を読んでいないからです。私は左再帰の後ろに隠れて、オプションのエントリでは、バックアップオプションを含む、例えば(また、より複雑な状況に適用され、それはexprのおもちゃの構文に適用される単純なケースを参照するか、または複数の間で相互に再帰的なルールを伴います)が、Pythonの構文、私は考えることができる最も複雑な場合には、まだかなり穏やかであるので、私は定理とその人々を証明するために信頼することができます。

だから、私たちがやって主張し、いくつかの実際のコードを表示してみましょう。

まず、パーサジェネレータは、再帰的なルールを残っているものを検出しなければなりません。これが問題となっていた、グラフ理論です。私は実際に私が更なる作業を簡素化します、ここではアルゴリズムが表示されませんし、再帰的な文法規則を残された唯一の仮説は、私たちのおもちゃの文法exprのように、直接左再帰です。そして、左再帰をチェックするだけで始まり、現在のルール名に代わるものを見つける必要があります。私たちは書くことができます。

def is_left_recursive(rule):
    for alt in rule.alts:
        if alt[0] == rule.name:
            return True
    return False

左再帰的なルールのため、それは別のデコレータを生成することができるように今、私たちは、パーサジェネレータを変更します。最初の3回の記事では、我々はすべての分析方法@memoize修正を使用したことを思い出してください。我々は、我々はmemoize_left_recデコレータにだまし、@memoize_left_rec置き換え、発電機になりました左再帰ルールの小さな変更、こと。そして、コードジェネレータの残りの部分は変更する必要はありませんサポート!(ただし、私は視覚的なコードでそれをいじる必要がありました。)

参考のため、ここで最初の三つからコピーされた元の@memoizeのデコレータです。なお、セルフパーサー・インスタンス(空辞書初期有する)属性を持つメモ、マーク()と現在位置を取得する()メソッドをリセットし、トークナイザを設定します。

def memoize(func):
    def memoize_wrapper(self, *args):
        pos = self.mark()
        memo = self.memos.get(pos)
        if memo is None:
            memo = self.memos[pos] = {}
        
        key = (func, args)
        if key in memo:
            res, endpos = memo[key]
            self.reset(endpos)
        else:
            res = func(self, *args)
            endpos = self.mark()
            memo[key] = res, endpos
        return res
    return memoize_wrapper

入力タグ(不活性)アレイ、別個の各位置における- @memoizeデコレータは、各入力位置の前の呼び出しを覚えmemo辞書。最初の4行memoize_wrapper機能と正しい得るmemo辞書関連します。

これは@memoize_left_recです。唯一の他の支店と@memoize上記異なります。

    def memoize_left_rec(func):
    def memoize_left_rec_wrapper(self, *args):
        pos = self.mark()
        memo = self.memos.get(pos)
        if memo is None:
            memo = self.memos[pos] = {}
        key = (func, args)
        if key in memo:
            res, endpos = memo[key]
            self.reset(endpos)
        else:
            # Prime the cache with a failure.
            memo[key] = lastres, lastpos = None, pos
            # Loop until no longer parse is obtained.
            while True:
                self.reset(pos)
                res = func(self, *args)
                endpos = self.mark()
                if endpos <= lastpos:
                    break
                memo[key] = lastres, lastpos = res, endpos
            res = lastres
            self.reset(lastpos)
        return res
    return memoize_left_rec_wrapper

それはおそらく生成するには、show exprの()メソッドを助け、私たちはデコレータと装飾的な方法との間の流れを追跡することができます。

    @memoize_left_rec 
    def expr(self):
        pos = self.mark()
        if ((expr := self.expr()) and
            self.expect('+') and
            (term := self.term())):
            return Node('expr', [expr, term])
        self.reset(pos)
        if term := self.term():
            return Node('term', [term])
        self.reset(pos)
        return None

私たちが解析してみましょうfoo + bar + baz

あなたの呼び出しが式expr()関数で装飾されているときはいつでも、デコレータは「インターセプト」のコールは、一見、現在位置の前にそれを呼び出します。それ以外で最初の呼び出しでは、それが繰り返し装飾のない関数を呼び出す枝を入力します。もちろんポイントのバージョンを装飾するための装飾のない関数呼び出し式expr()、ので、この再帰呼び出しが再びインターセプトされたとき。ここで再帰の停止、今メモキャッシュヒットであるため。

今何?このラインからの初期キャッシュ値:

            # Prime the cache with a failure.
            memo[key] = lastres, lastpos = None, pos

これは、()exprの飾りを作る(に失敗する場合は、最初のものexprの()には、Noneを返しますexpr := self.expr())。それが成功した期間を特定し、あればそのため、我々は(我々の例では「foo」である)第二に進み、exprはノードのインスタンスを返します。それはどこに戻りますか?whileループ内のデコレータへ。この新しい結果は、メモキャッシュ(ノードインスタンス)を更新し、次の反復を開始します。

)(再び装飾のない式exprを呼び出し、傍受再帰呼び出しは、ノードのキャッシュ(用語)の新しいインスタンスを返します。これは、呼び出しが(「+」)を期待し続け、成功を収めました。この成功は、再び、我々は最初の「+」演算子になりました。その後、我々は、用語を見つけたい(「バー」を見つける。)成功しています。

空の式expr()のために、同定されているのでfoo + bar、新たな(長い)結果はメモしてキャッシュを更新するために、そして次の反復を開きます、whileループバック、同じプロセスを通過します。

ゲームは再び演奏しました。インターセプト再帰exprは()(これがfoo +バーです)キャッシュから新しい結果を取得するために再び呼び出され、私たちは別の「+」(第2)と別の用語(「バズ」)を見つけることを期待します。私たちは、ノード表現を構築(foo + bar) + bazし、メモキャッシュにそれを記入し、再び繰り返すだろうwhileループに戻りました。

しかし、次回のものは異なります。新しい結果で、我々は別の「+」を見つけたが、見つけられませんでした!したがって、このexprは()の第二の選択肢に戻ります呼び出し、貧しい用語を返します。私たちはwhileループに到着したとき、それは結果が最後のものよりも短くなっていることを見つけることが残念です、それは中断され、元の通話に戻ります結果((FOO +バー)+バズ)長い外部exprの()の呼び出しを初期化することです(例えば、文の()の呼び出し - ここでは示されていない)ところ。

これは、物語の今日の終わり:我々は成功したPEG(-ish)パーサで左再帰を飼いならしています。来週のためとして、私は構文で「アクション」(アクション)を追加することについて議論する予定で、私たちは与えられたバックアップ・オプションのための分析方法は、それが返されます(ただし、常にノードのインスタンスを返さない)結果をカスタマイズすることを考えることができます。

あなたは、コードを使用したい場合は、参照GitHubのリポジトリを(私はまた、再帰視覚的なコードを左に追加しましたが、私は特に満足していないので、私はリンクを与えることを意図していません。)

サンプルコードの内容の使用許諾契約書:CC-BY NC-SA 4.0

著者について:グイド・ヴァンロッサム、Pythonの生みの親、退位する2018年7月12日まで「優しい終身の独裁者」となっています。現在、彼は、新しいトップの意思決定の5人のメンバーの一人であるコミュニティでまだアクティブです。毎週日曜日に更新中で書かれた彼のブログシリーズオープンパーサ、まだシリーズのシリーズからこの記事は、。

翻訳者の紹介:エンドウの猫は、広東省で生まれた今、ソ連のドリフトプログラマ武漢大学、卒業し、考え、いくつかのオタクがあり、一部の人間の感情があり、いくつかの温度、およびいくつかの態度があります。いいえ公共ん: "Pythonの猫"(python_cat)。

公開番号[ Pythonの猫 ]、記事の質の高いシリーズのシリアル番号は、そこに哲学的な猫のニャースターシリーズ、Pythonのアドバンスシリーズ、推奨叢書、テクニカルライティングであり、高品質の英語翻訳など、歓迎注目ああをお勧めします。

おすすめ

転載: www.cnblogs.com/pythonista/p/11479373.html