あると便利なデータクラスを自分仕様で作る。
withやforに対応できるデータクラス。
コンテキスト対応でイテレータブルなデータクラスを作る!
クローズの心配をしなくてよいとか実運用時にはさっと書けるのでとても便利です。
折角あるのだし使いましょうというのが今回の趣旨です。
それぞれ何をすればよいのか
コンテキスト対応
__enter__
__exit__
以上のメソッドを持てば良いです。
イテレータ対応
__iter__
__next__
以上のメソッドを持てば良いです。
データの出し入れ
変数の重複などを防ぐために一つの変数へアクセスすることにします。
__setitem__
__getitem__
以上のメソッドを持ち、
また、データ削除に対応するため
__delitem__
以上に対応すれば余計な変数へのアクセスを防ぎトラブルを回避することが出来そうです。
コード
説明すっ飛ばし気味ですがコードは以下の通りになりました。
以下を dataobj.py として保存します
データを入れる際、入れるデータが更新されているかどうかのチェックをしていて、これは _isUpdate
に格納されます。
保存の際などに参照して必要が無ければファイルアクセスしない為の目安に出来ると思います。
class DataObj:
def __init__(self, **kwargs):
self._isUpdate = False
self._data = {}
self._iter_pos = 0
self._param = {}
for key in kwargs:
self._param[key] = kwargs[key]
# key一覧を返す
def keys(self):
return self._data.keys()
def open(self, filename=None):
pass
def close(self):
pass
def __setitem__(self, key, value):
# 更新されたかどうかをフラグで管理
if key in self._data.keys():
if self._data[key] != value:
self._isUpdate = True
else:
self._isUpdate = True
# データを格納する
if self._isUpdate:
self._data[key] = value
def __getitem__(self, key):
# データを取り出す
return self._data.get(key)
def __delitem__(self, key):
# 項目を削除する
if key in self._data.keys():
del(self._data[key])
self._isUpdate = True
#
# コンテキストマネージャ
#
def __enter__(self):
self.open()
return self
def __exit__(self, exctype, excvalue, traceback):
self.close()
#
# イテレータブル
#
def __iter__(self):
self.open()
return self
def __next__(self):
keys = list(self.keys())
if self._iter_pos == len(keys):
self._iter_pos = 0
self.close()
raise StopIteration()
buf = self.iterItemGetter(keys[self._iter_pos])
self._iter_pos += 1
return buf
# オーバーライドで楽できるように分けておく
def iterItemGetter(self, key):
return (key, self._data[key])
使い方。
# 通常の方法で使う
obj = DataObj()
obj["abc"] = 123
obj["hello"] = "world"
print(obj["abc"])
print(obj["hello"])
obj.close()
# withで使う
with DataObj() as obj:
obj["abc"] = 123
print(obj["abc"])
# forで使う ※現時点ではデータが無いので何も出力されない
for data in DataObj():
print(data)
# 中のデータを消すにはこんな感じ。
with DataObj() as obj:
del obj["abc"]
このままではただ一時的にデータを入れたり出したりできる入れ物でしかありませんが、基底クラスとして利用し、JSONやpickle、その他諸々への対応が出来そうです。
JSON対応データクラス
一つやってしまえば後は似たようなものです。
実装しなければならないのは基本的にopenとcloseですがその他は必要に応じて実装します
import dataobj
import os
import json
class JSONObj(DataObj):
def open(self, filename=None):
if "filename" in self._param.keys():
filename = self._param["filename"]
if os.path.exists(filename):
with open(filename, "r") as fr:
self._data = json.load(fr)
def close(self):
if self._isUpdate and "filename" in self._param.keys():
with open(self._param["filename"], "w") as fw:
json.dump(self._data, fw, ensure_ascii=False, indent=4)
使い方はDataObjと一緒ですが、データが保存されるようになったのでfor文で値が出てくるあたりが違います。
個人的な主観ではありますが、業務などに於いてデータをJSONで扱うと何かトラブルが有った際などに当事者が自己解決してくれる確率が増えるように思いますし、原因の特定までの時間を短縮するのに便利。
データベースを使うまでもない小規模なデータなどにはこういったものを使うのも良いのかもしれませんね。
尚、何かしらのエラーで空のファイルが出来てしまい、それを読み込もうとするとエラーになって先に進まなくなります。例外処理は入れてありませんので必要に応じてその辺りはどうにかしてください。これはあくまでサンプルコードです。
使い方は大体同じですがファイル名を指定しないと保存されませんので指定して利用します
with JSONObj(filename="hello.json") as obj:
obj["abc"] = 123
with JSONObj(filename="hello.json") as obj:
print(obj["abc"])
for data in JSONObj(filename="hello.json"):
print(data)
pickle対応データクラス
import dataobj
import os
import pickle
class PickleObj(DataObj):
def open(self, filename=None):
if "filename" in self._param.keys():
filename = self._param["filename"]
if os.path.exists(filename):
with open(filename, "rb") as fr:
self._data = pickle.load(fr)
def close(self):
if self._isUpdate and "filename" in self._param.keys():
with open(self._param["filename"], "wb") as fw:
pickle.dump(self._data, fw)
ほとんどコードに違いはありませんが、pickleはインスタンスなども保存できるのでJSONでは物足りない場合に便利です。Javaのシリアライザブルなオブジェクトと似たような使用感です。
しかし業務レベルになるとほぼ出番がないのがコレです…。
データがバイナリなので多少の隠ぺい能力があります。(ほぼ無いに等しいですが)
データの暗号化のコード等を仕込んでやればデータ流出対策などに対応することが。もしかしたら出来るのかもしれません。
まとめ
ということで主題としてはコンテキストマネージャ対応、イテレータブルなオブジェクトを作る話でした。
分かりやすいところでデータクラスを作りましたがそれ以外にも最後に閉じなきゃいけないケース(ネットワークとかシリアル通信とか)にも適用できる話かと思います。
今回のサンプルについては単なるシリアライズデータオブジェクトとして使う以外にアプリケーション間のデータのやり取りなどにもそこそこ便利だった記憶があります。
頻繁にやり取りするアプリケーションならばネットワークを利用したほうが良いですが手を抜けるところはとことん抜いて素早くコーディングしたほうが精神的にも良いので…。
楽して楽しくPythonを使ってゆきたいですね~:D