2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

namedtuple, dataclass そして datajuggler

Last updated at Posted at 2022-10-25

はじめに

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() のように地味に便利ではないかと考えている機能もありますので、一度触ってみてもらいご意見をいただけると幸いです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?