『Python数値計算ノート』ではアフィリエイトプログラムを利用して商品を紹介しています。

【NumPy】viewとcopy

viewとcopy

NumPy の配列 (ndarray) を変数に代入したとき、一見すると奇妙なことが起こります。

# PYTHON_NUMPY_VIEW_COPY

# In[1]

import numpy as np

a = np.array([1, 2, 3])

b = a

# 配列bの第1要素を100に変更
b[0] = 100

print("a : {}".format(a))
print("b : {}".format(b))

# a : [100   2   3]
# b : [100   2   3]

このコードでは変数 b に配列 a を代入し、b の第 1 要素を 100 に変更しています。ところが実行結果をみると、元の配列 a の内容まで書き換えられています。これは配列の変数への代入が参照代入であることを意味しています。すなわち、b = a のように記述すると、配列 b は配列 a と同じメモリを参照するようになります。配列 b を操作するということは、a と b が共通して参照しているメモリを書き換えていることになるのです。このとき「 b は a の view (ビュー) である」といいます。

a と同じ要素をもちながら、異なるメモリを参照する配列 b を生成したいときには、copy()メソッドを用いて配列 a の copy (コピー) を作成します。

# In[2]

a = np.array([1, 2, 3])

# 配列aのコピーを作成
b = a.copy()

# 配列bの第1要素を100に変更
b[0] = 100

print("a : {}".format(a))
print("b : {}".format(b))

# a : [1 2 3]
# b : [100   2   3]

a と b は互いに独立した別個の配列なので、この場合は b を変更しても a には反映されません。

copy を作るということは、その分だけ余分なメモリを消費するということなので、メモリ効率という観点から見ればなるべく避けたいところです。しかし最初のコードに見たように、view には書き換えのリスクが伴います。安全性を重視する場合は copy, メモリの節約が優先される場合は view というように使い分ける必要があります。

メモリアドレス

オブジェクトに割り当てられているメモリアドレスを確認したいときは id() 関数を使います。

# PYTHON_NUMPY_MEMORY_ID

# In[1]

import numpy as np

# 1次元配列を作成
a = np.array([1, 2, 3])

# 配列aのメモリアドレス
print(id(a))

# 63896056

もちろん、実行結果はその時々で割り当てられたアドレスにより異なります。念のために b = a として、a と b の参照先が同じであるかどうかを確認してみましょう。

# In[2]

# 1次元配列を作成
a = np.array([1, 2, 3])

b = a

# aとbのアドレスは同じ?
print(id(a) == id(b))

# True

a と b は確かに同じメモリアドレスを共有しています。

要素の参照

配列 x に対して x[0] という記述は、メモリブロックへの直接的アクセスです。

# PYTHON_NUMPY_BLOCKS_MEMORY

# In[1]

import numpy as np

# 1次元配列を作成
x = np.array([1, 2, 3])

# 配列xの第1要素を参照
print(x[0])
# 1

すなわち、x[0] に対する操作はメモリの書き換えを意味します。

# In[2]

# 1次元配列を作成
x = np.array([1, 2, 3])

# 配列xの第1要素を100に変更
x[0] = 100

print(x)
# [100   2   3]

Python におけるリストのスライシングが copy を作成するのに対して、NumPy の配列のスライシングは view を返します。したがって、スライシングによって取り出した部分配列への操作は元の配列に反映されます。

# In[3]

# 3×3の2次元配列を定義
x = np.array([[82, 20, 47],
              [11, 62, 23],
              [34, 79, 58]])

# スライシングによる部分配列の取り出し
y = x[:2, :2]

print("部分配列y")
print(y, \n)

# 部分配列yの変更
y[0, 0] = 100

print("配列x")
print(x)

# 部分配列y
# [[82 20]
#  [11 62]]
 
# 配列x
# [[100 20 47]
#  [11 62 23]
#  [34 79 58]]

コメント

  1. あとりえこばと より:

    【GPTドラマ脚本】ビューとコピーの対立

    キャラクター:ダニエル (経験豊富なエンジニア)、サラ (新入社員)
    場面: IT部門のオフィス – デスクエリア

    (舞台は忙しいIT部門のオフィス。ダニエルとサラがそれぞれのデスクで仕事をしている。)

    ダニエル: (コードを書きながらつぶやく)ここでビューを使えば、メモリを節約しつつ処理速度も向上できるんだよ。
    サラ: (興味津々)ビューって何ですか?
    ダニエル: ビューは元の配列と同じデータを共有する、効率的な方法なんだ。データを複製することなく操作できるから、メモリを食わないし処理も速いんだよ。
    サラ: でも、もし元の配列のデータが変更されたら、ビューも変わっちゃうんでしょ?
    ダニエル: そうだけど、それを考慮して使えばいい。安全に使う方法を知ってれば問題ない。
    (サラはうなずきながら自分のデスクに戻る。)

    (数日後、オフィスの休憩スペースでダニエルとサラがランチをしている。)
    サラ: ダニエル、前の話、ビューのことだけど…コピーを使う方が安全じゃないですか?
    ダニエル: コピーは確かに安全だけど、メモリの使用量が増えるし、処理も遅くなることがあるんだ。
    サラ: でも、わかりやすいしミスも少ないんじゃないですか?
    ダニエル: それも事実だけど、最終的にはどちらを選ぶかは状況次第なんだよ。どんな状況でどちらが得策か、それを見極めるのがエンジニアとしての腕の見せ所だと思うんだ。
    (ダニエルとサラは少し考え込む。)

    サラ: でも、私、まだ経験が浅いし、正直ビューとコピーの使い分けが難しいんです。
    ダニエル: 大丈夫、サラ。経験を積んでいくうちに、どちらを選ぶべきかが分かるようになるさ。まずは両方を試してみて、感覚を掴むことから始めてみるといいよ。
    サラ: そうですか…ありがとう、ダニエル。助けていただいて感謝しています。
    ダニエル: どういたしまして、サラ。仕事で困ったことがあればいつでも聞いてみてくれ。