💣

[事故回避] 定数として扱うコンテナは変更不可能なtuple,MappingProxyTypeを利用しよう

2021/08/18に公開

題意が結論です。

結論

定数には、変更不可能なデータ型(※)を使おう

コンテナデータ型 変更不可能なコンテナデータ型(※)
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. 変数のスコープがモジュールレベルになっていて広い
  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