欧美三区_成人在线免费观看视频_欧美极品少妇xxxxⅹ免费视频_a级毛片免费播放_鲁一鲁中文字幕久久_亚洲一级特黄

Python 之父的解析器系列之五:左遞歸 PEG 語(yǔ)法

系統(tǒng) 1608 0

原題 | Left-recursive PEG grammars

作者 | Guido van Rossum(Python之父)

譯者 | 豌豆花下貓(“Python貓”公眾號(hào)作者)

聲明 | 本翻譯是出于交流學(xué)習(xí)的目的,基于 CC BY-NC-SA 4.0 授權(quán)協(xié)議。為便于閱讀,內(nèi)容略有改動(dòng)。

我曾幾次提及左遞歸是一塊絆腳石,是時(shí)候去解決它了。基本的問(wèn)題在于:使用遞歸下降解析器時(shí),左遞歸會(huì)因堆棧溢出而導(dǎo)致程序終止。

【這是我的 PEG 系列的第 5 部分。其它文章參見(jiàn)這個(gè)目錄】

假設(shè)有如下的語(yǔ)法規(guī)則:

          
            expr: expr '+' term | term
          
        

如果我們天真地將它翻譯成遞歸下降解析器的片段,會(huì)得到如下內(nèi)容:

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

也就是 expr() 以調(diào)用 expr() 開(kāi)始,后者也以調(diào)用 expr() 開(kāi)始,以此類推……這只能以堆棧溢出而結(jié)束,拋出異常 RecursionError

傳統(tǒng)的補(bǔ)救措施是重寫(xiě)語(yǔ)法。在之前的文章中,我已經(jīng)這樣做了。事實(shí)上,上面的語(yǔ)法也能識(shí)別出來(lái),如果我們重寫(xiě)成這樣:

          
            expr: term '+' expr | term
          
        

但是,如果我們用它生成一個(gè)解析樹(shù),那么解析樹(shù)的形狀會(huì)有所不同,這會(huì)導(dǎo)致破壞性的后果,比如當(dāng)我們?cè)谡Z(yǔ)法中添加一個(gè) '-' 運(yùn)算符時(shí)(因?yàn)? a - (b - c) (a - b) - c 不一樣)。

這通常可以使用更強(qiáng)大的 PEG 特性來(lái)解決,例如分組和迭代,我們可以將上述規(guī)則重寫(xiě)為:

          
            expr: term ('+' term)*
          
        

實(shí)際上,這正是 Python 當(dāng)前語(yǔ)法在 pgen 解析器生成器上的寫(xiě)法(pgen 與左遞歸規(guī)則具有同樣的問(wèn)題)。

但是這仍然存在一些問(wèn)題:因?yàn)橄? '+' '-' 這樣的運(yùn)算符,基本上是二進(jìn)制的(在 Python 中),當(dāng)我們解析像 a + b + c 這樣的東西時(shí),我們必須遍歷解析的結(jié)果(基本上是列表['a','+','b','+','c'] ),以構(gòu)造一個(gè)左遞歸的解析樹(shù)(類似于 [['a','+','b'] ,'+','c'] )。

原始的左遞歸語(yǔ)法已經(jīng)表訴了所需的關(guān)聯(lián)性,因此,如果我們可以直接以該形式生成解析器,那將會(huì)很好。我們可以!一位粉絲向我指出了一個(gè)很好的技巧,還附帶了一個(gè)數(shù)學(xué)證明,很容易實(shí)現(xiàn)。我會(huì)試著在這里解釋一下。

讓我們考慮輸入 foo + bar + baz 作為示例。我們想要解析出的解析樹(shù)對(duì)應(yīng)于 (foo + bar)+ baz 。這需要對(duì) expr() 進(jìn)行三次左遞歸調(diào)用:一次對(duì)應(yīng)于頂級(jí)的“+” 運(yùn)算符(即第二個(gè)); 一次對(duì)應(yīng)于內(nèi)部的“+”運(yùn)算符(即第一個(gè)); 還有一次是選擇第二個(gè)備選項(xiàng)(即 term )。

由于我不善于使用計(jì)算機(jī)繪制實(shí)際的圖表,因此我將在此使用 ASCII 技巧作演示:

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

我們的想法是希望在 expr() 函數(shù)中有一個(gè)“oracle”(譯注:預(yù)言、神諭,后面就不譯了),它要么告訴我們采用第一個(gè)備選項(xiàng)(即遞歸調(diào)用 expr()),要么是第二個(gè)(即調(diào)用 term())。在第一次調(diào)用 expr() 時(shí),“oracle”應(yīng)該返回 true; 在第二次(遞歸)調(diào)用時(shí),它也應(yīng)該返回 true,但在第三次調(diào)用時(shí),它應(yīng)該返回 false,以便我們可以調(diào)用 term()。

在代碼中,應(yīng)該是這樣:

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

我們?cè)撛趺磳?xiě)這樣的“oracle”呢?試試看吧......我們可以嘗試記錄在調(diào)用堆棧上的 expr() 的(左遞歸)調(diào)用次數(shù),并將其與下面表達(dá)式中“+” 運(yùn)算符的數(shù)量進(jìn)行比較。如果調(diào)用堆棧的深度大于運(yùn)算符的數(shù)量,則應(yīng)該返回 false。

我?guī)缀跸胗? sys._getframe() 來(lái)實(shí)現(xiàn)它,但有更好的方法:讓我們反轉(zhuǎn)調(diào)用的堆棧!

這里的想法是我們從 oracle 返回 false 處調(diào)用,并保存結(jié)果。這就有了 expr()->term()->'foo' 。(它應(yīng)該返回初始的 term 的解析樹(shù),即 'foo' 。上面的代碼僅返回 True,但在本系列第二篇文章中,我已經(jīng)演示了如何返回一個(gè)解析樹(shù)。)很容易編寫(xiě)一個(gè) oracle 來(lái)實(shí)現(xiàn),它應(yīng)該在首次調(diào)用時(shí)就返回 false——不需要檢查堆棧或向前回看。

然后我們?cè)俅握{(diào)用 expr() ,這時(shí) oracle 會(huì)返回 true,但是我們不對(duì) expr() 進(jìn)行左遞歸調(diào)用,而是用前一次調(diào)用時(shí)保存的結(jié)果來(lái)替換。瞧吶,預(yù)期的 '+' 運(yùn)算符及隨后的 term 也出現(xiàn)了,所以我們將會(huì)得到 foo + bar

我們重復(fù)這個(gè)過(guò)程,然后事情看起來(lái)又很清晰了:這次我們會(huì)得到完整表達(dá)式的解析樹(shù),并且它是正確的左遞歸((foo + bar)+ baz )。

然后我們?cè)俅沃貜?fù)該過(guò)程,這一次,oracle 返回 true,并且前一次調(diào)用時(shí)保存的結(jié)果可用,沒(méi)有下一步的'+' 運(yùn)算符,并且第一個(gè)備選項(xiàng)失效。所以我們嘗試第二個(gè)備選項(xiàng),它會(huì)成功,正好找到了初始的 term('foo')。與之前的調(diào)用相比,這是一個(gè)糟糕的結(jié)果,所以在這里我們停止并留下最長(zhǎng)的解析(即(foo + bar)+ baz )。

為了將其轉(zhuǎn)換為實(shí)際的工作代碼,我首先要稍微重寫(xiě)代碼,以將 oracle() 的調(diào)用與左遞歸的 expr() 調(diào)用相結(jié)合。我們稱之為 oracle_expr() 。代碼:

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

接著,我們將編寫(xiě)一個(gè)實(shí)現(xiàn)上述邏輯的裝飾器。它使用了一個(gè)全局變量(不用擔(dān)心,我稍后會(huì)改掉它)。 oracle_expr() 函數(shù)將讀取該全局變量,而裝飾器操縱著它:

          
            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 = 
            
              
        if new_parsed_length <= parsed_length:
            break
        saved_result = new_result
        parsed_length = new_parsed_length
    return saved_result
            
          
        

這過(guò)程當(dāng)然是可悲的,但它展示了代碼的要點(diǎn),所以讓我們嘗試一下,將它發(fā)展成我們可以引以為傲的東西。

決定性的洞察(這是我自己的,雖然我可能不是第一個(gè)想到的)是我們可以使用記憶緩存而不是全局變量,將一次調(diào)用的結(jié)果保存到下一次,然后我們不需要額外的 oracle_expr() 函數(shù)——我們可以生成對(duì) expr() 的標(biāo)準(zhǔn)調(diào)用,無(wú)論它是否處于左遞歸的位置。

為了做到這點(diǎn),我們需要一個(gè)單獨(dú)的 @memoize_left_rec 裝飾器,它只用于左遞歸規(guī)則。它通過(guò)將保存的值從記憶緩存中取出,充當(dāng)了 oracle_expr() 函數(shù)的角色,并且它包含著一個(gè)循環(huán)調(diào)用,只要每個(gè)新結(jié)果所覆蓋的部分比前一個(gè)長(zhǎng),就反復(fù)地調(diào)用 expr()。

當(dāng)然,因?yàn)橛洃浘彺娣謩e按輸入位置和每個(gè)解析方法來(lái)處理緩存,所以它不受回溯或多個(gè)遞歸規(guī)則的影響(例如,在玩具語(yǔ)法中,我一直使用 expr 和 term 都是左遞歸的)。

我在第 3 篇文章中創(chuàng)建的基礎(chǔ)結(jié)構(gòu)的另一個(gè)不錯(cuò)的屬性是它更容易檢查新結(jié)果是否長(zhǎng)于舊結(jié)果:mark() 方法將索引返回到輸入的標(biāo)記符數(shù)組中,因此我們可以使用它,而非上面的parsed_length 。

我沒(méi)有證明為什么這個(gè)算法總是有效的,不管這個(gè)語(yǔ)法有多瘋狂。那是因?yàn)槲覍?shí)際上沒(méi)有讀過(guò)那個(gè)證明。我看到它適用于玩具語(yǔ)法中的 expr 等簡(jiǎn)單情況,也適用于更復(fù)雜的情況(例如,涉及一個(gè)備選項(xiàng)里可選條目背后藏著的左遞歸,或涉及多個(gè)規(guī)則之間的相互遞歸),但在 Python 的語(yǔ)法中,我能想到的最復(fù)雜的情況仍然相當(dāng)溫和,所以我可以信任于定理和證明它的人。

所以讓我們堅(jiān)持干,并展示一些真實(shí)的代碼。

首先,解析器生成器必須檢測(cè)哪些規(guī)則是左遞歸的。這是圖論中一個(gè)已解決的問(wèn)題。我不會(huì)在這里展示算法,事實(shí)上我將進(jìn)一步簡(jiǎn)化工作,并假設(shè)語(yǔ)法中唯一的左遞歸規(guī)則就是直接左遞歸的,就像我們的玩具語(yǔ)法中的 expr 一樣。然后檢查左遞歸只需要查找以當(dāng)前規(guī)則名稱開(kāi)頭的備選項(xiàng)。我們可以這樣寫(xiě):

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

現(xiàn)在我們修改解析器生成器,以便對(duì)于左遞歸規(guī)則,它能生成一個(gè)不同的裝飾器。回想一下,在第 3 篇文章中,我們使用 @memoize 修飾了所有的解析方法。我們現(xiàn)在對(duì)生成器進(jìn)行一個(gè)小小的修改,對(duì)于左遞歸規(guī)則,我們替換成 @memoize_left_rec ,然后我們?cè)趍emoize_left_rec 裝飾器中變魔術(shù)。生成器的其余部分和支持代碼無(wú)需更改!(然而我不得不在可視化代碼中搗鼓一下。)

作為參考,這里是原始的 @memoize 裝飾器,從第 3 篇中復(fù)制而來(lái)。請(qǐng)注意,self 是一個(gè)Parser 實(shí)例,它具有 memo 屬性(用空字典初始化)、mark() 和 reset() 方法,用于獲取和設(shè)置 tokenizer 的當(dāng)前位置:

          
            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 裝飾器在每個(gè)輸入位置記住了前一調(diào)用——在輸入標(biāo)記符的(惰性)數(shù)組的每個(gè)位置,有一個(gè)單獨(dú)的 memo 字典。memoize_wrapper 函數(shù)的前四行與獲取正確的 memo 字典有關(guān)。

這是 @memoize_left_rec 。只有 else 分支與上面的 @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
          
        

它很可能有助于顯示生成的 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
          
        

讓我們?cè)囍馕? foo + bar + baz

每當(dāng)你調(diào)用被裝飾的 expr() 函數(shù)時(shí),裝飾器就會(huì)“攔截”調(diào)用,它會(huì)在當(dāng)前位置查找前一個(gè)調(diào)用。在第一個(gè)調(diào)用處,它會(huì)進(jìn)入 else 分支,在那里它重復(fù)地調(diào)用未裝飾的函數(shù)。當(dāng)未裝飾的函數(shù)調(diào)用 expr() 時(shí),這當(dāng)然指向了被裝飾的版本,因此這個(gè)遞歸調(diào)用會(huì)再次被截獲。遞歸在這里停止,因?yàn)楝F(xiàn)在 memo 緩存有了命中。

接下來(lái)呢?初始的緩存值來(lái)自這行:

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

這使得被裝飾的 expr() 返回 None,在那 expr() 里的第一個(gè) if 會(huì)失敗(在 expr := self.expr() )。所以我們繼續(xù)到第二個(gè) if,它成功識(shí)別了一個(gè) term(在我們的例子中是 ‘foo’),expr 返回一個(gè) Node 實(shí)例。它返回到了哪里?到了裝飾器里的 while 循環(huán)。這新的結(jié)果會(huì)更新 memo 緩存(那個(gè) node 實(shí)例),然后開(kāi)始下一個(gè)迭代。

再次調(diào)用未裝飾的 expr(),這次截獲的遞歸調(diào)用返回新緩存的 Node 實(shí)例(一個(gè) term)。這是成功的,調(diào)用繼續(xù)到 expect('+')。這再次成功,然后我們現(xiàn)在處于第一個(gè)“+” 操作符。在此之后,我們要查找一個(gè) term,也成功了(找到 'bar')。

所以對(duì)于空的 expr(),目前已識(shí)別出 foo + bar ,回到 while 循環(huán),還會(huì)經(jīng)歷相同的過(guò)程:用新的(更長(zhǎng)的)結(jié)果來(lái)更新 memo 緩存,并開(kāi)啟下一輪迭代。

游戲再次上演。被截獲的遞歸 expr() 調(diào)用再次從緩存中檢索新的結(jié)果(這次是 foo + bar),我們期望并找到另一個(gè) ‘+’(第二個(gè))和另一個(gè) term(‘baz’)。我們構(gòu)造一個(gè) Node 表示 (foo + bar) + baz ,并返回給 while 循環(huán),后者將它填充進(jìn) memo 緩存,并再次迭代。

但下一次事情會(huì)有所不同。有了新的結(jié)果,我們查找另一個(gè) '+' ,但沒(méi)有找到!所以這個(gè)expr() 調(diào)用會(huì)回到它的第二個(gè)備選項(xiàng),并返回一個(gè)可憐的 term。當(dāng)走到 while 循環(huán)時(shí),它失望地發(fā)現(xiàn)這個(gè)結(jié)果比最后一個(gè)短,就中斷了,將更長(zhǎng)的結(jié)果((foo + bar)+ baz )返回給原始調(diào)用,就是初始化了外部 expr() 調(diào)用的地方(例如,一個(gè) statement() 調(diào)用——此處未展示)。

到此,今天的故事結(jié)束了:我們已經(jīng)成功地在 PEG(-ish)解析器中馴服了左遞歸。至于下周,我打算論述在語(yǔ)法中添加“動(dòng)作”(actions),這樣我們就可以為一個(gè)給定的備選項(xiàng)的解析方法,自定義它返回的結(jié)果(而不是總要返回一個(gè) Node 實(shí)例)。

如果你想使用代碼,請(qǐng)參閱GitHub倉(cāng)庫(kù)。(我還為左遞歸添加了可視化代碼,但我并不特別滿意,所以不打算在這里給出鏈接。)

本文內(nèi)容與示例代碼的授權(quán)協(xié)議:CC BY-NC-SA 4.0

作者簡(jiǎn)介: Guido van Rossum,Python 的創(chuàng)造者,一直是“終身仁慈獨(dú)裁者”,直到 2018 年 7 月 12 日退位。目前,他是新的最高決策層的五位成員之一,依然活躍在社區(qū)中。本文出自他在 Medium 開(kāi)博客所寫(xiě)的解析器系列,該系列仍在連載中,每周日更新。

譯者簡(jiǎn)介: 豌豆花下貓,生于廣東畢業(yè)于武大,現(xiàn)為蘇漂程序員,有一些極客思維,也有一些人文情懷,有一些溫度,還有一些態(tài)度。公眾號(hào):「Python貓」(python_cat)。

Python 之父的解析器系列之五:左遞歸 PEG 語(yǔ)法_第1張圖片

公眾號(hào)【 Python貓 】, 本號(hào)連載優(yōu)質(zhì)的系列文章,有喵星哲學(xué)貓系列、Python進(jìn)階系列、好書(shū)推薦系列、技術(shù)寫(xiě)作、優(yōu)質(zhì)英文推薦與翻譯等等,歡迎關(guān)注哦。


更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號(hào)聯(lián)系: 360901061

您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對(duì)您有幫助就好】

您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長(zhǎng)會(huì)非常 感謝您的哦!!!

發(fā)表我的評(píng)論
最新評(píng)論 總共0條評(píng)論
主站蜘蛛池模板: 精品成人一区二区三区 | 亚洲精品久久久一区二区三区 | 国产一级大片在线观看 | 免费高清欧美一区二区视频 | 天天操人人射 | 天天拍夜夜添久久精品中文 | 久久国产高清视频 | 性做久久久久免费看 | 欧美日韩一区在线 | 理论片午午伦夜理片在线播放 | 伊人色综合网 | 日本中文字幕在线视频 | 全色网站 | 91视视频在线观看入口直接观看 | 午夜免费视频 | 五月网婷婷 | 九月激情网 | 日韩中文字幕 | 亚洲一区二区三区在线看 | 午夜精品小视频 | 欧美1区2区 | 精品无码国产一区二区日本 | 夜夜爽日日澡人人 | 激情大乳女做爰办公室韩国 | 天堂网果冻传媒 | 大色综合 | 日韩 亚洲 欧美 中文 高清 | 九九九久久久久久久爱 | 午夜福利视频 | 精品粉嫩aⅴ一区二区三区四区 | 奇米四色在线观看 | 亚洲精品视频免费观看 | 欧美精品国产精品 | 欧美精品久久久久久久久老牛影院 | 香蕉一区 | 在线久草 | 91日本在线观看亚洲精品 | 久久成人国产精品免费 | 成人年鲁鲁在线观看视频 | 成人性爱视频在线观看 | 国产精品拍拍拍福利在线观看 |