無駄と文化

実用的ブログ

NumPyのオーバーロード悪用っぷりが面白い

機械学習などの数値計算で使われる Python ライブラリの NumPy (ナンパイ)、DSL が独特で面白いので紹介します。

 

基本の ndarray とブロードキャスト

NumPy のクラスでよく使うのは、N次元配列を表現する ndarray (N-dimension Array) です。
例えばシンプルな一次元配列はこのように書けます。

import numpy

ndarray = numpy.array([1, 2, 3, 4, 5])
# => array([1, 2, 3, 4, 5])

 

この ndarray はさまざまな演算子をオーバーロードで独自定義しています。
例えば各要素に対する定数和, 定数倍はこのように書けます。

ndarray = numpy.array([1, 2, 3, 4, 5])

ndarray + 3
# => array([4, 5, 6, 7, 8]): 全ての要素に 3 を足す

ndarray * 3
# => array([3, 6, 9, 12, 15]): 全ての要素に 3 を掛ける

このような挙動は ブロードキャスト と呼ばれます。1

内部的には、

class numpy.ndarray:
  def __add__(self, right):
    # right を各要素に足した結果を返す

  def __mul__(self, right):
    # right を各要素に掛けた結果を返す

このように特殊メソッドが実装されていると思われます。便利ですね。

 

インデックスアクセス [] もオーバーロード!

インデックスアクセス ndarray[...] にもオーバーロードで独自の実装が入っています。

例えば、真理値の配列をインデックスに指定できます。

ndarray = numpy.array([1, 2, 3, 4, 5])

ndarray[[True, False, True, False, True]]
# => array([1, 3, 5])

これだけ見ると何の役にたつかわかりづらいですね。[] に条件式を置くと実用的な雰囲気になります。

ndarray = numpy.array([1, 2, 3, 4, 5])

ndarray[ndarray % 2 == 1]
# => array([1, 3, 5])

2で割った余りが1になる (ndarray % 2 == 1) ような要素を取り出すと、array([1, 3, 5]) になると読めますよね。

 

順を追って解説すると、

ndarray = numpy.array([1, 2, 3, 4, 5])

arr1 = ndarray % 2
# => array([1, 0, 1, 0, 1])
#   : `% 2` のブロードキャスト

arr2 = arr1 == 1
# => array([True, False, True, False, True])
#   : `== 1` のブロードキャスト

arr3 = ndarray[arr2]
# => array([1, 3, 5])
#   : 真理値配列でインデックスアクセスして True の位置の要素だけ残す

# 上記をまとめて書くと、
ndarray[ndarray % 2 == 1]
# => array([1, 3, 5])

このようにブロードキャストの連鎖によって上手く働くように作られています。

 

メソッドっぽいけどメソッドでないもの

二つの ndarray を連結するのはこのように書けます。

a = numpy.array([1, 3, 5])
b = numpy.array([2, 4, 6])

numpy.r_[a, b]
# => array([1, 3, 5, 2, 4, 6])

r_ という メソッド で連結ができるんだね!」→ 違います!

.r_[a, b] はメソッド呼び出しではなく、クラス定数 .r_ に対するインデックスアクセスです。振る舞いはメソッドっぽいけどメソッドじゃないんです。

 

なぜパーレン () ではなくブラケット [] でアクセスするのか。それは slice() を受け入れるためです。

numpy.r_[1:6:2, 2:7:2]
# => array([1, 3, 5, 2, 4, 6])

Python ではスライス記法 start:stop, start:stop:step はインデックス [] の中にしか書けません。
もし .r_ がメソッドだったら、 .r_(1:6) のような書き方は Python の文法の制約として許されないんですね。そこでインデックスアクセスのオーバーロード!(悪用!)

 

複素数オブジェクトも悪用するぜ

.r_[] のオーバーロード悪用っぷりは止まりません。スライス記法の step 部に虚数 j を指定できます。

numpy.r_[0:10:5]
# => array([0, 5])

numpy.r_[0:10:5j]
# => array([0., 2.5, 5., 7.5, 10.])

step 部が実数の場合、「step ずつ飛ばして数列を作る」です。
一方で step 部が虚数だと「start から stop までを step 個に分割して数列を作る」になります。オーバーロードを貪欲に悪用する姿勢に痺れますね。

 

まとめ

NumPy の DSL はオーバーロードをどこまで悪用できるか面白がっている節がある。

 

 

私からは以上です。


  1. ブロードキャストの概念は、実際にはスカラー演算だけでなくサイズの異なる配列同士にも適用される広い概念です