内部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は絶対に使ってはいけません!!