内部DSLパターン (2) ブロック

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

Rubyが内部DSLに向いている一つの理由は、ブロック構文の存在です。


Pythonでも、with文を使えば似たような事が出来ます。


前回のバックアップスクリプトは、十分機能的でしたが、バックアップの設定を1つしか書けないのが欠点でした。そこで、DSLの書式を変更します。

with Backup() as b:
    b.backup('d:/Owner/My Documents/Ruby', ~filename('*.tmp'))
    b.to('F:/backup')
    b.interval(30)

with Backup() as b:
    b.backup('d:/Owner/My Documents/My Music', 
              filename('*.mp3') | filename('*.ogg'))
    b.to('F:/backup')
    b.interval(60)

with 文は、Python 2.5から導入されました。withを使えば、Rubyのブロックのような事が出来ます。

前回は、Backupクラスはシングルトンでプログラムの根幹でしたが、今回は、Backupの上に、シングルトンBackupProgramを作り、BackupProgramがプログラムの根幹になっています。

from __future__ import with_statement #Python 2.5用、2.6以降は不要

from finder import *
import os
import time
import shutil
import threading

class SimpleThread(threading.Thread):
    def __init__(self, acallable, *a, **kw):
        self.a = a
        self.kw = kw
        self.acallable = acallable
        self._result = None
        super(SimpleThread, self).__init__()
    
    def run(self):
        self._result = self.acallable(*self.a, **self.kw)
    
    def result(self):
        return self._result
    
    
class BackupProgram(object):
    def __init__(self):
        self._backups = []
    
    def register(self, backup):
        self._backups.append(backup)
    
    def run(self):
        #スレッドで並列処理
        threads = []
        for backup in self._backups:
            t = SimpleThread(backup.run)
            t.start()
            threads.append(t)
        
        for t in threads:
            t.join()

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

class Backup(object):
    def __init__(self):
        self._data_sources = []
        self._backup_directory = "C:/backup"
        self._interval = 60
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        BackupProgram.register(self)
    
    def backup(self, dirpath, find_expression=All()):
        self._data_sources.append(DataSource(dirpath, find_expression))
    
    def to(self, dirpath):
        self._backup_directory = dirpath
    
    def 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)


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)
        
defstr = open("backup.pr2").read() # バックアップ設定ファイルの読み込み

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

BackupProgram.run()

Backupクラスは、__enter__メソッドと__exit__メソッドを定義する事で、with文で使用可能になります。

with Backup() as b:
    b.backup('d:/Owner/My Documents/Ruby', ~filename('*.tmp'))
    b.to('F:/backup')
    b.interval(30)

with文に入ったときに__enter__が呼び出され、as で指定された変数(上で言えば b )に、__enter__の返値が代入されます。

そして、with文を抜けたときに、__exit__が呼ばれます。

__exit__( self, exc_type, exc_value, traceback) 

つつがなくwith文の中身が実行されたときは_exit__の引数は全てNone、例外が発生したときは、その情報が与えられます。

このように、内部DSLは簡単かつ強力ですが、欠点もあります。

まず、エラーメッセージが分かり難いことです。

with Backup() as b:
    b.backup('d:/Owner/My Documents/Ruby', ~filename('*.tmp'))
    b.to('F:/backup')
    x.interval(30) # NameError: name 'x' is not defined

このときのエラーメッセージは、ライトユーザーには不親切でしょう。何とかできなくもないですが、完璧にやるのは無理です。

また、execの使用から生じる問題もあります。

内部DSLのスクリプトには、Pythonプログラムなら何でも書くことが出来ます。

したがって、悪意あるユーザーが、プログラムを簡単に乗っ取る事ができます。

セキュリティが重要な場合、内部DSLは絶対に使ってはいけません!!