読者です 読者をやめる 読者になる 読者になる

内部DSLパターン (1)

python デザインパターン

Rubyによるデザインパターン』(ラス・オルセン著 ピアソン・エデュケーション刊)の例をPythonに変換して書いています。目次

今回は、内部DSL(ドメイン特化言語)パターンです。
前回のInterpreterパターンで、特定のタスクのための簡単な言語を作りました。このような小言語を、DSL(ドメイン特化言語)と言います。

動的言語であるRubyは、幾つか関数・クラスを定義するだけで、ユーザーのDSLになるように仕立て上げる事が出来ます。

これを、内部DSLと言います。

もちろん、Pythonでも同じように内部DSLを作れます
・・・と、言いたい所ですが、チョット厳しいかも?
バックアッププログラムを作る事を考えます。

そのDSLは次のような文法です。バックアップするファイルの条件式は、前回作ったものを流用します。

#backup.pr

#Rubyフォルダの、拡張子がtmpで無いファイルを全てバックアップ
backup('d:/Owner/My Documents/Ruby', ~filename('*.tmp'))

#My Musicフォルダの音楽ファイルをバックアップ
backup('d:/Owner/My Documents/My Music', filename('*.mp3') | filename('*.ogg'))

#Pythonフォルダの全ファイルをバックアップ
backup('d:/Owner/My Documents/Python')

#バックアップ先
to('F:/backups')

#60分ごとにバックアップ
interval(60)

さて、これをどうやって構文解析するかですが、
正規表現やPLYを使ってもよいのですが、よく見るとこのDSLは、Pythonの文法と同じです。

ならば、手っ取り早くPythonを流用してしまいましょう

from finder import *

#DSL用の関数(仮)
def backup(dirpath, find_expression=All()):
    print "backup called, source dir=%s, find expr=%s"%(
                dirpath, find_expression)

def to(dirpath):
    print "to called, backup dir=%s"%(dirpath)

def interval(minutes):
    print "interval called, minutes=%s"%(minutes,)

defstr = open("backup.pr").read() # バックアップ設定ファイルの読み込み

exec defstr #文字列をPythonスクリプトであるかのように実行

内部DSLの利点は、Pythonの機能をそのまま流用できる事。たとえば、パス名にシングルクオートやバックスラッシュが入っている場合、

to("F:\\doloop\'s backup")

等と、エスケープする必要がありますが、
エスケープを処理する正規表現を書くのは退屈な作業です。内部DSLなら、こういったものをタダで手に入れられるのです。

足りないところを補完したのが次のコードです。

from finder import *
import os
import time
import shutil

class Backup(object):
    def __init__(self):
        self.data_sources = []
        self.backup_directory = "C:/backup"
        self.interval = 60
    
    def append(self, data_source):
        self.data_sources.append(data_source)
    
    def set_backup_directory(self, directory):
        self.backup_directory = directory
    
    def set_interval(self, minutes):
        self.interval = minutes
    
    def backup_files(self):
        this_backup_dir  = time.strftime("%Y-%m-%d %H-%M-%S")
        this_backup_path = os.path.join(self.backup_directory, this_backup_dir)
        
        for source in self.data_sources:
            source.backup(this_backup_path)
    
    def run(self):
        while True:
            self.backup_files()
            time.sleep(self.interval*60)

Backup = Backup() #簡単シングルトン


class DataSource(object):
    def __init__(self, directory, find_expression):
        self.directory = directory
        self.find_expression = find_expression
    
    def backup(self, backup_directory):
        files = self.find_expression.evaluate(self.directory)
        for f in files:
            self.backup_file(f, backup_directory)
    
    def backup_file(self, filepath, backup_directory):
        if not os.path.isdir(backup_directory):
            os.makedirs(backup_directory)
        shutil.copy(filepath, backup_directory)
        

def backup(dirpath, find_expression=All()):
    Backup.append(DataSource(dirpath, find_expression))

def to(dirpath):
    Backup.set_backup_directory(dirpath)

def interval(minutes):
    Backup.set_interval(minutes)

defstr = open("backup.pr").read() # バックアップ設定ファイルの読み込み

exec defstr #文字列をPythonスクリプトであるかのように実行

Backup.run()