Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

Pythonで読み取り専用コレクションを実装する

今回は、Pythonで読み取り専用コレクションのメンバ変数を実装する方法を解説します。

概要

通常、読み取り専用のメンバ変数を実装するときは、メンバ変数をプライベートにしつつ、getterのみ宣言して、setterを宣言しないようにします。

class Name:
    def __init__(self):
        self._firstname = 'Fugataro'

    @property
    def firstname(self):
        return self._firstname

name = Name()
print(name.firstname)
name.firstname = 'Hogemi'

# Fugataro
# AttributeError: can't set attribute

注意が必要なのは、listdictなどのコレクション型変数をメンバ変数とする場合です。

上記と同様に、getterのみ宣言して、setterを宣言しないことにより、コレクション型のメンバ変数を別のコレクション型変数に置き換えることは出来ません。

class NumbersList:
    def __init__(self):
        self._data = [1, 2, 3]

    @property
    def data(self):
        return self._data

numbers = NumbersList()
print(numbers.data)
numbers.data = [4, 5, 6]

# [1, 2, 3]
# AttributeError: can't set attribute

しかし、append()remove()などでコレクション型メンバ変数の中身を書き換えることが出来てしまっています。これは、getterのみの宣言により、メンバ変数そのものの置き換えは出来ないようになりましたが、コレクション型メンバ変数の関数はいまだ機能しているため、結果としてそれら関数により、中身の書き換えが出来てしまっているのです。

class NumbersList:
    def __init__(self):
        self._data = [1, 2, 3]

    @property
    def data(self):
        return self._data

numbers = NumbersList()
print(numbers.data)
numbers.data.append(4)
numbers.data.extend([5, 6])
numbers.data.remove(1)
print(numbers.data)

# [1, 2, 3]
# [2, 3, 4, 5, 6]

通常、この書き換えを防ぐために、別途用意されている読み取り専用のコレクションクラスを利用します。

Pythonの場合、読み取り専用のコレクションクラスは用意されていません。しかし、基本的なコレクションであるlistdictsetに対して、以下を用いることにより、代用可能です。

通常 読み取り専用
list tuple
dict types.MappingProxyType
set frozenset

実装方法

listdictsetに対して、対応する代用のコレクションにてキャストすればよいです。以下にサンプルソースを記載します。

tuple

class ReadOnlyList:
    def __init__(self):
        self._data = tuple([1, 2, 3])

    @property
    def data(self):
        return self._data

l = ReadOnlyList()
print(l.data)

print(l.data[0])
print(l.data[1])
print(l.data[2])

for i in l.data:
    print(i)

l.data.append(4)

# (1, 2, 3)
# 1
# 2
# 3
# 1
# 2
# 3
# AttributeError: 'tuple' object has no attribute 'append'

types.MappingProxyType

import types

class ReadOnlyDict:
    def __init__(self):
        d = {'msg': 'Hello,World!', 'param': 1}
        self._data = types.MappingProxyType(d)

    @property
    def data(self):
        return self._data

d = ReadOnlyDict()
print(d)

print(f'msg: {d.data["msg"]}')
print(f'param: {d.data["param"]}')

for key in d.data:
    print(key)

for value in d.data.values():
    print(value)

for key, value in d.data.items():
    print(key, value)

d.data['name'] = 'Foge'

# <__main__.ReadOnlyDict object at 0x0000024B239D7D88>
# msg: Hello,World!
# param: 1
# msg
# param
# Hello,World!
# 1
# msg Hello,World!
# param 1
# TypeError: 'mappingproxy' object does not support item assignment

frozenset

class ReadOnlySet:
    def __init__(self):
        s = set((1, 2, 3))
        self._data = frozenset(s)

    @property
    def data(self):
        return self._data

s = ReadOnlySet()
print(s.data)

for i in s.data:
    print(i)

s.data.add(4)

# frozenset({1, 2, 3})
# 1
# 2
# 3
# AttributeError: 'frozenset' object has no attribute 'add'