[事故回避] 定数として扱うコンテナは変更不可能なtuple,MappingProxyTypeを利用しよう
題意が結論です。
結論
定数には、変更不可能なデータ型(※)を使おう
コンテナデータ型 | 変更不可能なコンテナデータ型(※) |
---|---|
list | tuple |
dict | MappingProxyType |
set | frozenset |
※: 変更するインタフェースがない、変更が前提ではないという意味で変更不可能と表現しています。
説明
Python の dict, list, set, tuple は組み込みコンテナと呼ばれます。このコンテナを読み取り専用の値(定数)として扱いたいときがあります。
SPECIAL_USER_IDS = [
"12300",
"45600",
]
STATUS_MAPPING = {
0: "未認証",
1: "認証済み",
2: "退会申請中",
3: "退会処理済",
}
たとえば、上のようなデータを扱うときです。 SPECIAL_USER_IDS
は list、 STATUS_MAPPING
は dict です。
list や dict は、コンテナ内の値を変更する append, update など破壊的操作をそれぞれ許しています。
問題点
誤って append や、update などで値を書き換えてしまった場合、その影響範囲は実行プロセス全体に及びます。
この問題点は、2つあります。
- 変数のスコープがモジュールレベルになっていて広い
- 書き換えしてはいけない変数が書き換え可能である
利便性の観点から、1を変えることは難しいでしょう。1について対処するなら定数専用のクラスを用意したり、都度コピーを生成するメソッドを用意したりすることが考えられます。
本質的な問題は 2 です。そもそも書き換えができないのであれば、何も問題になりません。
対策
その方法は、単純です。書き換え不可能なコンテナを利用すればいいだけです。
- list であれば、 tuple
- dict であれば、 MappingProxyType
これらはほとんど同じインタフェースを持っているので、置き換えたときにコードを修正する手間が非常に少なく済みます。
tuple
tuple は要素にもアクセスできるし、Iterableですし、appendなどの破壊的変更を受け付けません。
>>> a = ("12300", "45600")
>>> a[0] # 要素アクセスも可能
'12300'
>>> ["Z" + v for v in a] # Iterable
['Z12300', 'Z45600']
>>> a + ("78900",) # 要素の追加
('12300', '45600', '78900')
>>> a # 変更されていない
('12300', '45600')
MappingProxyType
MappingProxyType はmappingを受け取り、読み取り専用オブジェクト(ビュー)を生成します。
update や pop などの破壊的操作はできません。 dict とほぼ同じインタフェースを持っています。要素アクセスも、存在しないキーのKeyError、getメソッドによる例外なしアクセス、keysやvalues、itemsなどのイテレーションを備えているので、違和感なく扱えます。
>>> from types import MappingProxyType
>>> b = {0: '未認証', 1: '認証済み', 2: '退会申請中', 3: '退会処理済'}
>>> b.update({1: "???"})
>>> b
{0: '未認証', 1: '???', 2: '退会申請中', 3: '退会処理済'}
>>> c = MappingProxyType({0: '未認証', 1: '認証済み', 2: '退会申請中', 3: '退会処理済'})
>>> c.update({1: "???"})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'mappingproxy' object has no attribute 'update'
>>> [x for x in b.keys()]
[0, 1, 2, 3]
>>> [x for x in c.keys()]
[0, 1, 2, 3]
>>> [x for x in b.values()]
['未認証', '???', '退会申請中', '退会処理済']
>>> [x for x in c.values()]
['未認証', '認証済み', '退会申請中', '退会処理済']
>>> len(b)
4
>>> len(c)
4
>>> [x for x in b.items()]
[(0, '未認証'), (1, '???'), (2, '退会申請中'), (3, '退会処理済')]
>>> [x for x in c.items()]
[(0, '未認証'), (1, '認証済み'), (2, '退会申請中'), (3, '退会処理済')]
>>>
ただし、 MappingProxyType の中に dict を入れてしまうとその dict は変更可能になります。 MappingProxyType の中では dict を使わずに MappingProxyType を使ってください。
>>> MappingProxyType({"abc": {"def": "ghi"}}) # だめな例
mappingproxy({'abc': {'def': 'ghi'}})
>>> MappingProxyType({"abc": MappingProxyType({"def": "ghi"})}) # 良い例
mappingproxy({'abc': mappingproxy({'def': 'ghi'})})
Discussion