コンストラクタと読み込みプロパティのテスト - Rubyist の Python 学習記録(5)

PythonLibrary Python 2019年 1月 3日

コンストラクタとプロパティのテスト

MonoImage は NumPy の ndarray を内包するクラスとする. また,ndarray の type とは別に画像のビット数をインスタンスとして持つ. まずは,コンストラクタのテストと,いくつかの読み込みプロパティのテストだけを実施してみる.

  • tests/test_mono_image.py を追記する.height だけ後から定義を修正した.
    from nose2.tools import such
    import numpy as np
    from hkob_pyimage import MonoImage
    
    with such.A('MonoImage class') as it:
        @it.has_test_setup
        def setup():
            np.set_printoptions(threshold=10)
            it.mu8 = MonoImage(np.array([[10, 20, 30], [40, 50, 60]], dtype=np.uint8))
            it.mi16 = MonoImage(np.array([[1, -2], [-3, 4], [5, -6]], dtype=np.int16), 16, True)
            it.md = MonoImage(np.array([3.1, 1.4, 1.5, 9.2, 6.5, 3.5], dtype=np.double), 0)
            it.marray = (it.mu8, it.mi16, it.md)
    
        @it.has_teardown
        def teardown():
            pass
    
        @it.should('have correct getter properties.')
        def test():
            it.assertEqual([x.bits for x in it.marray], [8, 16, 0])
            it.assertEqual([x.signed for x in it.marray], [False, True, False])
            it.assertEqual([x.dtype for x in it.marray], [np.uint8, np.int16, np.double])
            it.assertEqual([x.width for x in it.marray], [3, 2, 6])
            it.assertEqual([x.height for x in it.marray], [2, 3, 1])
            it.assertEqual([x.max_value for x in it.marray], [255, 65535, 0])
    
        it.createTests(globals())
  • 自分の覚書に気になるところをメモしておく.
    • nose2 を使うならnose2.tools から such module をインポートする
      from nose2.tools import such
    • numpy を import するが,長いので通常 np という名前で利用する.
      import numpy as np
    • RSpec の describe に相当するものが,with such.A である. ここで,受けている it は nose2.tools.such.Scenario のオブジェクトらしい.
      with such.A('MonoImage class') as it:
    • @it.has_test_setup はセットアップメソッドのためのデコレータである (has_setup は最初の一回だけ,has_test_setup はテストごとに呼ばれる). デコレータなので,メソッド名はなんでもよいが,setup(): にするのが一般的だろう. RSpec の before に相当する.
      @it.has_test_setup
      def setup():
    • テストが失敗した時にその原因を表示するために,Numpy array を表示することがある. この時にあまりに数字が多く表示されるとエラーの原因がわかりにくくなるため,threshold を 10 にして設定しておく.
      np.set_printoptions(threshold=10)
    • setup() の中では,it.mu8 のようにインスタンスに代入しておくことで,後々のテストで利用するオブジェクトを設定しておく. setup() は各テストの実行前に必ず呼ばれるためである. MonoImage クラスのコンストラクタは,データ本体である ndarray とデータのビット数,符号ありかどうかのフラグを受ける. ただし,ビット数と符号フラグは省略可能とする.ビット数が省略の場合は 8 ビットとし,符号フラグがない場合には,符号なしとする. 以下の例では,2行3列の uint8 の ndarray を持つ,8ビット符号なしモノクロ画像を意味する.
      it.mu8 = MonoImage(np.array([[10, 20, 30], [40, 50, 60]], dtype=np.uint8))
    • この場合は,3行2列の int16 のndarray を持つ,16ビット符号ありモノクロ画像である.
      it.mi16 = MonoImage(np.array([[1, -2], [-3, 4], [5, -6]], dtype=np.int16), 16, True)
    • 同様にこの場合は,長さ 6 の double の ndarray を持つ,double の符号なしモノクロ画像とする.MonoImage ではビット数 0 で double を示すことにする.
      it.md = MonoImage(np.array([3.1, 1.4, 1.5, 9.2, 6.5, 3.5], dtype=np.double), 0)
    • (it.mu8, it.mi16, it.md) の表記はタプルを示す.タプルは Swift と同様で imutabble なリストに近い.
      it.marray = (it.mu8, it.mi16, it.md)
    • @it.has_teardown は Rails の after に相当するデコレータで,テスト終了時に行う後始末のメソッドである. 今のところ後始末すべきことはないので,メソッドだけ用意しておく. pass は MonoImage の空クラスを作った時にも使用した. インデントベースの Python では中身が空のブロックを表現できず,何もしない pass 文を記述しなければならない.
      @it.has_teardown
      def teardown():
        pass
    • @it.should デコレータは RSpec の it に相当するもので,ここがテストになる. RSpec の it に指定する文字列を should の中に記述する. デコレータの文字列が異なれば異なるメソッドになるため,def test(): は常に同じ表記で構わない.
      @it.should('have correct getter properties.')
      def test():
    • it.assertEqual は引数の二つが等しいかどうか判定するメソッドである. 何行も書きたくないので,Python の内包表現でリストを作成した. この表記では it.marray のタプルのそれぞれの中身に対して,x.bits というメソッドを呼び出し,結果をリストで返す. Ruby だと map に相当する処理になる.
      it.assertEqual([x.bits for x in it.marray], [8, 16, 0])
      この行は,RSpec だと以下のような表記になるだろう.
      expect(subject.map(&:bits)).to eq [8, 16, 0]
    • nose2 では最後に必ず以下の行を記述する.これを忘れるとテストをしてくれないので注意する.すっかり忘れてテストが動かず焦った.
      it.createTests(globals())
  • chokidar の結果は以下のようになる.
  • change:mono_image.py
    ERROR
    
    ======================================================================
    ERROR: LayerSuite
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/hkob/python3/hkob_pyimage/tests/test_mono_image.py", line 8, in setup
        it.mu8 = MonoImage(np.array([[10, 20, 30], [40, 50, 60]], dtype=np.uint8))
    TypeError: MonoImage() takes no arguments
    
    ----------------------------------------------------------------------
    Ran 0 tests in 0.000s
    
    FAILED (errors=1)

コンストラクタの作成

  • テストが通過するように mono_image.py にコンストラクタを追加する.
  • import numpy as np
    
    class MonoImage:
        def __init__(self, array, bits=8, signed=False):
            self._array = array
            self._bits = bits
            self._signed = signed
  • chokidar の結果は以下のようになる.
  • change:mono_image.py
    test 0000: should have correct getter properties. (test_mono_image.A MonoImage class) ... ERROR
    
    ======================================================================
    ERROR: test 0000: should have correct getter properties. (test_mono_image.A MonoImage class)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/hkob/python3/hkob_pyimage/tests/test_mono_image.py", line 19, in test
        it.assertEqual([x.bits for x in it.marray], [8, 16, 0])
      File "/Users/hkob/python3/hkob_pyimage/tests/test_mono_image.py", line 19, in <listcomp>
        it.assertEqual([x.bits for x in it.marray], [8, 16, 0])
    AttributeError: 'MonoImage' object has no attribute 'bits'
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    FAILED (errors=1)

読み込みプロパティの作成

  • bits という属性がないと言われたのでプロパティを追加する.
  • class MonoImage:
      (中略)
    
      @property
      def bits(self):
          return self._bits
  • chokidar の結果は以下のようになる.
  • change:mono_image.py
    test 0000: should have correct getter properties. (test_mono_image.A MonoImage class) ... ERROR
    
    ======================================================================
    ERROR: test 0000: should have correct getter properties. (test_mono_image.A MonoImage class)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/hkob/python3/hkob_pyimage/tests/test_mono_image.py", line 20, in test
        it.assertEqual([x.signed for x in it.marray], [False, True, False])
      File "/Users/hkob/python3/hkob_pyimage/tests/test_mono_image.py", line 20, in <listcomp>
        it.assertEqual([x.signed for x in it.marray], [False, True, False])
    AttributeError: 'MonoImage' object has no attribute 'signed'
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    FAILED (errors=1)
  • 残りの読み込みプロパティも同様に実装してみた.height だけ後から修正している.
  • class MonoImage:
      (中略)
    
      @property
      def signed(self):
          return self._signed
    
      @property
      def dtype(self):
          return self._array.dtype
    
      @property
      def width(self):
          return self._array.shape[1] if self._array.ndim > 1 else self._array.shape[0]
    
      @property
      def height(self):
          return self._array.shape[0] if self.ndim > 1 else 1
    
      @property
      def max_value(self):
          return 0 if self._bits == 0 else (1 << self._bits)-1
  • 最終的に chokidar の結果は以下のようになる.
  • change:mono_image.py
    test 0000: should have correct getter properties. (test_mono_image.A MonoImage class) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK

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