見出し画像

DP.20:ステートセーブ・ロードを実現する - Mementoパターン -【Python】

【1】Mementoパターン

Mementoパターンは

対象オブジェクトが持つ値の「スナップショットを作成」したり、必要な時にスナップショットから「オブジェクトに値を再設定」したりできるようにする

書き方。要するに「ステートセーブ・ステートロード(UnDo等も含む)」をできるようにするというもの。

【2】Mementoパターンで使う用語

Mementoパターンには「Mement」「Originator」「Caretaker」という3つの用語が出てくる。

【Memento】
「Originator」が出力したり、読み込んだりするオブジェクト。ようは「スナップショット」相当。

【Originator】

ステートセーブ・ロードなどをさせたいオブジェクト。自身の現在の状態を「Mementoオブジェクト(スナップショット)」を出力したり、以前に作成した「Mementoオブジェクト」読み込んで値を戻したりする機能をもつ。

【Caretaker】
メインプログラム側などステートセーブ・ロードなどを起動するトリガー役。(main関数などのメインルーチン内に含まれていることもある)

■イメージ図1:OriginatorとMementoの関係
「Originator」が自身の状態・値を「mementoオブジェクト」に吐き出したり、「mementoオブジェクト」を読み込んで自身の状態・値を巻き戻す。

画像2

■イメージ図2:Caretakerの役割(ステートセーブ)
プログラム上は、「Caretakerオブジェクト」が「Originatorのメソッド」をコールして、「Mementoオブジェクト」を出力させる。

画像2


■イメージ図3:Caretakerの役割(ステートロード)
Caretakerオブジェクトが、MementoオブジェクトをOriginatorに渡しつつ、「Originatorのメソッド」をコールする

画像3


【3】利用例

ZODB(Zope Object Database)」というオブジェクトデータベースがある。python のオブジェクトを永続化して保存できる、トランザクション、アンドゥ可能な履歴機能をサポートしている、という点が特徴。

「Pyramid(ウェブフレームワーク)」など、他のアプリの組み合わせて使われることもある。

ZODBがMementoパターンを使ったソフトウェアの一例といえる。


【4】コンセプトコード

状態を1つだけもたせたOriginatorを使った、簡単なコンセプトコードを書いてみる。具体的には

①「Originatorのクラス変数:_state」を「pickleオブジェクト」として出力しておき、main関数から書き換える。

■mementoを作る

画像4

■Originatorの状態を書き換える

画像5

② 事前に出力した「pickleオブジェクト」から「Originatorクラスの変数:_state」の値を元に戻す

■Mementoをロードして元に戻す

画像6

というもの。

【コンセプトコード】

import pickle


class Originator:
   def __init__(self):
       self._state = False # セーブやロードをしたいステート:状態
       print(vars(self))

   def set_memento(self, memento:bytes):
       previous_state = pickle.loads(memento)
       print(vars(self))
       
       vars(self).clear() # クラスの属性(メンバ変数)をクリアする
       print(vars(self))
       
       vars(self).update(previous_state)
       print(vars(self))


   def create_memento(self):
       return pickle.dumps(vars(self))


def main():
   originator = Originator()
   memento = originator.create_memento()
   originator._state = True
   originator.set_memento(memento)


if __name__ == "__main__":
   main()

実行結果
{'_state': False}   # 初期状態
{'_state': True}    # クラス属性の書き換え時点
{}     # クラス属性のclear()時点
{'_state': False}   # クラス属性のupdate()時点

▲今回はオブジェクトの状態を保存するMementoとしてpython固有のデータフォーマットである「pickleオブジェクト」を利用した。

【pickleオブジェクト】

pickleオブジェクトについては以下参照。簡単に言うとpython固有のデータフォーマット(バイトデータ)

▲「赤枠で警告事項」が書かれていてひるむかもしれない。ざっくりいうと、『ネット上から拾ったもの、他人からもらったpickleオブジェクトなどを安易に読み込まないこと。』ということ。
これは、pickleオブジェクトはバイトデータ群なので悪意のあるプログラムが何かしら仕込まれていると実行されてしまうからである。

■Originatorへのデータセットの仕方

Originatorへのデータ再セット方法として今回は「vars()」と「update()」をつかって Originatorクラス内の「__dict__ 属性」(今回の場合は、self._state)を上書きしている。

【5】例題:Quote(名言・格言)管理・出力プログラム

誰かが話したり、書いたりした名言・格言を一時的に記憶しておき、修正したり元に戻すプログラムをつくってみる。

名言・格言を保存しておくオブジェクトは次の通りにする。
【Quoteクラスオブジェクト】

import pickle
from dataclasses import dataclass #dataclassで__init__()は自動生成

@dataclass
class Quote:
   text:str
   author:str

   # ステートセーブ
   def save_state(self):
       current_state = pickle.dumps(self.__dict__) # データダンプ
       return current_state # ダンプされたバイトデータを返す

   # ステートロード
   def restore_state(self, memento):
       previous_state = pickle.loads(memento) # ダンプされたデータをロード
       print(self.__dict__)

       self.__dict__.clear() # クラス変数をクリア
       print(self.__dict__)
       
       self.__dict__.update(previous_state) # クラス変数をロードしたダンプデータから更新
       print(self.__dict__)


   def __str__(self):
       return f"{self.text} by {self.author}"
 

▲今回はPickleオブジェクトとしてダンプしたデータは、外部ファイルとして書き出していない。
「save_state()」でPickleオブジェクトのバイトデータにしてメモリ上に用意しておき、すぐプログラム上から「restore_state()」で戻せるようなプログラムになっている。

【6】全体コード

import pickle
from dataclasses import dataclass



@dataclass
class Quote:
   text:str
   author:str

   def save_state(self):
       current_state = pickle.dumps(self.__dict__)
       return current_state

   def restore_state(self, memento):
       previous_state = pickle.loads(memento)
       print(self.__dict__)

       self.__dict__.clear()
       print(self.__dict__)
       
       self.__dict__.update(previous_state)
       print(self.__dict__)


   def __str__(self):
       return f"{self.text} by {self.author}"


def main():
   print("Quote 1")
   
   # Unknown authorとして名言を登録
   q1 = Quote("A room without books is like a body without a soul.", 'Unknown author')
   print(f'\nOriginal version:\n{q1}')
   q1_mem = q1.save_state() # ステートセーブ

   # 値の書き換え
   q1.author = 'Marcus Tullius Cicero'
   print(f'\nWe found the author, and did an updated:\n{q1}')
   
   print("")

   # ステートロード(書き換えたauthor部分がもとにもどる)
   q1.restore_state(q1_mem)
   
   # q1オブジェクト内の値がUnknown authorにもどっていることを確認する
   print(f'\nWe had to restore the previous version:\n{q1}')

   print("-------------------")

if __name__ == '__main__':
   main()

【実行結果】

Quote 1

# もともとの「q1オブジェクトの状態」(この後ステートセーブ実施)
Original version:
A room without books is like a body without a soul. by Unknown author

# 値を直接書き換えたあとの「q1オブジェクト」の状態
We found the author, and did an updated:
A room without books is like a body without a soul. by Marcus Tullius Cicero

# restore_state()内の処理を出力
{'text': 'A room without books is like a body without a soul.', 'author': 'Marcus Tullius Cicero'} # pickleバイトデータロード直後(まだ変わらない)
{} #「__dict__.clear()」をコール
{'text': 'A room without books is like a body without a soul.', 'author': 'Unknown author'} #「__dict__.update()」をコール(元に戻る)

# ステートロード後「q1オブジェクト」の状態
We had to restore the previous version:
A room without books is like a body without a soul. by Unknown author



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