不変な辞書って欲しいよね
なにがしかのデータを辞書形式で集めた後、それを参照するだけの用途で用いるということをしたいときに、「うっかり」新たなkey
を追加してしまったり、value
を変えてしまったりしないように保証した辞書が欲しい。
通常はtypes.MappingProxyType
を使う
from types import MappingProxyType
assc_arr = {
'foo': 'hoge',
'bar': 'fuga',
'baz': 'piyo'
}
frozen_assoc_arr = MappingProxyType(assoc_arr)
frozen_assoc_arr['qux'] = 'moge'
# >>> TypeError: 'mappingproxy' object does not support item assignment
じゃあ、サブクラス化したいときは?
可能かどうかを試すために、MappingProxyType
をただ継承元にしたクラスを作ると、
class FrozenMapping(MappingProxyType):
pass
# >>> TypeError: type 'mappingproxy' is not an acceptable base type
基底クラスにできないと怒られてしまう。
collections.UserDict
を参考にしよう
標準ライブラリ内のクラスcollections.UserDictは
辞書をシミュレートするクラスです。インスタンスの内容は通常の辞書に保存され、 UserDict インスタンスの data 属性を通してアクセスできます。 initialdata が与えられれば、 data はその内容で初期化されます。他の目的のために使えるように、 initialdata への参照が保存されないことがあるということに注意してください。
というように、内部にdata
属性としてdict
インスタンスを持っていて、これにUserDict
自身に実装されたメソッドを介してアクセスして辞書のような振る舞いをしている。
でも欲しいのってImmutableな辞書
UserDict
は既に__setitem__
や__delitem__
を実装してしまっているので、これをサブクラス化することでImmutableな辞書を実装することはできない。
基底クラスを辿る旅
UserDict
は抽象クラス_collections_abc.MutableMapping
を継承している。
_collections_abc.MutableMapping
はさらに_collections_abc.Mapping
を継承している。
class Mapping(Collection):
__slots__ = ()
"""A Mapping is a generic container for associating key/value
pairs.
This class provides concrete generic implementations of all
methods except for __getitem__, __iter__, and __len__.
"""
@abstractmethod
def __getitem__(self, key):
raise KeyError
def get(self, key, default=None):
'D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.'
try:
return self[key]
except KeyError:
return default
def __contains__(self, key):
try:
self[key]
except KeyError:
return False
else:
return True
def keys(self):
"D.keys() -> a set-like object providing a view on D's keys"
return KeysView(self)
def items(self):
"D.items() -> a set-like object providing a view on D's items"
return ItemsView(self)
def values(self):
"D.values() -> an object providing a view on D's values"
return ValuesView(self)
def __eq__(self, other):
if not isinstance(other, Mapping):
return NotImplemented
return dict(self.items()) == dict(other.items())
__reversed__ = None
これから継承していくのがよさそうだ。
実装
from _collections_abc import Mapping
class ImmutableMapping(Mapping):
def __init__(self, data=None):
self._data = data if data is not None else {}
def __len__(self):
return len(self._data)
def __getitem__(self, key):
if key in self._data:
return self._data[key]
if hasattr(self.__class__, '__missing__'):
return self.__class__.__missing__(self, key)
raise KeyError(key)
def __iter__(self):
return iter(self._data)
def __contains__(self, key):
return key in self._data
def __repr__(self):
return f'ImmutableMapping({repr(self._data)})'
@classmethod
def recursively(cls, data={}):
d = {}
for key, val in data.items():
if isinstance(data, (dict, Mapping)):
val = cls(val)
d[key] = val
return cls(d)
サブクラス化の例
class SampleFrozenMapping(ImmutableMapping):
def __init__(self, name):
data = {
f'{name}`s parrot': 'Polly'
}
ImmutableMapping.__init__(self, data)
if __name__ == '__main__':
foo = SampleFrozenMapping('Mr. Praline')
print(foo['Mr. Praline`s parrot'])
# >>> Polly
ImmutableMapping.recursively
って?
こういう機能はMappingProxyType
には存在しないが、必要な場面があるために実装した。
引数に渡した辞書のvalue
がdict
または_collections_abc.Mapping
のサブクラスであれば、それもImmutableMapping
に変換する。
if __name__ == '__main__':
foo = {
'a': {
'hoge': 'spam',
'fuga': 'ham'
},
'b': {
'moge': 'egg',
'piyo': 'bacon'
}
}
bar = ImmutableMapping.recursively(foo)
# >>> ImmutableMapping({
# 'a': ImmutableMapping({'hoge': 'spam', 'fuga': 'ham'}),
# 'b': ImmutableMapping({'moge': 'egg', 'piyo': 'bacon'})
# })
展望
immutableなcollections.defaultdict
も実装したい。