7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

イミュータブルな NumPy 配列が欲しいとき

7
Last updated at Posted at 2025-12-22

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]

この場合のソリューションは簡単で、ここまでして配列を書き換えようとする無法開発者をチームから叩き出すことで解決できます。より非暴力的な解決策を思い付いた方がいましたらご連絡ください。

  1. Python 3.5 以降の @ 演算子とか NumPy のためだけに導入されたようなもんですし。

  2. C++などでこんなことをすると基本ダングリング参照が爆誕してしまいますが、Python では参照カウンタによるガベージコレクション戦略が採用されているため、ビューに束縛された変数が生きている限り大丈夫のはずです。違ったらごめんなさいご指摘願います。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?