1. 程式人生 > 實用技巧 >python設計模式之直譯器模式

python設計模式之直譯器模式

python設計模式之直譯器模式

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

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

直譯器( Interpreter)模式僅能引起應用的高階使用者的興趣。這是因為直譯器模式背後的主要思想是讓非初級使用者和領域專家使用一門簡單的語言來表達想法。然而,什麼是一種簡單的語言?對於我們的需求來說,一種簡單的語言就是沒程式語言那麼複雜的語言。

一般而言,我們想要建立的是一種領域特定語言( Domain Specific Language, DSL)。 DSL是一種針對一個特定領域的有限表達能力的計算機語言。很多不同的事情都使用DSL,比如,戰鬥模擬、記賬、視覺化、配置、通訊協議等。

內部DSL構建在一種宿主程式語言之上。內部DSL的一個例子是,使用Python解決線性方程組的一種語言。使用內部DSL的優勢是我們不必擔心建立、編譯及解析語法,因為這些已經被宿主語言解決掉了。劣勢是會受限於宿主語言的特性。如果宿主語言不具備這些特性,構建一種表達能力強、簡潔而且優美的內部DSL是富有挑戰性的。

外部DSL不依賴某種宿主語言。 DSL的建立者可以決定語言的方方面面(語法、句法等),但也要負責為其建立一個解析器和編譯器。為一種新語言建立解析器和編譯器是一個非常複雜、長期而又痛苦的過程。

直譯器模式僅與內部DSL相關。因此,我們的目標是使用宿主語言提供的特性構建一種簡單但有用的語言,在這裡,宿主語言是Python。注意,直譯器根本不處理語言解析,它假設我們已經有某種便利形式的解析好的資料,可以是抽象語法樹( abstract syntax tree, AST)或任何其他好用的資料結構。

1. 現實生活的例子

音樂演奏者是現實中直譯器模式的一個例子。五線譜圖形化地表現了聲音的音調和持續時間。音樂演奏者能根據五線譜的符號精確地重現聲音。在某種意義上,五線譜是音樂的語言,音樂演奏者是這種語言的直譯器。

2. 軟體的例子

內部DSL在軟體方面的例子有很多。 PyT是一個用於生成(X)HTML的Python DSL。 PyT關注效能,並聲稱能與Jinja2的速度相媲美。當然,我們不能假定在PyT中必須使用直譯器模式。然而, PyT是一種內部DSL,非常適合使用直譯器模式。

3. 應用案例

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

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

4. 實現

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

open -> gate

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

increase -> boiler temperature -> 3 degrees

->符號用於標記事件一個部分的結束,並宣告下一個部分的開始。實現一種內部DSL有多種方式。我們可以使用普通的正則表示式、字串處理、操作符過載的組合以及超程式設計,或者一個能幫我們完成困難工作的庫/工具。雖然在正規情況下,直譯器不處理解析,但我覺得一個實戰的例子也需要覆蓋解析工作。因此,我決定使用一個工具來完成解析工作。 該工具名為Pyparsing,是標準Python3發行版的一部分。 如果你的系統上還沒安裝Pyparsing,可以使用下面的命令來
安裝。

pip3 install pyparsing

在編寫程式碼之前,為我們的語言定義一種簡單語法是一個好做法。我們可以使用巴科斯-諾
爾形式( Backus-Naur Form, BNF)表示法來定義語法。

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)

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

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

完整程式碼如下:

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 # 單位為攝氏度
    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: # 沒有引數
            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: # 有引數
            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]) # 抽取數值部分
            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 __neme__ == '__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

5. 小結

直譯器模式用於為高階使用者和領域專家提供一個類程式設計的框架,但沒有暴露出程式語言那樣的複雜性。這是通過實現一個DSL來達到目的的。

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

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