5
2

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 5 years have passed since last update.

`types.MappingProxyType`をシミュレートする

Last updated at Posted at 2019-06-23

不変な辞書って欲しいよね

なにがしかのデータを辞書形式で集めた後、それを参照するだけの用途で用いるということをしたいときに、「うっかり」新たな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を継承している。

_collections_abc.py

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には存在しないが、必要な場面があるために実装した。
引数に渡した辞書のvaluedictまたは_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も実装したい。

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?