6.2. ndarray

ndarray は、numpy で数値データを保持する型です。これは2つのものが1つになっています。データの単一のパックされたブロックと、そのブロックの前にある、それをどう読むかを示す小さなディスクリプタです。

6.2.1. ボックスの中身

データブロックは、配列のすべての要素を端から端まで保持し、それらの間に余分なものは一切ありません。各要素は同じバイト数を占めます -- uint8 値の配列なら1、uint16 なら2、float なら4です。256要素の uint8 配列はちょうど256バイトのデータです。同じ256個の数値を Python の list に入れると1キロバイトを使います -- 値が実際に必要とするビット数がどれだけ少なくても、要素ごとに1つの32ビットスロットが使われるためです。

ディスクリプタは、そのブロックが 何を意味するか を記録します。何次元であっても、任意の矩形配列を記述するには5つの値で十分です。

  • dtype -- 要素の型です。各要素が何バイトを取り、どの範囲の値を保持できるかを決めます(Dtype で扱います)。

  • itemsize -- dtype から導かれる、1要素のバイト幅です。

  • ndim -- 次元の数です(ベクトルは1、行列は2、ボリュームは3で、最大4まで)。

  • shape -- 各次元に沿ったサイズをタプルで表したものです。

  • strides -- 各軸をたどるためにデータブロックをどうステップするかです。形状とストライド で扱います。

それだけです。numpy のすべての高速パス -- 算術演算、リダクション、ブロードキャスト、スライス -- は、これら5つの値とデータポインタから直接動作し、要素ごとの Python のオーバーヘッドはありません。

6.2.2. この設計がもたらすもの

「パックされたブロック + 小さなディスクリプタ」から3つの性質が生まれ、このセクションの残りの挙動を定義します。

要素ごとの数学が単一の呼び出しとして実行されます。 形状が一致する2つの配列の間の a + b は、2つのバッファを加算して3つ目に書き込みます。これはすべて1回のライブラリ呼び出しの中で行われます。np.sin(a) はすべての要素のサインに対して同じことを行います。算術演算子、比較演算子、ビット演算子はすべてこの方法で動作します。

同じデータを別の見方で見ることは無償です。 配列の部分領域を求めること、または同じデータを別の形状で並べたものを求めることは、バイトを一切移動しません。この操作は、同じ データブロックを指す新しいディスクリプタを返します。その結果は ビュー と呼ばれます -- 同じ基盤バッファへの2つ目の窓です。ビューを通じて書き込むと、ソースに書き込まれます。

形状が混在していても動作します。 短い配列を長い配列に対して、行を行列に対して、列を行に対して -- numpyブロードキャスト によってそれらを揃えます。これは、どの短い軸が長い軸に合わせて引き伸ばされるかを決める少数のルールです。引き伸ばしは仮想的なもので、データは複製されません。

6.2.3. この設計のコスト

同じ設計から2つの制約が生まれます。

すべての要素が同じ型を持ちます。 リストは int の隣に str、その隣にさらに3つの int 値のリスト、というように保持できますが、ndarray はできません。dtype は構築時に固定されます。Dtype のページでは、numpy がサポートする少数の型と、1つに固定することから生まれるルールを扱います。

配列を成長させることは無償ではありません。 リストは末尾に予備のスロットを保持し、.append を安価にサポートします。ndarray は必要なサイズちょうどであり、追加するには新しいより大きなバッファを確保して古い内容をそこにコピーすることになります。意図的に append() メソッドはありません。カメラ上での正しいパターンは、宛先を最終的なサイズで事前確保して 埋める ことです。パフォーマンス でその手法を扱います。

データ用のパックされた型付きバッファ、メタデータ用の小さなディスクリプタ、そして3つの挙動の保証(高速な要素ごとの数学、同じデータのコピーなしの別ビュー、ブロードキャストする形状)を備えた ndarray は、この章の残りが乗る土台です。配列が実際にどのように生まれるか -- リテラルから、あらかじめ埋められた割り当てから、ペリフェラルバッファから -- が、次の実践的な問いです。