はじめに
Pythonで辞書を使うとき地味に面倒なので、[KEYNAME]
での参照です。辞書をdataclass や namedtuple のようにドット表記でアトリビュート参照するように値にアクセスできるようにしたライブラリが datajuggler です。
先人たちの功績のおかげ
といっても別段真新しいものではなくて、以下の素晴らしいプロジェクトをリファクタリングやラッピングして統合させたただけのものです。
dataclass や namedtuple があるのになぜ?
dataclass や namedtuple は値を保持するという目的では十分に機能するのですが、
dataset を使ってデータベースにアクセスしているような場合に、面倒な場合があることがモチベーションになりました。
In [1]: from datajuggler import serializer as io
In [2]: io.read_contents('sqlite:///users.sqlite#users')
Out[2]:
[{'id': 1, 'name': 'David Coverdale', 'age': 71, 'belongs': 'Whitesnake'},
{'id': 2, 'name': 'Neal Schon ', 'age': 68, 'belongs': 'Journey'},
{'id': 3, 'name': 'Tom Scholz', 'age': 75, 'belongs': 'Boston'},
{'id': 4, 'name': 'David Gilmour', 'age': 75, 'belongs': 'Pink Floyd'},
{'id': 5, 'name': 'Ann Wilson', 'age': 71, 'belongs': 'Heart'},
{'id': 6, 'name': 'Nacy Wilson', 'age': 67, 'belongs': 'Heart'}]
In [3]:
ここにある、io.read_contents()
はdatasetのラッパーです。
datasset はデータベースを読み込んで辞書のリストを返してくれます。
dataset.connect()
に row_type
で型を与えるともっと理解しやすい結果になります。そこで、次のようなクラスを作ることになります。
In [8]: # %load users_class.py
...: class User:
...:
...: def __init__(self, *nargs, **kwargs):
...: self.data = dict(*nargs, **kwargs)
...: self.id: int = self.data['id']
...: self.name: str = self.data['name']
...: self.age: int = self.data['age']
...: self.belongs: str = self.data['belongs']
...:
...: def __repr__(self):
...: return(f'User(id={self.id}, '
...: f'name="{self.name}", '
...: f'age={self.age}, '
...: f'belongs="{self.belongs}")' )
...:
...:
In [9]: users = io.read_contents('sqlite:///users.sqlite#users',row_type=User)
In [10]: users
Out[10]:
[User(id=1, name="David Coverdale", age=71, belongs="Whitesnake"),
User(id=2, name="Neal Schon ", age=68, belongs="Journey"),
User(id=3, name="Tom Scholz", age=75, belongs="Boston"),
User(id=4, name="David Gilmour", age=75, belongs="Pink Floyd"),
User(id=5, name="Ann Wilson", age=71, belongs="Heart"),
User(id=6, name="Nacy Wilson", age=67, belongs="Heart")]
In [11]: users[0].name
Out[11]: 'David Coverdale'
In [12]:
このUser
クラスを使うことでもドット表記でデータにはアクセスできます。
ここで、__repr__()
を省略してもこのクラスは定義することはできますが、次のような結果になるため実用的ではなくなります。
In [4]: class User:
...:
...: def __init__(self, *nargs, **kwargs):
...: self.data = dict(*nargs, **kwargs)
...: self.id: int = self.data['id']
...: self.name: str = self.data['name']
...: self.age: int = self.data['age']
...: self.belongs: str = self.data['belongs']
...:
In [5]: io.read_contents('sqlite:///users.sqlite#users',row_type=User)
Out[5]:
[<__main__.User at 0x1092c0f40>,
<__main__.User at 0x1092c3430>,
<__main__.User at 0x1092c0cd0>,
<__main__.User at 0x1092c1d20>,
<__main__.User at 0x1092c26e0>,
<__main__.User at 0x1092c1660>]
In [6]:
結果として、都度__init__()
や __repr__()
の定義することになり、地味に面倒です。
そうしたことから namedtuple が生まれているわけです。
namedtuple の残念なところ
namedtuple でも良さそうなのですが、この例の場合は、初期化時に辞書が渡されるため、安直に __init__()
を再定義しようとしてもエラーになり定義できません。
In [18]: from typing import NamedTuple
...:
...: try:
...: class User(NamedTuple):
...: id: int
...: name: str
...: age: int
...: belongs: str
...:
...: def __init__(self, profile):
...: d = dict(profile)
...: self.id = d['id']
...: self.name = d['name']
...: self.age = d['age']
...: self.belongs = d['belongs']
...:
...: except AttributeError as e:
...: print(e)
...:
Cannot overwrite NamedTuple attribute __init__
In [19]:
__init__()
だけでなく __new__()
の再定義も許されません。
この問題を回避するためには、次のようにクラスを階層化します。
In [20]: from typing import NamedTuple
...:
...: class UserBase(NamedTuple):
...: id: int
...: name: str
...: age: int
...: belongs: str
...:
...: class User(UserBase):
...: def __new__(cls, profile):
...: return super().__new__(cls, **dict(profile))
...:
In [21]:
これでも良いとは言えるのですが、__init__()
の引数について辻褄をあわせるためだけにクラスを定義するのは、すこし残念です。
dataclass の残念なところ
namedtuple と同じような目的で、dataclass を使って実装することもできます。
この場合は、__init__()
の引数を辻褄を合わせるためには次のようにします。
In [1]: from datajuggler import serializer as io
In [2]: from dataclasses import dataclass, field
...:
...: @dataclass
...: class User:
...: id: int = field(init=False)
...: name: str = field(init=False)
...: age: int = field(init=False)
...: belongs: str = field(init=False)
...:
...: def __init__(self, *nargs, **kwargs):
...: self.data = dict(*nargs, **kwargs)
...: self.id = self.data['id']
...: self.name = self.data['name']
...: self.age = int(self.data['age'])
...: self.belongs = self.data['belongs']
...:
...:
In [3]:
datatclass の利点がほとんどなくなるうえに、フィールドごとに field(init=False)
が必要になるわけです。これも地味に面倒です。
datajuggler の aDict を使うと簡潔に記述できる
datajuggler の aDict は dataclass や namedtuple のようにドット表記で値にアクセスできる辞書です。
In [1]: from datajuggler import serializer as io
In [2]: from datajuggler import aDict
In [3]: class User(aDict):
...: pass
...:
In [4]: users = io.read_contents('sqlite:///users.sqlite#users',row_type=User)
In [5]: users
Out[5]:
[User({'id': 1, 'name': 'David Coverdale', 'age': 71, 'belongs': 'Whitesnake'}),
User({'id': 2, 'name': 'Neal Schon ', 'age': 68, 'belongs': 'Journey'}),
User({'id': 3, 'name': 'Tom Scholz', 'age': 75, 'belongs': 'Boston'}),
User({'id': 4, 'name': 'David Gilmour', 'age': 75, 'belongs': 'Pink Floyd'}),
User({'id': 5, 'name': 'Ann Wilson', 'age': 71, 'belongs': 'Heart'}),
User({'id': 6, 'name': 'Nacy Wilson', 'age': 67, 'belongs': 'Heart'})]
In [6]: users[0].name
Out[6]: 'David Coverdale'
In [7]:
aDict はイミュータブルにもできる
namedtuple は名前通りにイミュータブルなオブジェクトです。dataclass でもfrozen=True
を設定することで、フィールド単位やクラス単位でイミュータブルな
オブジェクトにすることができます。aDict では、freeze()
メソッドを呼び出すと、以後unfreeze()
が呼び出されるまではイミュータブルなオブジェクトとなります。
In [8]: users[0].freeze()
In [9]: try:
...: users[0].age=20
...: except AttributeError as e:
...: print(e)
...:
User frozen object cannot be modified.
In [10]:
また、aDict の frozen オブジェクトはハッシュすることができるため、
辞書のキーとして使用することもできます。
In [25]: _ = [x.freeze() for x in users ]
In [26]: artist = {x: x.belongs for x in users}
In [27]: artist
Out[27]:
{User({'id': 1, 'name': 'David Coverdale', 'age': 71, 'belongs': 'Whitesnake'}): 'Whitesnake',
User({'id': 2, 'name': 'Neal Schon ', 'age': 68, 'belongs': 'Journey'}): 'Journey',
User({'id': 3, 'name': 'Tom Scholz', 'age': 75, 'belongs': 'Boston'}): 'Boston',
User({'id': 4, 'name': 'David Gilmour', 'age': 75, 'belongs': 'Pink Floyd'}): 'Pink Floyd',
User({'id': 5, 'name': 'Ann Wilson', 'age': 71, 'belongs': 'Heart'}): 'Heart',
User({'id': 6, 'name': 'Nacy Wilson', 'age': 67, 'belongs': 'Heart'}): 'Heart'}
In [28]:
辞書データとしてそのままキーにできるため、何かと便利です。
In [28]: gilmour = users[3]
In [29]: gilmour
Out[29]: User({'id': 4, 'name': 'David Gilmour', 'age': 75, 'belongs': 'Pink Floyd'})
In [30]: gilmour in artist
Out[30]: True
In [31]:
iListについて
aDict はイミュータブルを制御でき、ハッシュ可能なオブジェクトを作れるので、これを利用したリストが iList です。
通常、リストオブジェクトはアトリビュートを追加できません。
In [31]: l1 = list([1, 2, 3])
In [32]: l1.python=’osaka'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In [32], line 1
----> 1 l1.python='osaka'
AttributeError: 'list' object has no attribute 'python'
In [33]:
これが、iList では可能になります。
In [38]: i1 = iList([1, 2, 3])
In [39]: i1.python='Osaka'
In [40]: i1
Out[40]: iList([1, 2, 3])
In [41]: i1.python
Out[41]: 'Osaka'
In [42]:
これは内部に aDict を保持しているためです。
In [42]: i1._attrs
Out[42]: aDict({'python': 'Osaka'})
In [43]:
listとの互換性
iList 自体は list と互換性を保っているため、
通常のリスト操作についても問題なく、かつ、辞書を内包しているオブジェクトとなります。
In [1]: from datajuggler import iList
In [2]: l1 = iList([1,2,3,4,5])
...: l2 = iList([1,2,3,4,5])
...: assert l1 == l2
In [3]: l1 = iList([1,2,3,4,5])
...: l2 = list([1,2,3,4,5])
...: assert l1 == l2
In [4]: l1 = iList([5,4,3,2,1])
...: l2 = iList([1,2,3,4,5])
...: assert l1 != l2
In [5]: l1 = iList([5,4,3,2,1])
...: l2 = list([1,2,3,4,5])
...: assert l1 != l2
In [6]: l1 = iList([1,2,3])
...: l2 = list([4,5,6])
...: assert l1 + l2 == [1,2,3,4,5,6]
In [7]: l1 = iList([1,2,3])
...: l2 = iList([4,5,6])
...: assert l1 + l2 == [1,2,3,4,5,6]
In [8]: l1 = iList([1,2,3,4,5,6])
...: l2 = iList([4,5,6])
...: assert l1 - l2 == [1,2,3]
In [9]: l1 = iList([1,2,3,4,5,6])
...: l2 = list([4,5,6])
...: assert l1 - l2 == [1,2,3]
In [10]: l1 = iList([1,2,3])
...: assert l1 * 3 == [1,2,3,1,2,3,1,2,3]
In [11]: l1 = iList([1,2,3,4,5,6])
...: l2 = iList([4,5,6])
...: assert ( l1 & l2 ) == [4,5,6]
In [12]: l1 = iList([1,4,2,5,3,6])
...: l2 = list([6,4,5])
...: assert (l1 & l2 ) == [4,5,6]
In [13]: l = iList([1,2,3,[4,5]])
...: assert l.find(2) == [1]
In [14]: l = iList([1,2,3,[4,5]])
...: assert l.find(0) == None
In [15]: l = iList([1,2,3,[4,5]])
...: assert l.find([2,3]) == [1,2]
In [16]: l1 = iList([])
...: l1.append(4)
...: assert l1 == [4]
In [17]: l1 = iList([])
...: l1.append([4,5,6])
...: assert l1 == [[4,5,6]]
In [18]: l1 = iList([1,2,3,4,5,6])
...: _ = l1.pop()
...: assert l1 == [1,2,3,4,5]
In [19]: l1 = iList([1,2,3,4,5,6])
...: _ = l1.pop(0)
...: assert l1 == [2,3,4,5,6]
In [20]: l1 = iList([1,2,3,4,5,6])
...: l1.remove(3)
...: assert l1 == [1,2,4,5,6]
In [21]: l1 = iList([1,2,3,4,5,6,7,8,9])
...: v = l1[3:5]
...: assert v == [4, 5]
In [22]:
iList もイミュータブルオブジェクトにできる
aDictを利用してfrozenなオブジェクトにできるため、タプルのようにイミュータブルな挙動をさせることができます。
In [54]: i1
Out[54]: iList([1, 2, 3])
In [55]: i1.freeze()
In [56]: try:
...: i1.append(4)
...: except AttributeError as e:
...: print(e)
...:
iList frozen object cannot be modified.
In [57]:
また、frozen なオブジェクトはハッシュ可能なため、辞書のキーに配置できます。
In [60]: hash(i1)
Out[60]: -3783003762566077833
In [61]: d = { i1: 'happy'}
In [62]: d
Out[62]: {iList([1, 2, 3]): 'happy'}
In [63]:
iList の copy()
copy()
メソッドは、リストをコピーしますが、アトリビュートはコピーしません。
freeze=True
を与えると、frozen なオブジェクトとして生成されます。
In [63]: i1
Out[63]: iList([1, 2, 3])
In [64]: i2 = i1.copy()
In [65]: i2._attrs
Out[65]: aDict({})
In [66]: i2 = i1.copy(freeze=True)
In [67]: hash(i2)
Out[67]: -3783003762566077833
In [68]: i2
Out[68]: iList([1, 2, 3])
In [69]:
iList の clone()
clone()
メソッドは、リストと共にアトリビュートもコピーします。
empty=True
を与えると、アトリビュートだけがコピーされます。
In [69]: i3 = i1.clone()
In [70]: i3
Out[70]: iList([1, 2, 3])
In [71]: i3._attrs
Out[71]: aDict({'python': 'Osaka'})
In [72]: i3 = i1.clone(empty=True)
In [73]: i3
Out[73]: iList([])
In [74]: i3._attrs
Out[74]: aDict({'python': 'Osaka'})
In [75]:
iLst の without()
与えた要素を含まないリストを作成して、イテラルを返します。
要素は複数与えることができます。
In [1]: from datajuggler import iList
In [2]: l1 = iList([1,2,3,4,5,6])
...: result = l1.without([3])
...: assert result == [1,2,4,5,6]
In [3]: l1 = iList([1,2,3,4,5,6])
...: result = l1.without([0])
...: assert result == l1
In [4]: l1 = iList([1,2,3,4,5,6])
...: l2 = [3,4,5]
...: result = l1.without(l2)
...: assert result == [1,2,6]
In [5]: l1 = iList([1,2,3,4,5,6])
...: l2 = [3,4]
...: l3 = [5,6]
...: result = l1.without(l2, l3)
...: assert result == [1,2]
In [6]:
uDictについて
uDict はネストされた辞書データの値参照が容易になる辞書です。
In [1]: from datajuggler import uDict, Keylist, Keypath
In [2]: data = { "a": 1,
...: "b": { "c": { "x": 2, "y": 3, },
...: "d": { "x": 4, "y": 5, },
...: "e": [ { "x": 1, "y": -1, "z": [101, 102, 103], },
...: { "x": 2, "y": -2, "z": [201, 202, 203], },
...: { "x": 3, "y": -3, "z": [301, 302, 303], },
...: ],
...: },
...: }
In [3]: d = uDict(data)
In [4]: d['a']
Out[4]: 1
In [5]: d[Keylist(['b', 'e[1]', 'z[2]'])]
Out[5]: 203
In [6]: d[Keypath('b.e[1].z[2]')]
Out[6]: 203
In [7]:
Keylist オブジェクトは値を特定するキーをリストで保持します。
同様に、Keypath オブジェクトは値を特定するキーを文字列として保持します。
そもそも、こんなデータ構造になっていることが問題です。とわいえ、場合によってはネストしたデータ構造を使用することもあります。
uDict のメソッド
uDict には便利なメソッドが多数提供されています。
clean(d1: dict, strings=True, collections=True, inplace=False, factory=dict)
clone(d1: dict, empty=False, memo=None)
compare(d1: dict, d2: dict, keys=None, thrown_error=False)
counts(pattern, d=None, count_for"key", wild=False, verbatim=False)
filter(predicate, d=None, factory=dict)
get_keys(d=None, output_as=None)
get_values(keys, d=None)
groupby(seq, key, factory=dict)
invert(d=None, flat=False, inplace=False, factory=dict)
keylists(d=None, indexes=False)
keypaths(d=None, indexes=False, separator=".")
map(func, d=None, map_for=None, inplace=False, factory=dict)
merge(other, d=None, overwrite=False, inplace=False, factory=dict)
move(key_src, key_dest, d=None, overwrite=False, inplace=False, factory=dict)
nest(items, key, patrent_key, children_key)
rename(key, key_new, d=None, case_name=None, overwrite=False, inplace=False, factory=dict)
remove(keys, d=None, inplace=False, factory=dict)
subset(keys, d=None, default=None, use_keypath=False, separator=".", inplace=False, factory=dict)
find(keys, d=None, default=None, first_one=True, factory=dict)
search(query, d=None, search_for="key", exact=False, ignore_case=False)
sort(d=None, sort_by="key", reverse=False, inplace=False, factory=dict)
swap(key1, key2, d=None, inplace=False, factory=dict)
flatten(d=None, separator=".", inplace=False, factory=dict)
unflatten(d=None, default=None, separator=".", inplace=False, factory=dict)
traverse(callback, d=None, parents=[], *args, **kwargs)
unique(d=None)
get_items(loc, value, d=None, func=None, separator='.',inplace=False, factory=dict)
pop_items(loc, value, d=None, func=None, separator='.',inplace=False, factory=dict)
del_items(loc, value, d=None, func=None, separator='.',inplace=False, factory=dict)
set_items(loc, value, d=None, func=None, separator='.',inplace=False, factory=dict)
実は、これらのメソッドは datajuggler.dicthelper で定義されている次のヘルパー関数を呼び出しているだけなので、ノーマルな辞書やaDictでも利用することができます。
d_clean()
d_clone()
d_compare()
d_counts()
d_filter()
d_groupby()
d_invert()
d_map()
d_merge()
d_move()
d_rename()
d_remove()
d_nest()
d_subset()
d_find()
d_sort()
d_search()
d_swap()
d_flatten()
d_unflatten()
d_traverse()
d_unique()
get_keys()
get_values()
get_items()
pop_items()
del_items()
set_items()
Srializer
datajuggler がサポートしているシリアル化のフォーマットは次の17種類です。
- bson, dill, json, msgpack, phpserialize, pickle, serpent, yaml
- json:cusom, yaml:cusom, msgpack:custom, toml, xml
- querystring, ini, csv, cloudpickle,
- base64(support encrypt/decrypt)
次のパッケージがインストールされていれば、それを認識して有効になります。
- cloudpickle
- bson
- dill
- msgpack
- serpent
- PyYAML
- toml
- xmllibtodict
それぞれのシリアル化ライブラリは、統一的なAPIで操作できようになっています。
In [1]: from datajuggler import serializer as io
In [2]: import decimal
In [3]: import datetime
In [4]: data = {
...: 'a': 1,
...: 'b': decimal.Decimal('2'),
...: 'c': datetime.datetime(2020, 5, 24, 8, 20),
...: 'd': datetime.date(1962, 1, 13),
...: 'e': datetime.time(11, 12, 13),
...: 'f': [1, 2, 3, decimal.Decimal('4')]
...: }
In [5]: io.dumps(data, format='json')
Out[5]: b'{"a": 1, "b": {"__class_name__": "<class \'decimal.Decimal\'>", "__dumped_obj__": {"__type__": "Decimal", "value": "2"}}, "c": {"__class_name__": "<class \'datetime.datetime\'>", "__dumped_obj__": {"__type__": "datetime", "value": [2020, 5, 24, 8, 20, 0]}}, "d": {"__class_name__": "<class \'datetime.date\'>", "__dumped_obj__": {"__type__": "date", "value": [1962, 1, 13]}}, "e": {"__class_name__": "<class \'datetime.time\'>", "__dumped_obj__": {"__type__": "time", "value": [11, 12, 13]}}, "f": [1, 2, 3, {"__class_name__": "<class \'decimal.Decimal\'>", "__dumped_obj__": {"__type__": "Decimal", "value": "4"}}]}'
In [6]: io.dumps(data, format='msgpack')
Out[6]: b"\x86\xa1a\x01\xa1b\x82\xae__class_name__\xb9<class 'decimal.Decimal'>\xae__dumped_obj__\x82\xa8__type__\xa7Decimal\xa5value\xa12\xa1c\x82\xae__class_name__\xbb<class 'datetime.datetime'>\xae__dumped_obj__\x82\xa8__type__\xa8datetime\xa5value\x96\xcd\x07\xe4\x05\x18\x08\x14\x00\xa1d\x82\xae__class_name__\xb7<class 'datetime.date'>\xae__dumped_obj__\x82\xa8__type__\xa4date\xa5value\x93\xcd\x07\xaa\x01\r\xa1e\x82\xae__class_name__\xb7<class 'datetime.time'>\xae__dumped_obj__\x82\xa8__type__\xa4time\xa5value\x93\x0b\x0c\r\xa1f\x94\x01\x02\x03\x82\xae__class_name__\xb9<class 'decimal.Decimal'>\xae__dumped_obj__\x82\xa8__type__\xa7Decimal\xa5value\xa14"
In [7]: io.dumps(data, format='pickle')
Out[7]: b"\x80\x04\x95\xa8\x01\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01a\x94K\x01\x8c\x01b\x94\x8c\x08builtins\x94\x8c\x07getattr\x94\x93\x94\x8c'datajuggler.serializer.class_serializer\x94\x8c\x16DecimalClassSerializer\x94\x93\x94)\x81\x94\x8c\x06decode\x94\x86\x94R\x94}\x94(\x8c\x08__type__\x94\x8c\x07Decimal\x94\x8c\x05value\x94\x8c\x012\x94u\x85\x94R\x94\x8c\x01c\x94h\x05h\x06\x8c\x17DatetimeClassSerializer\x94\x93\x94)\x81\x94h\n\x86\x94R\x94}\x94(h\x0e\x8c\x08datetime\x94h\x10]\x94(M\xe4\x07K\x05K\x18K\x08K\x14K\x00eu\x85\x94R\x94\x8c\x01d\x94h\x05h\x06\x8c\x13DateClassSerializer\x94\x93\x94)\x81\x94h\n\x86\x94R\x94}\x94(h\x0e\x8c\x04date\x94h\x10]\x94(M\xaa\x07K\x01K\reu\x85\x94R\x94\x8c\x01e\x94h\x05h\x06\x8c\x13TimeClassSerializer\x94\x93\x94)\x81\x94h\n\x86\x94R\x94}\x94(h\x0e\x8c\x04time\x94h\x10]\x94(K\x0bK\x0cK\reu\x85\x94R\x94\x8c\x01f\x94]\x94(K\x01K\x02K\x03h\x0c}\x94(h\x0eh\x0fh\x10\x8c\x014\x94u\x85\x94R\x94eu."
In [8]: io.dumps(data, format='yaml')
Out[8]: b"a: 1\nb: !!python/object/apply:decimal.Decimal\n- '2'\nc: 2020-05-24 08:20:00\nd: 1962-01-13\ne: !!python/object/apply:datetime.time\n- !!binary |\n CwwNAAAA\nf:\n- 1\n- 2\n- 3\n- !!python/object/apply:decimal.Decimal\n - '4'\n"
In [9]:
Base64 とサブフォーマット
Base64シリアライザはサブフォーマットを受付ます。。もし"base64, json"
が、format
に与えられると、サブフォーマットは json と認識されます。
- dumps(): もしサブフォーマットが指定されていると、はじめにサブフォーマットでエンコードされ、次にBas64でエンコードされます。
- loads():もしサブフォーマットが指定されていると、はじめにBase64でデコードされて、次にサブフォーマットでデコードされます。
Base4の暗号化/復号化
Base64シリアライザはpassword
引数も受け付けます。もしpassword
が設定されていると、dups()/loads() で暗号化/復号化の処理が行われます。
- dumps()
- raw_data -> subformat エンコード -> 暗号化 -> Base64エンコード
- loads()
-Base64デコード -> 復号化 -> sabformat デコード -> raw_data
シリアル化ライブラリの追加をサポート
AbstractSerializer を継承したサブクラスを定義することで、
別フォーマットでのシリアル化を(比較的)容易になります。
datajuggler での cloudpickle の登録をみてみてください。
from datajuggler.serializer.abstract import (
AbstractSerializer, register_serializer
)
try:
import cloudpickle
cloudpickle_enable = True
except ImportError: # pragma: no cover
cloudpickle_enable = False
cloudpickle = AbstractSerializer()
class CloudpickleSerializer(AbstractSerializer):
def __init__(self):
super().__init__(format='cloudpickle',
extension=['pickle', 'cpickle'],
package='cloudpickle',
enable=cloudpickle_enable,
overwrite=True)
def loads(self, s, **kwargs):
data = cloudpickle.loads(s, **kwargs)
return data
def dumps(self, d, **kwargs):
data = cloudpickle.dumps(d, **kwargs)
return data
register_serializer(CloudpickleSerializer)
cloudpickle パッケージがない場合でもエラーにはならずに、
呼び出し時に NotImplementedError
が発生します。
このクラスで定義するべきメソッドは次のものです。
dumps()
、dump()
、loads()
load()
全てを定義する必要はなく該当する他方(dumps() なら dump())を使って自動的に補間定義がされます。
カスタムなオブジェクトのシリアル化をサポート
datajuggler のシリアル化では共通の encode()
/ decode()
を経由しているため、
独自に定義したクラスなどのシリアル化についても(比較的)容易になります。
datajuggler で datetime オブジェクトをシリアル化を登録をみてみてください。
import datetime
from datajuggler.serializer.abstract import (
AbstractClassSerializer, register_serializer
)
from datajuggler.validator import TypeValidator as _type
class DatetimeClassSerializer(io.AbstractClassSerializer):
def __init__(self, cls=datetime.datetime):
super().__init__(cls)
def encode(self, obj):
if _type.is_datetime(obj):
return {
"__type__": "datetime",
"value": [
obj.year,
obj.month,
obj.day,
obj.hour,
obj.minute,
obj.second,
],
}
else:
super().encode(obj)
def decode(self, obj):
v = obj.get("__type__")
if v == "datetime":
return datetime.datetime(*obj["value"])
self.raise_error(obj)
register_serializer(DatetimeClassSerializer)
IODict についても紹介しておきます。
aDict と uDict は IODict のサブクラスとして定義しています。このIODict は、
Seiralzier をサポートしているため次のメソッドを持っています。
-
from_base64(cls, s, subformat="json", encoding="utf-8", **kwargs)
-
from_csv(cls, s, columns=None, columns_row=True, **kwargs)
-
from_ini(cls, s, **kwargs)
-
from_json(self, s, **kwargs)
-
from_pickle(cls, s, **kwargs)
-
from_plist(cls, s, **kwargs)
-
from_query_string(cls, s, **kwargs)
-
from_toml(cls, s, **kwargs)
-
from_xml(cls, s, **kwargs)
-
from_yaml(cls, s, **kwargs)
-
from_serializer(cls, s, format, **kwargs)
-
to_base64(cls, s, subformat="json", encoding="utf-8", **kwargs)
-
to_csv(cls, s, columns=None, columns_row=True, **kwargs)
-
to_ini(cls, s, **kwargs)
-
to_json(self, s, **kwargs)
-
to_pickle(cls, s, **kwargs)
-
to_plist(cls, s, **kwargs)
-
to_query_string(cls, s, **kwargs)
-
to_toml(cls, s, **kwargs)
-
to_xml(cls, s, **kwargs)
-
to_yaml(cls, s, **kwargs)
-
to_serializer(cls, s, format, **kwargs)
datajuggler のいけてないところ
今時点で認識している弱点は次のものです。
- サポート体制がなっていない(弱小プロジェクトなのです)
- pure python での実装のため、ノーマルな dict に比べて遅い。
- Out-of-Core 処理ができない。
- マルチスレッド対応になっていない
- 非同期処理は考慮されていない
まとめ
datajuggler の aDict, uDict, iList と Srializer について紹介しました。
他にも、冒頭の read_contents()
のように地味に便利ではないかと考えている機能もありますので、一度触ってみてもらいご意見をいただけると幸いです。