PythonでNamedTupleのようにDictを使えるようにする

# はじめに
pythonによる開発での問題点を3点あげる．
1. 似たような変数名の辞書が何度も利用されるがどのような変数を含んでいるかわからない
2. 何が格納されているかわかったとしても変数の定義がわからない
3. keyの打ち間違いが多発する

このような問題が発生するときにNamedTupleのような構造体を使いたいが，NamedTupleはImmutableな使い方をすべきであり，dictのような使い方をすべきではない．

したがって，今回はdictをNamedTupleのように使うためのコードを紹介する．（改善点はぜひコメントによろしくお願いいたします．）

# コード

```python
class BaseDict(dict):
    def __init__(self, **kwargs):
        var_dict = {var_name: getattr(self, var_name)
                    if hasattr(self, var_name) else None
                    for var_name in self.__annotations__.keys()}

        for var_name, default_value in var_dict.items():
            if var_name not in kwargs.keys():
                kwargs[var_name] = default_value

        for var_name, value in kwargs.items():
            self.__setattr__(var_name, value)

    def __repr__(self):
        super_cls = set([obj.__name__ for obj in self.__class__.__mro__])
        super_cls -= set(['BaseDict', 'dict', 'object'])
        dict_name = list(super_cls)[0]

        header = f"BaseDict('{dict_name}', "
        ret = "{"
        for key, value in self.items(): 
            ret += f"'{key}': {value}, "

        ret = ret[:-2] + "})" if len(ret) > 1 else ret + "})"
        return "".join([header, ret])

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        super().__setitem__(name, value)

    def __setitem__(self, key, value):
        setattr(self, key, value)
        super().__setitem__(key, value)

    def __getitem__(self, key):
        if hasattr(self, key):
            return getattr(self, key)
        else:
            raise KeyError(key)
```

# 簡単な動作例
定義は現状のNamedTupleと同様で，変数名とTyping Hintを与える．
また，上記のBaseDictを継承する．
辞書の定義時に変数を代入しない場合は`None`で初期化され，全ての変数はkeyとAttributeの両方でアクセスできるように内部で調整．
AttributesとDictの要素で同一Addressを参照するので，メモリ量自体は大きな変化は起こらない（はず）．

```python
    >>> class NewDict(BaseDict):
    >>>     a: int = 3
    >>>     b: float = 2.0
    >>>     c: str
    >>> new_dict = NewDict(a=1, d=5)
    >>> print(new_dict)
        BaseDict('NewDict', {'a': 1, 'd': 5, 'b': 2.0, 'c': None})
    >>> print(new_dict.a, new_dict.b, new_dict.c, new_dict.d)
        1 2.0 None 5
    
    >>> new_dict.a = 100
    >>> print(new_dict.a, new_dict['a'])
        100 100
```

# あとがき
コメントにあるshiracamusさんの記事を参考に簡潔にしました．
追加機能として，dictのDefault関数の上書き防止を実装しました．

```python
_prohibited = ['clear', 'copy', 'fromkeys', 'get', 'items', 'keys',
               'pop', 'popitem', 'setdefault', 'update', 'values']


class BaseDict(dict):
    def __init__(self, **kwargs):
        if not hasattr(self, "__annotations__"):
            self.__annotations__ = {}

        var_dict = {var_name: getattr(self, var_name)
                    if hasattr(self, var_name) else None
                    for var_name in self.__annotations__.keys()}

        for var_name, default_value in var_dict.items():
            self._prohibited_overwrite(var_name)
            if var_name not in kwargs.keys():
                kwargs[var_name] = default_value
        
        for var_name in kwargs.keys():
            self._prohibited_overwrite(var_name)

        dict.__init__(self, **kwargs)
        self.__dict__ = self

    def __repr__(self):
        super_cls = set([obj.__name__ for obj in self.__class__.__mro__])
        super_cls -= set(['BaseDict', 'dict', 'object'])
        dict_name = list(super_cls)[0]

        seg = [f"BaseDict('{dict_name}', ", "{"]
        seg += [f"'{key}': {value}, " for key, value in self.items()]
        seg[-1] = seg[-1][:-2] + "})"

        return "".join(seg)
    
    def _prohibited_overwrite(self, name):
        if name in _prohibited:
            raise AttributeError(f"Cannot overwrite dict attribute '{name}'. "
                                 "Use another variable name.")
    
    def __setattr__(self, name, value):
        self._prohibited_overwrite(name)
        super().__setattr__(name, value)

    def __setitem__(self, key, value):
        self._prohibited_overwrite(key)
        super().__setitem__(key, value)
```