Python で MultiIndex
土曜日に boost 勉強会の UStream 見てたら Python でも MultiIndex を使いたくなったので作ってみた.
ちなみに,Python3 です.
こんな風に使いたい!!
MultiIndex は何を index とするかをどんな風に表現するのかが肝なのかなぁって思う.
boost の場合は,型を指定したり,メンバーのアドレス指定したりといろいろめんどくさそう.
せっかく Python なのでそんな制限したくないし,どうせつっこむデータの型なんて同じなんだし,とういことで関数オブジェクトとかで指定するようにしてみた.
実際には↓な感じで使えるように作る.
import collections class Employee: def __init__(self, name, age, sex): self.name = name; self.age = age self.sex = sex def __str__(self): return '({})'.format(', '.join([self.name, str(self.age), self.sex])) def __repr__(self): return str(self) def multi_index_test(): mdict = MultiIndex(name=Unique(lambda x: x.name, collections.OrderedDict), age=NonUnique(lambda x: x.age), sex=NonUnique(lambda x: x.sex)) mdict.append(Employee('Mike', 20, 'male')) mdict.append(Employee('Jane', 21, 'female')) mdict.append(Employee('Bob', 21, 'male')) print(mdict['name']['Mike']) print(mdict['age'][21])
実装
Python は基本的に shallow copy なので得に気にすることなく,標準のコンテナにどんどんつっこむようにする.
問題は追加,削除時のいっせい更新だけど,それは Observer パターンで解決する.
そんなんでできたのが↓
class Index(object): def __init__(self): self._index_holder = None def _set_index_holder(self, index_holder): self._index_holder = index_holder def append(self, obj): self._index_holder._complete_add(obj) def pop(self, obj): self._index_holder._complete_pop(index) class Unique(Index): def __init__(self, rule, container=dict): super(Index, self).__init__() self._make_key = rule self._contents = container() def __getitem__(self, k): return self._contents[k] def __contains__(self, obj): return self._make_key(obj) in self._contents def __iter__(self): return self._contents.values() def _add(self, obj): key = self._make_key(obj) self._contents[key] = obj def _erase(self, obj): key = self._make_key(obj) return self.contents.pop(key) class NonUnique(Index): def __init__(self, rule, container=dict, drawer=list): super(Index, self).__init__() self._make_key = rule self._drawer = drawer self._contents = container() def __getitem__(self, k): return self._contents[k] def __contains__(self, obj): return obj in self[obj] def __iter__(self): for drawer in self._contents.values(): for obj in drawer: yield obj def _add(self, obj): key = self._make_key(obj) self._contents.setdefault(key, self._drawer()).append(obj) def _erase(self, obj): key = self._make_key(obj) return self._contents[key].pop(obj) class MultiIndex(object): def __init__(self, *args, **kwargs): self._catalog = {} for i, index in enumerate(args): index._set_index_holder(self) self._catalog[i] = index for i, index in kwargs.items(): index._set_index_holder(self) self._catalog[i] = index def __getitem__(self, k): return self._catalog[k] def _complete_add(self, obj): for index in self._catalog.values(): index._add(obj) def _complete_erase(self, obj): for index in self._catalog.values(): index._pop(obj) def append(self, obj): self._complete_add(obj) def pop(self, obj): self._complete_pop(obj)
Python の場合,dict が hash map なので全部 dict を使っちゃう.
入れた順番を覚えておきたい場合も collections.OrderedDict っていうのがあるので,dict と collections.OrderedDict を使い分けれるように引数で実際に使用するコンテナを渡せるようにした.
NonUnique の場合,ある index で検索した結果がただのリストでいい場合と,さらに MultiIndex の方がいい場合とがあると思うので,そこも引数で選択できるようにした.
こんな感じのことができます.
def multi_index_dict_test2(): drawer1 = lambda: MultiIndex(name=Unique(lambda x: x.name, OrderedDict), sex=NonUnique(lambda x: x.sex)) drawer2 = lambda: MultiIndex(name=Unique(lambda x: x.name, OrderedDict), age=NonUnique(lambda x: x.age)) mdict = MultiIndex(name=Unique(lambda x: x.name, OrderedDict), age=NonUnique(lambda x: x.age, drawer=drawer1), sex=NonUnique(lambda x: x.sex, drawer=drawer2)) mdict.append(Employee('Mike', 20, 'male')) mdict.append(Employee('Jane', 21, 'female')) mdict.append(Employee('Bob', 21, 'male')) print(mdict['name']['Mike']) print(mdict['age'][21]['sex']['female']) # age == 21 && sex == 'female' をさがす
まとめ
boost::MultiIndex にインスパイアされて Python MultiIndex を実装しました.
全部 hash map でいいじゃん? ということで全部 dict を使って実装できました.
Unique Index に同じものがきたときどうするか,とか考えなきゃいけないことがいっぱいありますが,だいたいはできたと思います.
あと,ぜったいに mdict['name']['Mike'].set_name('マイク')
みたいなことはしちゃだめです.
Index にしていいのは Unmutable なものだけですよ!