Block iterator(vector 編1) - Rubyist の Python 学習記録(12)

PythonLibrary Python NumPy 2019年 1月 15日

ブロックごとにアクセスをするイテレータのテストを行いたい. 例として,ブロックスクランブルを計算する generate_with_block_trans のテストを行う.

ブロックベースのイテレータのテスト(vector 版)

  • tests/test_mono_image.py に generate_with_block_trans のテストを記述する. まずは MonoImage がベクトルの場合のテストである. obj.generate_with_block_trans は index とブロックサイズを渡し,index にしたがってブロックの入れ替えを行う.
  • with such.A('MonoImage class') as it:
        @it.should('exec block scramble')
        def test():
            ### vector test
            org_vec = MonoImage(np.arange(0, 9), 8)
            index = MonoImage.create_with_array([2, 0, 3, 1], np.int32)
            correct_vec = MonoImage.create_with_array([4, 5, 0, 1, 6, 7, 2, 3, 8], np.int, 8)
            ans_vec = org_vec.generate_with_block_trans(index, 1, 2)
            it.assertEqual(ans_vec, correct_vec)
            rev_vec = ans_vec.generate_with_reverse_block_trans(index, 1, 2)
            it.assertEqual(rev_vec, org_vec)
  • これまでいくつか使っていない表現があるので個別にメモしておく.
    • オリジナルのデータは 0 から 8 までの連続したデータを np.arange で作成した. Ruby だと 0...9 に相当する表現で 9 は含まれない.
    • org_vec = MonoImage(np.arange(0, 9), 8)
    • 次に並び替えの index を作成する. 今回は 4つのブロックを 2, 0, 3, 1 の順に並べることとした.
    • index = MonoImage.create_with_array([2, 0, 3, 1], np.int32)
    • 最後に並び替え結果を準備しておく. 今回は 2 個の長さのブロックを仮定し,4 つのブロックを index の順序で入れ替えている. 一番最後の 8 はブロックに収まりきらない端数なので,オリジナルのまま残っている.
    • correct_vec = MonoImage.create_with_array([4, 5, 0, 1, 6, 7, 2, 3, 8], np.int, 8)
    • generate_with_block_trans を index と ブロックサイズ 1, 2 を使って実行する. 今回はベクトルなので,2番目の引数は意味はないが,1 としておいた.
    • ans_vec = org_vec.generate_with_block_trans(index, 1, 2)
    • 同時に generate_with_reverse_block_trans というメソッドのテストを実行する. こちらは,ブロックを逆に入れ替えるメソッドである. 入れ替えたものを同じ index を使って元に戻せればテストが完了となる.
    • rev_vec = ans_vec.generate_with_reverse_block_trans(index, 1, 2)

generate_with_block_trans の実装

  • まず,generate_with_block_trans のメイン処理を記述する. ここでブロックイテレータを利用する.
  • class MonoImage:
        (中略)
        def generate_with_block_trans(self, index, br, bc):
            ans = self.generate_with_copy()
            if self.ndim == 1:
                for (pi, mi) in zip(index.pixel_iterator(), ans.block_iterator(br, bc, br, bc, False)):
                    vc = pi.value * bc
                    bi.value = self._array[vc:vc+bc]
            return ans
  • こちらもいくつかわかりにくい部分があるので,個別に説明する.
    • 返却する ans を最初に用意する. ただし,端にあまる部分は入れ替えを行わないので,最初にオリジナルをコピーして用意する. generate_with_copy はあとで実装する.
    • ans = self.generate_with_copy()
    • まずはベクトルの場合だけ実装するので,self.ndim が 1 の場合だけ処理をする.
    • if self.ndim == 1:
    • 次が今回のキモの部分である. まず,block_iterator(br, bc, sr, sc, is_small_ok) という引数を持つ. (br, bc) はブロック高さとブロック幅であり,(sr, sc) は処理後の 行移動量, 列移動量を示す. (br, bc) と (sr, sc) が同じ場合は,ブロックが重なり合わず移動することを示す. is_small_ok は,ブロックサイズが (br, bc) に満たない場合に処理をするかどうかを示すフラグである. 今回の場合は,block_iterator(br, bc, br, bc, False) なので,ブロックは隣接しながら移動し,大きさに満たないブロックは処理しないことを示している. また,この block_iterator は index の pixel_iterator と同時に処理される. Python 3 から zip はイテレータを返すようになったので,同時に処理したいイテレータをこのように zip でまとめることが可能である. (pi, mi) はそれぞれ pixel_iterator と block_iterator の返り値(イテレータ)を示すタプルである.
    • for (pi, mi) in zip(index.pixel_iterator(), ans.block_iterator(br, bc, br, bc, False)):
    • index の中身で指定されたブロックの先頭位置(pi.value で取得できる)を vc として計算する(単に bc 倍するだけである).
    • vc = pi.value * bc
    • index の中身で指定されたブロックを self._array から取得し,block_iterator 自体にセットする. bi.value の書き込みプロパティに代入することで,ブロックの該当部分に自動的に書き込みすることができる.
    • bi.value = self._array[vc:vc+bc]
    • ループが終了したところで,ans を返却する.
    • return ans

generate_with_copy の実装

  • generate_with_copy は単に ndarray をコピーしてコンストラクタを呼ぶだけである.
  • def generate_with_copy(self):
        return MonoImage(self._array.copy(), self.bits, self.signed)

block_iterator の実装

  • pixel_iterator と同様 MonoImageBlockIterator オブジェクトを作成するだけである. pixel_iterator と異なるのは引数が存在することであるが,そのままコンストラクタに渡すだけである.
  • def block_iterator(self, br, bc, sr, sc, is_small_ok):
        return MonoImageBlockIterator(self, br, bc, sr, sc, is_small_ok)

MonoImageBlockIterator クラスの実装

  • まずコンストラクタから実装する. MonoImageBlockIterator のコンストラクタは,必要な値を取得しておき vector_iterator_func を設定するまでは MonoImagePixelIterator とほぼ同様の実装である.
  • class MonoImageBlockIterator:
        def __init__(self, mono_image, br, bc, sr, sc, is_small_ok):
            self._mono_image = mono_image
            self._br = br
            self._bc = bc
            self._sr = sr
            self._sc = sc
            self._is_small_ok = is_small_ok
            self._width = mono_image.width
            self._height = mono_image.height
            self._f = self.vector_iterator_func
            self._getter = self.vector_getter_func
            self._setter = self.vector_setter_func
  • __iter__ は MonoImagePixelIterator と全く同じである.
  • def __iter__(self):
        return self._f()
  • vector_iterator_func は以下のように実装した.
  • class MonoImageBlockIterator:
        (中略)
        def vector_iterator_func(self):
            self._nowi = 0
            while (self._nowi != -1):
                remain = self._width - self._nowi
                self._len = self._bc if remain > self._bc else remain
                if self._len == self._bc or self._is_small_ok:
                    yield self
                self._nowi += self._sc
                if self._nowi >= self._width:
                    self._nowi = -1
  • MonoImagePixelIterator とほぼ同様であるが,vector_setter_func はキモなので一行ずつメモを記載しておく.
    • ブロックの先頭位置を _nowi は 0 で初期化しておく.
    • self._nowi = 0
    • self._nowi が -1 になるまでループする.
    • while (self._nowi != -1):
    • ベクトルの残りの長さを remain として計算しておく.
    • remain = self._width - self._nowi
    • 処理するブロックの長さを _len として記録しておく(remain または _bc となる).
    • self._len = self._bc if remain > self._bc else remain
    • ブロックとして処理するのは _len が _bc と同じ時か _is_small_ok が True の時のみである. 今回は,_is_small_ok が False なので,_len が _bc よりも短い場合には処理をしない.
    • if self._len == self._bc or self._is_small_ok:
    • yield では Pixel iterator と同様に self を返す.
    • yield self
    • 再度メソッドが呼ばれた時に,_nowi は _sc だけずらす._bc と _sc が同じ場合には,隣接したブロック処理が行われることになる.
    • self._nowi += self._sc
    • _nowi が _width を超えた場合に,_nowi を -1 にしてループを終了させる.
    • if self._nowi >= self._width:
          self._nowi = -1
  • value の getter プロパティは MonoImagePixelIterator と全く同じである.
  • @property
    def value(self):
        return self._getter()
  • value の setter プロパティも MonoImagePixelIterator と全く同じである.
  • @value.setter
    def value(self, v):
        self._setter(v)
  • property から呼ばれる vector_getter_func は以下のようにスライスを返す. Python の場合には,MATLAB と異なり array[from:to] では to の位置は入らないことに注意しておく(Ruby の from ... to に相当).
  • def vector_getter_func(self):
        return self._mono_image._array[self._nowi:self._nowi+self._len]
  • 同様に property から呼ばれる vector_setter_func は以下のようにスライスに値を代入する処理を行う.
  • def vector_setter_func(self, nda):
        self._mono_image._array[self._nowi:self._nowi+self._len] = nda

generate_with_reverse_block_trans の実装

  • ここまで記述すると順方向のブロックスクランブルは実施できた. 次にブロックを逆に戻す generate_with_reverse_block_trans のメイン処理を記述する. 処理自体は順方向処理と変わらないので,index だけ逆順に変更してから generate_with_block_trans を呼び出すことにしている.
  • class MonoImage:
        (中略)
        def generate_with_reverse_block_trans(self, index, br, bc):
            reverse_index = index.generate_with_zeros()
            i = 0
            for pi in index.pixel_iterator():
                reverse_index.set_value_at(i, pi.value)
                i += 1
            return self.generate_with_block_trans(reverse_index, br, bc)
  • 上記の中で set_value_at(v, i) というメソッドを利用している.こちらは値vを位置iに書き込む作業である.
  • def set_value_at(self, v, p):
        self._array[p] = v
  • ここまでの実装で全てのテストが通過した.

端数がない場合のテスト

  • 念のため端数が存在しない場合にもうまく動作することを確認するために,長さを 8 に変更した場合のテストも追加しておく. np.delete で org_vec と correct_vec の最後の要素を削ってみる. これによって,両ベクトルとも長さが 8 (ブロックサイズ 2 の倍数)となり,端数がなくなる.
  • ### vector test (without edge)
    org_vec.delete_self(-1, None)
    correct_vec.delete_self(-1, None)
    ans_vec = org_vec.generate_with_block_trans(index, 1, 2)
    it.assertEqual(ans_vec, correct_vec)
    rev_vec = ans_vec.generate_with_reverse_block_trans(index, 1, 2)
    it.assertEqual(rev_vec, org_vec)
  • ここで delete_self というベクトルを短くするメソッドを呼び出している. NumPy.delete は削除はするがオリジナルは元のままなので,delete_self を MonoImage に追加した.
  • class MonoImage:
        def delete_self(self, obj, axis):
            self._array = np.delete(self._array, obj, axis)
            return self
  • テストを追加しても,テストはそのまま成功した.

index の数が一致しない時の例外のテスト

  • さらに index の数とブロック数が一致しないときには例外を出したい. さらにベクトルの長さを 7 に減らすと,index の長さ 4 と一致しなくなる. このときに例外が発生するかどうかをテストする.
  • ### raise exception when index size is wrong
    org_vec.delete_self(-1, None)
    with it.assertRaises(IndexError):
        org_vec.generate_with_block_trans(index, 1, 2)
  • 現在は例外処理をしていないので,例外発生せずテストが通過しない. そこで,generate_with_block_trans の先頭で例外処理を追加する.
  • class MonoImage:
        (中略)
        def generate_with_block_trans(self, index, br, bc):
            if int(self.width / bc) * int(self.height / br) != index.width:
                raise IndexError('wrong index length')
            (後略)

長くなったので今日はここまで.