NumPy はサードパーティライブラリでありながらもほぼほぼ言語標準みたいな立ち位置にあり1、Python でなにかしら凝ったことをしようとすると必ずと言っていいほど通る必要があります。
ところで、NumPy の配列はリストや辞書と同様に、再代入せずに内容を変更できます。
これは便利なこともありますがそうでないこともあります。たとえば、コード中に定数として NumPy 配列を宣言したいときです。
イミュータブルなリストっぽいものにはタプルが、イミュータブルな辞書っぽいものには types.MappingProxyType がありますが、同様に NumPy 配列をイミュータブルにするにはどうすればよいでしょうか。
WRITEABLE 属性を設定すれば読み取り専用になる
NumPy 配列はいくつかの属性を持ち、それらは numpy.ndarray#flags に格納されています。その中の "WRITEABLE" 属性は通常 True になっていますが、これを False に設定すると内部の値を書き換えることができなくなります。
import numpy as np
x = np.asarray([0, 1, 2])
print(x)
# [0 1 2]
x[0] = 42
print(x)
# [42 1 2]
x.flags["WRITEABLE"] = False # x.setflags(write=False) でもよい
x[0] = 6 * 9
# ---------------------------------------------------------------------------
#
# ValueError Traceback (most recent call last)
#
# /tmp/ipython-input-1437034665.py in <cell line: 0>()
# 10
# 11 x.flags["WRITEABLE"] = False # x.setflags(write=False) でもよい
# ---> 12 x[0] = 6 * 9
#
# ValueError: assignment destination is read-only
でも WRITEABLE 属性は再設定できる
ではこれで万事解決なのかと言うと、当然同様の方法で "WRITEABLE" 属性を再設定することで書き換えできてしまいます。
import numpy as np
x = np.asarray([0, 1, 2])
print(x)
# [0 1 2]
x.flags["WRITEABLE"] = False
try:
x[0] = 6 * 9
except ValueError:
pass
print(x)
# [0 1 2]
x.flags["WRITEABLE"] = True
x[0] = 6 * 9
print(x)
# [54 1 2]
まあそこまでして書き換えようとする無法者のことまでケアしてられっかというのはそうなのですが、もう少しよい方法を探してみます。
オリジナルが読み取り専用ならビューから勝手に書き換え可能にできない
NumPy には、スライスによって配列の一部(たまに全部だったりする)を取り出すときメモリ内容をコピーせずにその参照を用いて通常の配列のように扱える機能、ビューがあります。
ビューも配列ではあるため "WRITEABLE" 属性を持ちます。では、オリジナルが numpy.ndarray#flags["WRITEABLE"] == False であるとき、ビューではどうなるでしょうか。
import numpy as np
x = np.asarray([0, 1, 2])
print(x)
# [0 1 2]
x.flags["WRITEABLE"] = False
print(x.flags["WRITEABLE"])
# False
y = x[1:]
print(y)
# [1 2]
print(y.flags["WRITEABLE"])
# False
このようにビューの "WRITEABLE" 属性も False になっています。オリジナルが書き換えてほしくないと言っているのにビューから書き換えられたら困るので、まあそれは当然でしょう。では、オリジナルが numpy.ndarray#flags["WRITEABLE"] == False のままビューの "WRITEABLE" 属性を True に書き換えようとするとどうなるでしょうか。
import numpy as np
x = np.asarray([0, 1, 2])
print(x)
# [0 1 2]
x.flags["WRITEABLE"] = False
y = x[1:]
print(y)
# [1 2]
y.flags["WRITEABLE"] = True
# ---------------------------------------------------------------------------
#
# ValueError Traceback (most recent call last)
#
# /tmp/ipython-input-192209625.py in <cell line: 0>()
# 10 # [1 2]
# 11
# ---> 12 y.flags["WRITEABLE"] = True
# 13 # ---------------------------------------------------------------------------
# 14 #
#
# ValueError: cannot set WRITEABLE flag to True of this array
このように、ビューを書き換え可能にすることでオリジナルを書き換えようとする試みはちゃんと拒否されます。
ビューだけ返すラッパー関数を作ろう
ということで、ビューを定数として束縛し、オリジナルの配列にはアクセスできないようにしてしまいましょう。ラッパー関数を作成してオリジナルの配列はローカル変数とし、ビューだけ返すようにすれば概ねこの目的を達成できます2。可変長引数機能を利用して numpy.asarray 関数のシグネチャをそのまま乗っ取ります。
import numpy as np
def immutable_array(*args, **kwargs):
orig_array = np.asarray(*args, **kwargs)
orig_array.flags["WRITEABLE"] = False # 読み取り専用にする
return orig_array[:] # 全体へのビューを返す
x = immutable_array([0, 1, 2]) # np.asarray と同じように扱える
print(x)
# [0 1 2]
try:
x[0] = 42
except ValueError:
pass
print(x)
# [0 1 2]
x.flags["WRITEABLE"] = True
# ---------------------------------------------------------------------------
#
# ValueError Traceback (most recent call last)
#
# /tmp/ipython-input-929167401.py in <cell line: 0>()
# 17 # [0 1 2]
# 18
# ---> 19 x.flags["WRITEABLE"] = True
# 20 # ---------------------------------------------------------------------------
# 21 #
#
# ValueError: cannot set WRITEABLE flag to True of this array
このようにイミュータブルな NumPy 配列を実現できました。
しかし万能ではない
ただ、関数のローカル変数として隠蔽したからといってビューからオリジナルの配列を取得するのが不可能というわけではありません。
NumPy 配列がビューである場合 numpy.ndarray#base 属性がオリジナルの配列を返します(ビューでない場合この属性は None になります)。よって執念深く以下のようにすれば書き換えできてしまいます。
import numpy as np
def immutable_array(*args, **kwargs):
orig_array = np.asarray(*args, **kwargs)
orig_array.flags["WRITEABLE"] = False
return orig_array[:]
x = immutable_array([0, 1, 2])
print(x)
# [0 1 2]
try:
x[0] = 42
except ValueError:
pass
print(x)
# [0 1 2]
x.base.flags["WRITEABLE"] = True
x.flags["WRITEABLE"] = True
x[0] = 42
print(x)
# [42 1 2]
この場合のソリューションは簡単で、ここまでして配列を書き換えようとする無法開発者をチームから叩き出すことで解決できます。より非暴力的な解決策を思い付いた方がいましたらご連絡ください。