第12章練習問題の前半の解答です。本章は問題が20問とかなり多いので、可読性の向上を目的に10問ずつに分けて公開します。以下ネタバレなので、読みたくない方は立ち去りましょう。問題文は載せませんので悪しからず(英語版はpdfで公開されている模様)。日本語版が必要なら買いましょう。その前にこの章の個人的なメモ。
- デフォルト引数も、可変長引数も仮引数の右に置かなければならない。可変長引数は1つだけ設定することが許されている。
- 可変長引数リストを呼び出すと、実引数はタプルとして関数定義に渡される
- 例外はtry~except構文で書く。tryで例外が発生しない場合、exceptの後ろにelseをつけておくと、その内容を実行することができる。
- 例外を生成する場合はraise 例外名(メッセージ)という書き方をするとよい。
- 低いところからスロー、高いところからキャッチする。つまり例外を投げられる可能性がある関数で逐一キャッチするのではなく、関数を呼んでいる側でまとめてキャッチするとよいということである。エラーの処理場所を減らすことで、エラーの処理方法を変えることが容易にできる。
テスト駆動開発(TDD)のメリット
- テストを必ず書かされることになる
- プログラマにとって全部のケースに通ればOKという目安が与えられる
- 文章よりも仕様を正確に与えられる
- テストコードを書いている最中に設計について考えることができる
では解答にうつります。
1
Pythonの名前つき引数が利用される場面は引数が大量にある場合である。順番を気にするよりも、引数名と値を与えるほうが楽だからである。逆に、本来の順序が分からなくなる恐れがあるので、名前を省略した書き方をした際にバグを引き起こすというリスクが発生する。
2
-1は有効な添字である。-1ではなくNoneを指定しておくことで、何も指定されていないということがはっきりする。
3
a)の解答はこちらが解答に近いと思います(4.7.3、4.7.4の項目)。
b)の解答
def test(var, *pargs, **kwargs): print var print pargs print kwargs test(1, 2, 3, x=4, y=5, z=6)
上のコードを実行すると次のようになる。
1 (2, 3) {'y': 5, 'x': 4, 'z': 6}
varには値が1つ収まり、pargsには名前のついていない可変長引数、kwargsには名前つきの可変長引数が当てはめられる。テキストにあるように、受け取った関数では、varは変数、pargsはタプル、kwargsは辞書になる。従って、上の例ではvar=1(1番左にある値を代入)、pargs=(2, 3)(名前がついておらず、かつまだ利用されていない実引数の集まり)、kwargs={'x':4, 'y':5, 'z':6}(名前つきの実引数の集まり)という結果になり、その値が表示される。引数に与える順序としては通常の引数、キーワード引数、名前なし可変長引数、名前つき可変長引数の順序で左から並べなければならない。引数は次の順序でマッチングを行う。
- 通常の引数の位置によるマッチング
- キーワード引数の名前によるマッチング
- *を使った引数が定義されていれば、上の2つでマッチングできなかったキーワード引数でない引数をタプルにまとめる
- **を使った引数が定義されていれば、上の3つでマッチングできなかったキーワード引数を辞書にまとめる
- その他指定されていない引数がデフォルト値が設定されていれば、その値を代入する
c)の解答
HTMLのタグを扱うときなどにキーワード引数を受け取れるようにしておくと便利である。例えばHTMLのimgタグには大量のオプション(altやwidthなど)が付けられるが、これを逐一引数に与えるのは面倒である。そこで指定したものだけをつけたいというときに利用すればよい。
4
ここで言う線が線分なのか直線なのかで解答が変わるので、問題文が曖昧になっています。問題文より、エラー処理は考えないということなので、入力に数値が必ず入るものとして、文字列が入ってきたときは考えないものとします。また、本問と次の問題において、問題文に登場する「線」は直線や半直線ではなく、線分と仮定します。
- [[1.0, 1.0], [1.5, 1.5]]と[[2.0, 2.0], [3.0, 3.0]]のように、線は重ならないものの、延ばせば一致するという場合のテスト。線分は重ならないのでNoneが返される。
- [[2.0, 2.0], [3.0, 3.0]]と[[2.0, 3.0], [3.0, 2.0]]のように、線が他方の線の真ん中あたりで重なる場合、交点の座標[2.5, 2.5]が返される。一般的に想定される重なり方をテスト。
- [[1.0, 1.0], [2.0, 2.0]]と[[1.5, 1.5], [3.0, 3.0]]のように、線が一部重なる場合、第一引数[[1.0, 1.0], [2.0, 2.0]]が返される。
- [[-1.0, 0.0], [4.0, 0.0]]と[[0.0, 0.0], [0.0, 2.0]]のように、一方が他方を包含する場合のテスト。第一引数[[-1.0, 0.0], [4.0, 0.0]]が返される。
- [[0.0, 0.0], [0.0, 2.0]]と[[0.0, 1.0], [2.0, 1.0]]のように、線が他方の線の端で重なる場合、交点の座標[0.0, 1.0]が返される。ぎりぎり重なるという状況のテスト。
- [[1.0, 1.0], [2.0, 2.0]]と[[2.0, 2.0], [3.0, 3.0]]のように、線の端と端で重なる場合のテスト。交点の座標[2.0, 2.0]が返される。
整数を引数に取った場合、実数として処理されるかということもテストで考えていましたが、次の問題文を読む限り、エラー扱いする入力のようなので、効果的な6種類のテストに含めることはできない模様です。
5
エラー処理のコードは全体の8割ほど占めると言われているので、まずはエラーが正しく処理されているかを確認する必要がある。エラーが起きない基本的なロジックではバグはあまり発生しないということもエラーからチェックすべきということを後押しする。以下、エラーが正しく処理されていることを確認するために考える6種類のテストを列挙した。
- [["a", 1.0], [2.0, 3.0]]のような、引数に文字列が含まれる場合にValueErrorが返されるか
- [[1, 2.0], [3.0, 4.0]]のような、引数が浮動小数点数になっていない場合にValueErrorが返されるか
- [[1.0, 2.0], [3.0, 4.0, 5.0]]のように、浮動小数点数の対になっていない場合にValueErrorが返されるか
- [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]のように、浮動小数点数の対になっていない場合にValueErrorが返されるか
- [[1.0, 1.0], [1.0, 1.0]]のような線を特定する2点が同じ点であった場合にGeometryError例外が返されるか
- [[1, 1], [1, 1]]のような浮動小数点数でない値(整数でなく文字列でもOK)の対が一致した場合、ValueErrorの方が返されるか(例外の実装の順序を確認)
6
106通り。前提は元素記号が国際コードに関係のないということである。元素記号と国際コードを1回以上引数に与えるようにテストケースを作成するので、106の元素と26の言語という数の大きい方を選べばよい。
7
==とするのがよくない。丸め誤差が発生してしまうため、必ずしも==で値が同じであることを正確に判定できるわけではない。実数が等しいかそうでないかを判定する際には差の絶対値が十分小さいということで評価するべき。このコードではabs(actual-expected)<10**-15などにする。10**-15とするのは、浮動小数点の丸め誤差の最小単位が10**-16程度なので、それよりも若干大きめにとる必要があるためである。もしくはDecimalモジュールを利用した方法を取ることを考える必要がある。
8
- Acids、Basesというものの存在を意識せずテストするべき。つまり、インタフェースである引数と返り値のみ既知として、内部仕様に依存したテストはやめるべき。関数の書き換えなどにより、動作しなくなる恐れがある(例えばAcidsという名前を書き換えられたら途端に動作しなくなる)。
- リリースしようとしているコードをテストのためだけに書き換えることはよくない。書き換え前の本番用データに問題があった場合に発生しうるバグを考慮するべき。
9
上はex12_9.py、下はtestprefixes.pyという名前がついています。関数のテストは4種類書けということなのですが、これにあいまいな仕様を含むのか含まないのかが問題文からではわかりません。ひとまず、あいまいな仕様の箇所も固定してテストするということにしています。あいまいな仕様というのは、空文字が含まれた場合の対処と、文字列の途中にある空白の取り扱いになります。
# -*- encoding: sjis -*- def all_prefixes(string): ''' 空文字が入ってきた場合の返り値に対する仕様がないこと、 複数の語からなり、途中に空白がある場合の仕様がない点は問題である。 今回は、空白も語の途中と扱うものと考えることにする。 ''' ret = set() for i in range(1,len(string)+1): tmp = string[:i] ret.add(tmp) return ret if __name__ == '__main__': print all_prefixes('lead') print all_prefixes('') print all_prefixes('a pen')
テスト用コード
# -*- encoding: sjis -*- import nose from ex12_9 import all_prefixes def test_null(): ''' 対象が空文字からなるテスト ''' assert all_prefixes('') == set([]) def test_one_char(): ''' 対象が1文字からなるテスト ''' assert all_prefixes('a') == set(['a']) def test_one_word(): ''' 対象が複数文字、1語からなるテスト ''' assert all_prefixes('lead') == \ set(['l', 'le', 'lea', 'lead']) def test_two_words(): ''' 対象が2語(途中に空白を含む)からなるテスト ''' assert all_prefixes('a pen') == \ set(['a', 'a ', 'a p', 'a pe', 'a pen']) if __name__ == '__main__': nose.runmodule()
10
10個テストケースを思いつけと言われても、難しいですよね。最後に作成したテストケースである、リストの中のリストは捻りだした感じになってしまいました。
# -*- encoding: sjis -*- import nose def is_sorted(data): if not type(data) == list: raise ValueError for elem in data: # 要素が整数でないものがあった場合 if not isinstance(elem, int): raise ValueError copy = data[:] return data == sorted(copy) def test_zero_elem(): ''' 空リストはリストであり、整数でない値を含まないので Trueが返されるべき ''' assert is_sorted([]) == True def test_tuple(): ''' タプルはリストでないのでValueError ''' try: is_sorted((1, 2, 3)) assert False except ValueError: pass except: assert False def test_list_in_list(): ''' リストの中にリスト ''' try: is_sorted([1, 4, [5, 7], 8]) assert False except ValueError: pass except: assert False def test_char_included(): ''' 要素に文字列を含む場合はValueError ''' try: is_sorted([1, 3 ,'ab2c']) assert False except ValueError: pass except: assert False def test_float_included(): ''' 実数が含まれた場合はValueError ''' try: is_sorted([1, 2, 3.14, 5, 7]) assert False except ValueError: pass except: assert False def test_one_elem(): ''' 1要素のみのリスト ''' assert is_sorted([3]) == True def test_all_the_same(): ''' 全部の要素が同じ値の場合 ''' assert is_sorted([4, 4, 4, 4, 4]) == True def test_dsc(): ''' 降順の場合 ''' assert is_sorted([4, 3, 2 ,1]) == False def test_asc(): ''' 昇順の場合 ''' assert is_sorted([1, 2, 5, 9]) == True def test_normal(): ''' 増加・減少がともにある、割と一般的なデータ ''' assert is_sorted([1, 2, 7, 9, 1]) == False if __name__ == '__main__': nose.runmodule()
0 件のコメント:
コメントを投稿