アルアカ - Arcadia Academia

Arcadia Academiaは「エンジニアリングを楽しむ」を合言葉に日本のデジタル競争力を高めることをミッションとするテックコミュニティです。

NumPyのfancy indexingでnp.clip(out=)が効かない時の直し方

Featured image of the post

「np.clip で範囲を制限しているはずなのに、値がどんどん大きくなっていく」 「エラーは一切出ていないのに、計算結果だけがおかしい」 「ファンシーインデックスに out= で書き戻したのに反映されない」

NumPyでこうした「例外は出ないのに結果だけ壊れる」トラブルに遭遇したことはないでしょうか。多くの場合、原因は整数配列(またはブールマスク)によるファンシーインデックスがコピーを返すという、NumPyの基本だが見落としやすい仕組みにあります。この記事では、再現コードと発生原理、そして代入で書き戻す正しい直し方、さらに気づきにくいバグを早期発見するための確認方法まで解説します。

💡
30秒で要点
vel[idx] のように整数配列でインデックスするとコピーが返る(スライスvel[3:7]はビューが返る)
コピーに対して np.clip(..., out=vel[idx]) のように書き戻しても、その場限りのコピーに書き込まれて捨てられる
エラーも警告も出ないため、症状は「実行は正常終了・値だけおかしい」という形で現れる
直し方は out= を使わず vel[idx] = np.clip(...) のように代入で書き戻すこと
.base is None で対象がコピーかビューかを事前に確認できる

目次を開く

何が起きているか:最小の再現コード

次のコードは、配列の一部だけ値を範囲内に収めようとしていますが、制限がまったく効きません。

import numpy as np

vel = np.array([10.0, 20.0, 30.0, 40.0, 50.0])
idx = np.array([1, 3])          # 整数配列でインデックス(ファンシーインデックス)

vel[idx] = vel[idx] * 2         # 一旦更新
np.clip(vel[idx], -18, 18, out=vel[idx])   # ← 制限したつもりが効かない

print(vel)   # [10. 40. 30. 80. 50.] のまま。-18〜18に収まっていない

例外もエラーも出ず、正常終了したように見えるのに、vel の値は範囲外のまま残ります。

原因:ファンシーインデックスはコピーを返す

NumPyでは、配列のインデックス方法によって「ビュー(元の配列を参照するだけ)」が返るか「コピー(別の新しい配列)」が返るかが変わります。

インデックスの書き方 例 返るもの
スライス vel[3:7] ビュー(元配列を参照)
整数配列(ファンシーインデックス) vel[idx](idxは配列) コピー(新しい配列)
ブールマスク vel[vel > 0] コピー(新しい配列)

np.clip(x, lo, hi, out=y) は「y というメモリ領域に結果を書き込む」という意味です。vel[idx] がビューであれば vel 本体に反映されますが、コピーの場合はその場で生成された使い捨ての配列に書き込んで、直後に捨てられるため、見た目には何も起きなかったことになります。

直し方:out= をやめて代入で書き戻す

out= を使わず、計算結果を明示的に代入すれば、コピーかビューかを気にせず確実に反映できます。

vel[idx] = np.clip(vel[idx] * 2, -18, 18)

print(vel)   # [10. 18. 30. 18. 50.]  範囲内に収まる

同じ理由で、ブールマスクへの書き戻しも out= ではなく代入にします。

mask = vel > 0
vel[mask] = np.clip(vel[mask], -18, 18)

事前に気づく方法:.base でビューかコピーか確認する

out= を使う前に、対象がビューかコピーかを一行で確認できます。

print(vel[3:7].base is not None)   # True  → ビュー
print(vel[idx].base is not None)   # False → コピー

.base は「元になった配列」を指す属性で、コピーの場合は None になります。out= を使うコードをレビューするときは、この一行を差し込んでどちらかを確認しておくと安全です。

症状が「絵が出ない」「発散する」形で現れる時の見つけ方

このバグは例外を出さないため、力学シミュレーションや粒子アニメーションのような計算では「制限をかけたはずなのに座標が発散して、描画結果が範囲外になる」という形でしか気づけないことがあります。原因不明の発散に遭遇したら、次の手順で切り分けます。

  1. 疑わしい out= の書き込み先が、スライスかファンシーインデックスかを確認する
  2. ファンシーインデックスなら .base is None でコピーであることを確認する
  3. out= をやめて代入に書き換え、同じ入力で結果が変わるか確認する

3で結果が変われば、このバグで確定です。

まとめ

vel[idx] のような整数配列・ブールマスクによるインデックスは、スライスと違ってコピーを返します。np.clip や他の out= を取る関数にコピーを渡すと、その場限りのコピーに書き込まれて捨てられるため、例外もエラーも出ないまま結果だけが反映されません。直し方は単純で、out= をやめて vel[idx] = np.clip(...) のように代入で書き戻すことです。

よくある質問(FAQ)

Q. スライスとファンシーインデックスは見た目が似ていますが、どう見分ければいいですか?

vel[3:7] のように : を使っていればスライス(ビュー)、vel[idx](idx が配列やリスト)や vel[vel > 0](ブールマスク)はファンシーインデックス(コピー)です。迷ったら .base is None で実際に確認するのが確実です。

Q. out= を使わずに書き換えると遅くなりませんか?

代入 vel[idx] = np.clip(...) も内部的には新しい配列を作ってから書き戻すだけなので、out= がビューに効く場合と比べて大きな性能差はありません。むしろ out= がコピーに対して無効なまま気づかずに使い続ける方が、原因不明のバグ調査に時間を取られます。

Q. np.clip 以外の関数でも同じ問題は起きますか?

out= 引数を取るNumPyの関数(np.add、np.multiply など)はすべて同じ制約を受けます。書き込み先がファンシーインデックスやブールマスクの結果であれば、同じく無効になります。

Q. 多次元配列でも同じですか?

はい。arr[[0, 2], :] のような行の指定も整数配列によるファンシーインデックスなのでコピーが返ります。一方 arr[0:2, :] のようにすべてスライスで構成されていればビューになります。

プログラミング学習でお悩みですか?

現役エンジニアがあなたの学習をマンツーマンでサポートします。

  • 学習の進め方がわからない
  • ポートフォリオの作り方を知りたい
  • 現場で使える技術を学びたい
まずは30分の無料相談

相談は完全無料・オンラインで気軽に

あなたを爆速で成長させるメンタリングプログラムはこちら

メンタープログラムバナー

学習・開発のお悩みは現役エンジニアに相談

メンタープログラムの詳細を見る

エンジニアの基礎学習ゲーム

プログラミング学習支援

無料相談はこちら