見出し画像

DP.18:より人間が使いやすい構文をつくる - Interpreter(インタープリター)パターン -【Python】

【1】Interpreter(インタープリター)パターン

Interpreter(インタープリター)パターンは

既存の言語環境(ここではPython)を、より人間が目で見て読みやすい構文に置き換えて、「特定のタスク解決に特化した言語(DSL:Domain Specific Language)」をつくる

というもの。

例えば「SQL」DB操作に特化した言語であり、人間が目で見て読解しやすい自然な言語体系(英文としてよみやすい形式)になっている。

【2】DSL:Domain Specific Language (ドメイン固有言語)

専門的な用語でいうと、特定のタスク向けに設計された言語は「DSL:Domain Specific Language」という。

DSLには次の2つに分かれる。

・Internal DSL (内部DSL) ← Interpreterパターンで使う方
・External DSL (外部DSL)

■ Internal DSL(内部DSL)
Internal DSL」はベースとなるプログラミング言語があって、それを使って「DSL」を構築していく。

ベースとなるプログラミング言語側に文法や構文解析、コンパイルなどが備わっているので、その観点において心配がない。(別の見方をすると、ベースとなる言語側が実現できる範囲の機能に制約されるともいえる)

■ External DSL(外部DSL)
「External DSL」はベースの言語がない。

そのため、言語の文法・構文を決め、構文解析用のパーサーやコンパイラを作成する必要がでてくる。
(※Yacc、 Lex、 Bison、 ANTLR等のパーサジェネレータを活用して作っていく)

要するに「Interpreterパターンは、(今回の場合はpythonで)「Internal DSL:内部DSL」を構築して、、、

特定の領域の専門家・有識者が、ITスキル(詳細なプログラム言語の知識)をそれほど持っていなくても、「簡単な言語・文法を使って」アプリケーションでやりたいことを容易に行えるようにする

ということ。

【3】使用例

■例1:音楽制作ソフト

作曲家はソースコードを打ち込むではなく、起動したソフト上の譜面を操作することで曲を作成する

→ 作曲家はサウンドプログラミングの仕方を知らなくてもコンピュータ上で音楽が作れる。

■例2:ゲームエンジン(Unity、Unreal Engine等)

ゲームエンジンを使った3Dゲーム開発を考えてみる。
光源などの環境設定やテクスチャ、アニメーションなど複雑な処理をソースコードを一つ一つ打ち込むことは少ない。ゲームエンジンが対応するスクリプトやアセット等を用意することで簡単に3D表現を実現できる。

→ 開発者はOpenGLやDirectX、Vulkanといった3Dグラフィクスに関するプログラミングの仕方に精通していなくても実現したいことができる


【4】例題:冷蔵庫の温度を操作する内部DSL

例として、家電(冷蔵庫の温度)をリモート操作できるスマートハウスに「Interpreterパターン」を適用してみる。(つまり「Internal DSL:内部DSL」作成してみる)

■利用イメージ

my_smart_house = MySmartHouse() # スマートハウスオブジェクトを作成する

# 事前に決めた文法でコマンドを流す(冷蔵庫の温度を上げる・下げる)
my_smart_house.run("increase -> fridge temperature -> 3 degrees") 
my_smart_house.run("decrease -> fridge temperature -> 6 degrees")

# 実行結果例
increase the fridge temerature +3
decrease the fridge temerature -6

▲ こんな感じで、プログラムを使う側は、あらかじめ決めておいた文法に沿ってコマンドを流すだけ、みたいにする。
               ↓
「裏では流し込んだコマンドを解析(パース)して対応する関数をコールしている」というもの。

今回の「内部DSLのルール(受け付けるコマンドの文法)」は次のようにする。

画像1
画像2

■ PyParsingの利用
今回は流し込むコマンドの解析に「PyParsing」を利用する。
pip等でインストールしておく。

pip install pyparsing

※ドキュメントは以下参照。

■PyParsingの使い方:
PyParsingでの構文解析では、文法を定義したオブジェクトを作成して、文字列を投げ込めばよい。

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


word = Word(alphanums) # Wordは1文字以上の「大文字小文字の英字」と「数字」
command = Group(OneOrMore(word)) # Commandは1つ以上のwordで構成さ
token = Suppress("->") # トークン(区切りの文字)
device = Group(OneOrMore(word)) # 操作するデバイス名:「1つ以上のword」で構成
argument = Group(OneOrMore(word)) # 引数:「1つ以上のword」で構成
   
#コマンド(イベント)形式の定義
event = command + token + device + Optional(token + argument)

# parseString()により「->」をトークンとして
# [['increase'], ['fridge', 'temperature'], ['3', 'degrees']]
# に分割されて返ってくる
cmd, dev, arg = event.parseString("increase -> fridge temperature -> 3 degrees")

# 分割された各要素を出力してみる
print(cmd) 
print(dev)
print(arg)

#実行結果
['increase']
['fridge', 'temperature']
['3', 'degrees']

PyParsingの使い方を踏まえてコーディングしていく。

■冷蔵庫クラス(Fridge)の作成

# 冷蔵庫クラス
class Fridge:
   def __init__(self):
       self.temperature = 2

   def increase_temperature(self,temperature):
       print(f"increase the fridge temerature +{temperature}")
       self.temperature += temperature

   def decrease_temperature(self,temperature):
       print(f"decrease the fridge temerature -{temperature}")
       self.temperature -= temperature

▲シンプルに「現在の温度」と「温度上げる/下げるメソッド」をもつ

■PyParsingを使って文法を定義する
あらかじめ決めておいた文法をPyParsingで定義する
【再掲】

画像3
# 文法を定義する
def create_grammar():
   
   word = Word(alphanums) # Wordは1文字以上の「大文字小文字の英字」と「数字」
   command = Group(OneOrMore(word)) # Commandは1つ以上のwordで構成さ
   token = Suppress("->") # トークン(区切りの文字)
   device = Group(OneOrMore(word)) # 操作するデバイス名:「1つ以上のword」で構成
   argument = Group(OneOrMore(word)) # 引数:「1つ以上のword」で構成

   return  command + token + device + Optional(token + argument) 

↓ この文法を使う「MySmartHouseクラス」を作成する

■MySmartHouseクラス

class MySmartHouse:

   def __init__(self):
       self.fridge = Fridge()
       self.event = create_grammar()

       #スマートハウスが呼び出せるデバイス名と対応する関数オブジェクトを辞書にしておく
       self.open_actions = {
                       'fridge temperature':self.fridge.increase_temperature
                       }

       self.close_actions = { 
                       'fridge temperature':self.fridge.decrease_temperature
                       } 
   
   
   # 命令コマンドを受けて実行する
   def run(self, input_command):
       
       command = self.event.parseString(input_command) # 入力されたコマンドをパース
       
       if len(command) == 2:
           print("this is Unimplemented") # オプションを使わない他デバイス時に使用する部分

       elif len(command) == 3:
           # list型になっている各要素をjoinを使って1つの文字列にする
           cmd_str = ' '.join(command[0])
           dev_str = ' '.join(command[1])
           arg_str = ' '.join(command[2])

           #文字列数字の処理(数字化)をしつつコマンド内容で分岐
           num_arg = 0
           try:
               num_arg = int(arg_str.split()[0])
           except ValueError as err:
               print(f"expected number but got : {arg_str[0]}")

           if 'increase' in cmd_str and num_arg > 0:
               self.open_actions[dev_str](num_arg)
           elif 'decrease' in cmd_str and num_arg > 0:
               self.close_actions[dev_str](num_arg)

少し拡張しやすいようにあれこれ書いているが、最後のif文のところで、コマンドに対応するメソッドをコールしている。

class MySmartHouse:

   def __init__(self):
       self.fridge = Fridge()

            ...(略)...

       #スマートハウスが呼び出せるデバイス名と対応する関数オブジェクトを辞書にしておく
       self.open_actions = {
                       'fridge temperature':self.fridge.increase_temperature
                       }

       self.close_actions = { 
                       'fridge temperature':self.fridge.decrease_temperature
                       } 
...(略)...


                       
 def run(self, input_command):                      

           ...(略)...

           if 'increase' in cmd_str and num_arg > 0:
               self.open_actions[dev_str](num_arg)
           elif 'decrease' in cmd_str and num_arg > 0:
               self.close_actions[dev_str](num_arg)

▲PyParsingで流したコマンドを解析して、特定のワードが入っていたら実行させる、という仕組み。

※少しわかりずらいかもしれないが第15回の最後に少し書いた「【補足】第一級オブジェクト(first-class object)」の仕組みを使い、定義しておいた辞書から対応する関数をコールさせている。

【5】全体コード

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


# 冷蔵庫クラス
class Fridge:
   def __init__(self):
       self.temperature = 2

   def increase_temperature(self,temperature):
       print(f"increase the fridge temerature +{temperature}")
       self.temperature += temperature

   def decrease_temperature(self,temperature):
       print(f"decrease the fridge temerature -{temperature}")
       self.temperature -= temperature

# 文法を定義する
def create_grammar():
   
   word = Word(alphanums) # Wordは1文字以上の「大文字小文字の英字」と「数字」
   command = Group(OneOrMore(word)) # Commandは1つ以上のwordで構成さ
   token = Suppress("->") # トークン(区切りの文字)
   device = Group(OneOrMore(word)) # 操作するデバイス名:「1つ以上のword」で構成
   argument = Group(OneOrMore(word)) # 引数:「1つ以上のword」で構成

   return  command + token + device + Optional(token + argument) 



class MySmartHouse:

   def __init__(self):
       self.fridge = Fridge()
       self.event = create_grammar()

       #スマートハウスが受け付けられるコマンドを辞書にしておく
       self.open_actions = {
                       'fridge temperature':self.fridge.increase_temperature
                       }

       self.close_actions = { 
                       'fridge temperature':self.fridge.decrease_temperature
                       } 


   def run(self, input_command):
       
       command = self.event.parseString(input_command)
       if len(command) == 2:
           print("this is Unimplemented")

       elif len(command) == 3:
           cmd_str = ' '.join(command[0])
           dev_str = ' '.join(command[1])
           arg_str = ' '.join(command[2])

           #文字列数字の処理(数字化)をしつつコマンド内容で分岐
           num_arg = 0
           try:
               num_arg = int(arg_str.split()[0])
           except ValueError as err:
               print(f"expected number but got : {arg_str[0]}")

           if 'increase' in cmd_str and num_arg > 0:
               self.open_actions[dev_str](num_arg)
           elif 'decrease' in cmd_str and num_arg > 0:
               self.close_actions[dev_str](num_arg)

   

def main():

   my_smart_house = MySmartHouse()
   my_smart_house.run("increase -> fridge temperature -> 3 degrees")
   my_smart_house.run("decrease -> fridge temperature -> 6 degrees")


if __name__ == "__main__":
   main()

#実行結果
increase the fridge temerature +3
decrease the fridge temerature -6

【6】おまけ:複数の家電に対応させる

今回は「Fridge(冷蔵庫)」「Boiler(ボイラー)」「AirCondition(エアコン)」「Garage(ガレージ)」の4つに増やしてみる。

「AirCondition(エアコン)」「Garage(ガレージ)」は文法のオプション部分をを使わないパターン。同じような処理だがクラスメソッド名は統一していない。

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

### 冷蔵庫クラス
class Fridge:
   def __init__(self):
       self.temperature = 2

   def increase_temperature(self,temperature):
       print(f"increase the fridge temerature +{temperature}")
       self.temperature += temperature

   def decrease_temperature(self,temperature):
       print(f"decrease the fridge temerature -{temperature}")
       self.temperature -= temperature


### ボイラー
class Boiler:  
   def __init__(self):  
       self.temperature = 83 # in celsius 


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

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


### エアコン
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 air condition')  
       self.is_on = True  

   def turn_off(self):  
       print('turning off the air condition')  
       self.is_on = 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


############ 文法を定義する
def create_grammar():
   
   word = Word(alphanums) # Wordは1文字以上の「大文字小文字の英字」と「数字」
   command = Group(OneOrMore(word)) # Commandは1つ以上のwordで構成さ
   token = Suppress("->") # トークン(区切りの文字)
   device = Group(OneOrMore(word)) # 操作するデバイス名:「1つ以上のword」で構成
   argument = Group(OneOrMore(word)) # 引数:「1つ以上のword」で構成

   return  command + token + device + Optional(token + argument) 



class MySmartHouse:

   class IoTid(Enum):
       Fridge = 0
       Boiler = 1
       AirCondition = 2
       Garage = 3


   def __init__(self):

       #self.fridge = Fridge()
       self.IoTs = [Fridge(), Boiler(), AirCondition(), Garage()]
       self.event = create_grammar()

       #スマートハウスが受け付けられるコマンドを辞書にしておく
       self.open_actions = {
                       'fridge temperature':self.IoTs[self.IoTid.Fridge.value].increase_temperature,
                       'boiler temperature':self.IoTs[self.IoTid.Boiler.value].increase_temperature,
                       'air condition':self.IoTs[self.IoTid.AirCondition.value].turn_on,
                       'garage':self.IoTs[self.IoTid.Garage.value].open
                       }

       self.close_actions = { 
                       'fridge temperature':self.IoTs[self.IoTid.Fridge.value].decrease_temperature,
                       'boiler temperature':self.IoTs[self.IoTid.Boiler.value].decrease_temperature,
                       'air condition':self.IoTs[self.IoTid.AirCondition.value].turn_off,
                       'garage':self.IoTs[self.IoTid.Garage.value].close
                       } 


   def run(self, input_command):
       
       command = self.event.parseString(input_command)

       # オプション不要のIoT        
       if len(command) == 2:
           #print("this is Unimplemented")
           cmd_str = ' '.join(command[0])
           dev_str = ' '.join(command[1])

           # デバイスによってコールするコマンドが違うので組み合わせをチェック
           if ('open' in cmd_str and 'garage' in dev_str) or ('turn on' in cmd_str and 'air condition' in dev_str):
               self.open_actions[dev_str]()
           elif ('close' in cmd_str and 'garage' in dev_str) or ('turn off' in cmd_str and 'air condition' in dev_str ):
               self.close_actions[dev_str]()


       # オプション部分を使う場合のコマンド
       elif len(command) == 3:
           cmd_str = ' '.join(command[0])
           dev_str = ' '.join(command[1])
           arg_str = ' '.join(command[2])

           #文字列数字の処理(数字化)をしつつコマンド内容で分岐
           num_arg = 0
           try:
               num_arg = int(arg_str.split()[0])
           except ValueError as err:
               print(f"expected number but got : {arg_str[0]}")

           if 'increase' in cmd_str and num_arg > 0:
               self.open_actions[dev_str](num_arg)
           elif 'decrease' in cmd_str and num_arg > 0:
               self.close_actions[dev_str](num_arg)


##### 動作確認
def main():

   my_smart_house = MySmartHouse()
   my_smart_house.run("increase -> fridge temperature -> 3 degrees")
   my_smart_house.run("decrease -> fridge temperature -> 6 degrees")
   

   my_smart_house.run("increase -> boiler temperature -> 3 degrees")
   my_smart_house.run("decrease -> boiler temperature -> 6 degrees")


   my_smart_house.run("open -> garage")
   my_smart_house.run("close -> garage")

   my_smart_house.run("turn on -> air condition")
   my_smart_house.run("turn off -> air condition")

if __name__ == "__main__":
   main()

# 実行結果
increase the fridge temerature +3
decrease the fridge temerature -6
increasing the boiler's temperature +3 degrees
decreasing the boiler's temperature -6 degrees
opening the garage
closing the garage
turning on the air condition
turning off the air condition

Enumについては以下参照。

※第3回でもenumを使っているが、こちらは「Functional API」版を使っている。


もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。