Python設計模式之解釋器模式

解釋器模式

對每個應用來說,至少有以下兩種不同的用戶分類。

  • 基本用戶:這類用戶只希望能夠憑直覺使用應用。他們不喜歡花太多時間配置或學習應用的內部。對他們來說,基本的用法就足夠了。
  • 高級用戶:這些用戶,實際上通常是少數,不介意花費額外的時間學習如何使用應用的高級特性。如果知道學會之后能得到以下好處,他們甚至會去學習一種配置(或腳本)語言。
    · 能夠更好地控制一個應用
    · 以更好的方式表達想法
    · 提高生產力

解釋器(Interpreter)模式僅能引起應用的高級用戶的興趣。這是因為解釋器模式背后的主要思想是讓非初級用戶和領域專家使用一門簡單的語言來表達想法。然而,什么是一種簡單的語言?對于我們的需求來說,一種簡單的語言就是沒有像編程語言那么復雜的語言。

一般而言,我們想要創建的是一種領域特定語言(Domain Specific Language,DSL)。DSL是一種針對一個特定領域的有限表達能力的計算機語言。很多不同的事情都使用DSL,比如,戰斗模擬、記賬、可視化、配置、通信協議等。DSL分為內部DSL和外部DSL(請參考網頁[t.cn/zHtEh5t]和網頁[t.cn/hBfQ2Y])。

內部DSL構建在一種宿主編程語言之上。內部DSL的一個例子是,使用Python解決線性方程組的一種語言。使用內部DSL的優勢是我們不必擔心創建、編譯及解析語法,因為這些已經被宿主語言解決掉了。劣勢是會受限于宿主語言的特性。如果宿主語言不具備這些特性,構建一種表達能力強、簡沽而且優美的內部DSL是富有挑戰性的(請參考網頁[t.cn/Rqr3B12])。

外部DSL不依賴某種宿主語言。DSL的創建者可以決定語言的方方面面(語法、旬法等),但也要負責為其創建一個解析器和編譯器。為一種新語言創建解析器和編譯器是一個非常復雜、長期而又痛苦的過程(請參考網頁[t.cn/Rqr3B12])。

解釋器模式僅與內部DSL相關。因此,我們的H標是使用宿主語言提供的特性構建一種簡單但有用的語言,在這里,宿主語言是Python。注意,解釋器根本不處理語言解析,它假設我們已經有某種便利形式的解析好的數據,可以是抽象語法樹(abstract syntax tree,AST)或任何其他好用的數據結構(請參考[GOF95,第276頁])。

現實生活的例子

音樂演奏者是現實中解釋器模式的一個例子。五線譜圖形化地表現了聲音的音調和持續時間。音樂演奏者能根據五線譜的符號精確地重現聲音。在某種意義上,五線譜是音樂的語言,音樂演奏者是這種語言的解釋器。下圖展示了音樂例子的圖形化描述,經www.sourcemaking.com準許使用(請參考網頁[t.cn/Rqr3Fqs])。

軟件的例子

內部DSL在軟件方面的例子有很多。PyT是一個用于生成(X)HTML的Python DSL。PyT關注性能,并聲稱能與Jinja2的速度相娘美(請參考網頁[t.cn/Rqr1vlP])。當然,我們不能假定在PyT中必須使用解釋器模式。然而,PyT是一種內部DSL,非常適合使用解釋器模式。

Chromium是一個自由開源的瀏覽器軟件,催生出了Google Chrome瀏覽器(請參考網頁[t.cn/hMjLK])。Chromium的Mesa庫Python綁定的一部分使用解釋器模式將C樣板參數翻譯成Python對象并執行相關的命令(請參考網頁[t.cn/Rqr1zZP])。

應用案例

在我們希望為領域專家和高級用戶提供一種簡單語言來解決他們的問題時,可以使用解釋器模式。不過要強調的第一件事情是,解釋器模式應僅用于實現簡單的語言。如果語言具有外部DSL那樣的要求,有更好的工具(yacc和lex、Bison、ANTLR等)來從頭創建一種語言。

我們的目標是為專家提供恰當的編程抽象,使其生產力更高;這些專家通常不是程序員。理想情況下,他們使用我們的DSL并不需要了解高級Python知識,當然了解一點Python基礎知識會更好,因為我們最終生成的是Python代碼,但不應該要求了解Python高級概念。此外,DSL的性能通常不是一個重要的關注點。重點是提供一種語言,隱藏宿主語言的獨特性,并提供人類更易讀的語法。誠然,Python已經是一門可讀性非常高的語言,與其他編程語言相比,其古怪的語法更少。

實現

我們來創建一種內部DSL控制一個智能屋。這個例子非常契合如今越來越受關注的物聯網時代。用戶能夠使用一種非常簡單的事件標記來控制他們的房子。一個事件的形式為command->receiver->arguments。參數部分是可選的。并不是所有事件都要求參數。不要求任何參數的事件例子如下所示。

open->gate

要求參數的事件例子如下所示:

increase -> boiler temperature -> 3 degrees

->符號用于標記事件一個部分的結束,并聲明下一個部分的開始。實現一種內部DSL有多種方式。我們可以使用普通的正則表達式、字符串處理、操作符重載的組合以及元編程,或者一個能幫我們完成困難工作的庫/工具。雖然在正規情況下,解釋器不處理解析,但我覺得一個實戰的例子也需要覆蓋解析工作。因此,我決定使用一個工具來完成解析工作。該工具名為Pyparsing,是標準Python3發行版的一部分心。要想獲取更多Pyparsing的信息,可參考Paul McGuire編寫的迷你書Getting Started with Pyparsing。如果你的系統上還沒安裝Pyparsing,可以使用下面的命令來安裝。

pip3 install pyparsing

下面的時序圖展示了用戶執行開門事件時發生的事情。對于其他事件,情況也是相似的,不過有些命令更復雜些,因為它們要求參數。

在編寫代碼之前,為我們的語言定義一種簡單語法是一個好做法。我們可以使用巴科斯-諾爾形式(Backus-Naur Form,BNF)表示法來定義語法(請參考網頁[t.cn/Rqr1ZrO])。

event ::= command token receiver token arguments command ::= word+
word ::= a collection of one or more alphanumeric characters token ::= ->
receiver ::= word+ arguments ::= word+

簡單來說,這個語法告訴我們的是一個事件具有command -> receiver -> arguments 的形式,并且命令、接收者及參數也具有相同的形式,即一個或多個字母數字字符的組合。包含數字部分是為了讓我們能夠在命令increase -> boiler temperature -> 3 degrees中傳遞3 degrees這樣的參數,所以不必懷疑數字部分的必要性。

既然定義了語法,那么接著將其轉變成實際的代碼。以下是代碼的樣子。

word = Word(alphanums)
command = Group(OneOrMore(word)) token = Suppress("->")
device = Group(OneOrMore(word)) argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)

代碼和語法定義基本的不同點是,代碼需要以自底向上的方式編寫。例如,如果不先為word賦一個值,那就不能使用它。Suppress用于聲明我們希望解析結果中省略->符號。

這個例子的完整代碼(文件interpreter.py)使用了很多占位類,但為了讓你精力集中一點,我會先只展示一個類。書中也包含完整的代碼列表,在仔細解說完這個類之后會展示。我們來看一下Boiler類。一個鍋爐的默認溫度為83攝氏度。類有兩個方法來分別提高和降低當前的溫度。

class Boiler:
    def __init__(self):
        self.temperature = 83 #

    def __str__(self):
        return 'boiler temperature: {}'.format(self.temperature)

    def increase_temperature(self, amount):
        print("increasing the boiler's temperature by {} degrees".format(amount))
        self.temperature += amount

    def decrease_temperature(self, amount):
        print("decreasing the boiler's temperature by {} degrees".format(amount))
        self.temperature -= amount

下一步是添加語法,之前已學習過。我們也創建一個boiler實例,并輸出其默認狀態。

word = Word(alphanums)
command = Group(OneOrMore(word))
token = Suppress("->")
device = Group(OneOrMore(word))
argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)

boiler = Boiler()
print(boiler)

獲取Pyparsing解析結果的最簡單方式是使用parseString()方法,該方法返回的結果是一個ParseResults實例,它實際上是一個可視為嵌套列表的解析樹。例如,執行print(event. parseString('increase -> boiler temperature -> 3 degrees'))得到的結果如下所示。

[['increase'], ['boiler', 'temperature'], ['3', 'degrees']]

因此,在這里,我們知道第一個子列表是命令(提高),第二個子列表是接收者(鍋爐溫度),第三個子列表是參數(3攝氏度)。實際上我們可以解開ParseResults實例,從而可以直接訪問事件的這三個部分。可直接訪問意味著我們可以匹配模式找到應該執行哪個方法(即使不可以直接訪問,也只能這樣做)。

cmd, dev, arg = event.parseString('increase -> boiler temperature -> 3 degrees')
if 'increase' in ' '.join(cmd):
    if 'boiler' in ' '.join(dev):
        boiler.increase_temperature(int(arg[0]))
        print(boiler)

執行上面的代碼片段會得到以下輸出。

python3 boiler.py
boiler temperature: 83
increasing the boiler's temperature by 3 degrees
boiler temperature: 86

interpreter.py的完整代碼與我剛描述的沒有什么大的不同,只是進行了擴展以支持更多的事件和設備。

from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums

class Gate:
    def __init__(self):
        self.is_open = False

    def __str__(self):
        return 'open' if self.is_open else 'closed'

    def open(self):
        print('opening the gate')
        self.is_open = True

    def close(self):
        print('closing the gate')
        self.is_open = False

class Garage:
    def __init__(self):
        self.is_open = False

    def __str__(self):
        return 'open' if self.is_open else 'closed'

    def open(self):
        print('opening the garage')
        self.is_open = True

    def close(self):
        print('closing the garage')
        self.is_open = False

class Aircondition:
    def __init__(self):
        self.is_on = False

    def __str__(self):
        return 'on' if self.is_on else 'off'

    def turn_on(self):
        print('turning on the aircondition')
        self.is_on = True

    def turn_off(self):
        print('turning off the aircondition')
        self.is_on = False

class Heating:
    def __init__(self):
        self.is_on = False

    def __str__(self):
        return 'on' if self.is_on else 'off'

    def turn_on(self):
        print('turning on the heating')
        self.is_on = True

    def turn_off(self):
        print('turning off the heating')
        self.is_on = False

class Boiler:
    def __init__(self):
        self.temperature = 83 # in celsius

    def __str__(self):
        return 'boiler temperature: {}'.format(self.temperature)

    def increase_temperature(self, amount):
        print("increasing the boiler's temperature by {} degrees".format(amount))
        self.temperature += amount

    def decrease_temperature(self, amount):
        print("decreasing the boiler's temperature by {} degrees".format(amount))
        self.temperature -= amount

class Fridge:
    def __init__(self):
        self.temperature = 2 # in celsius

    def __str__(self):
        return 'fridge temperature: {}'.format(self.temperature)

    def increase_temperature(self, amount):
        print("increasing the fridge's temperature by {} degrees".format(amount))
        self.temperature += amount

    def decrease_temperature(self, amount):
        print("decreasing the fridge's temperature by {} degrees".format(amount))
        self.temperature -= amount


def main():
    word = Word(alphanums)
    command = Group(OneOrMore(word))
    token = Suppress("->")
    device = Group(OneOrMore(word))
    argument = Group(OneOrMore(word))
    event = command + token + device + Optional(token + argument)

    gate = Gate()
    garage = Garage()
    airco = Aircondition()
    heating = Heating()
    boiler = Boiler()
    fridge = Fridge()

    tests = ('open -> gate',
             'close -> garage',
             'turn on -> aircondition',
             'turn off -> heating',
             'increase -> boiler temperature -> 5 degrees',
             'decrease -> fridge temperature -> 2 degrees')

    open_actions = {'gate':gate.open, 'garage':garage.open, 'aircondition':airco.turn_on,
                  'heating':heating.turn_on, 'boiler temperature':boiler.increase_temperature,
                  'fridge temperature':fridge.increase_temperature}
    close_actions = {'gate':gate.close, 'garage':garage.close, 'aircondition':airco.turn_off,
                   'heating':heating.turn_off, 'boiler temperature':boiler.decrease_temperature,
                   'fridge temperature':fridge.decrease_temperature}

    for t in tests:
        if len(event.parseString(t)) == 2: # no argument
            cmd, dev = event.parseString(t)
            cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)
            if 'open' in cmd_str or 'turn on' in cmd_str:
                open_actions[dev_str]()
            elif 'close' in cmd_str or 'turn off' in cmd_str:
                close_actions[dev_str]()
        elif len(event.parseString(t)) == 3: # argument
            cmd, dev, arg = event.parseString(t)
            cmd_str, dev_str, arg_str = ' '.join(cmd), ' '.join(dev), ' '.join(arg)
            num_arg = 0
            try:
                num_arg = int(arg_str.split()[0]) # extract the numeric part
            except ValueError as err:
                print("expected number but got: '{}'".format(arg_str[0]))
            if 'increase' in cmd_str and num_arg > 0:
                open_actions[dev_str](num_arg)
            elif 'decrease' in cmd_str and num_arg > 0:
                close_actions[dev_str](num_arg)

if __name__ == '__main__':
    main()
opening the gate
closing the garage
turning on the aircondition
turning off the heating
increasing the boiler's temperature by 5 degrees
decreasing the fridge's temperature by 2 degrees

執行上面的例子會得到以下輸出。

python3 interpreter.py opening the gate
closing the garage
turning on the aircondition
turning off the heating
increasing the boiler's temperature by 5 degrees
decreasing the fridge's temperature by 2 degrees

如果你想針對這個例子進行更多的實驗,我可以給你提一些建議。第一個會讓例子更有意思的改變是讓其變成交互式。目前,所有事件都是在tests元組中硬編碼的。然而,用戶希望能使用一個交互式提示符來激活命令。不要忘了Pyparsing對空格、Tab或意料之外的輸出都是敏感的。例如,如果用戶輸入turn off -> heating 37,那會發生什么呢?

另一個可能的改進是,注意open_actions和close_actions映射是如何用于將一個接收者關聯到一個方法的。使用一個映射而不是兩個,可能嗎?這樣做有何優勢?

小結

本章中,我們學習了解釋器設計模式。解釋器模式用于為高級用戶和領域專家提供一個類編程的框架,但沒有暴露出編程語言那樣的復雜性。這是通過實現一個DSL來達到H的的。

DSL是一種針對特定領域、表達能力有限的計算機語言。DSL有兩類,分別是內部DSL和外部DSL。內部DSL構建在一種宿主編程語言之上,依賴宿主編程語言,外部DSL則是從頭實現,不依賴某種已有的編程語言。解釋器模式僅與內部DSL相關。

樂譜是一個非軟件DSL的例子。音樂演奏者像一個解釋器那樣,使用樂譜演奏出音樂。從軟件的視角來看,許多Python模板引擎都使用了內部DSL。PyT是一個高性能的生成(X)HTML的 Python DSL。我們也看到Chromium的Mesa庫是如何使用解釋器模式將圖形相關的C代碼翻譯成Python可執行對象的。

雖然一般來說解釋器模式不處理解析的工作,但是在12.4節,我們使用了Pyparsing創建一種DSL來控制一個智能屋,并且看到使用一個好的解析工具以模式匹配來解釋結果更加簡單。

第13章將演示觀察者模式。觀察者模式用于在兩個或多個對象之間創建一個發布—訂閱通信類型。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,828評論 18 139
  • # Python 資源大全中文版 我想很多程序員應該記得 GitHub 上有一個 Awesome - XXX 系列...
    aimaile閱讀 26,535評論 6 427
  • GitHub 上有一個 Awesome - XXX 系列的資源整理,資源非常豐富,涉及面非常廣。awesome-p...
    若與閱讀 18,695評論 4 418
  • 當我真的愿意看見自己時 我可能還會痛 但已經不再抱怨 我深知 這痛 只因遇見真實的自己 曾經那個不懂愛的自己 透過...
    楊櫟穎閱讀 830評論 0 0
  • 前段時間在京東買了兩本書,《杜詩鏡銓》、《稼軒詞編年箋注》,皆為平裝,其中《杜詩鏡銓》是上下冊。我歷來認為,凡書,...
    老撒閱讀 428評論 0 1