Python の assert 文でテストする。


assert 文は簡単にテストをする方法を提供してくれます。


f:id:domodomodomo:20180818002132p:plain



書式は、次の通りで、「条件」が False になると AssertionError が発生します。

assert 条件, エラー文

assert 文は、プログラム内にデバッグアサーション (debugging assertion) を仕掛けるための便利な方法です:
7.3. assert 文 ... 7. 単純文 (simple statement)

◯ 簡単な使用例

1 + 1 == 2 なので 1 + 1 == 3 など間違ったことを書くとエラーになってしまいます。

assert 1 + 1 == 3, '残念!'
>>> assert 1 + 1 == 3, '残念!'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: 残念!
>>> 

◯ もう少し混みいった使用例

例えば sort をテストするコードを書いてみます。test 関数は、0 ~ 99 のランダムな 100 個の整数を正しくソートできるかを 100 回確認させています。

import random


def main():
    """bubble_sort と incorrect_sort をテストする。"""
    test(bubble_sort)
    test(incorrect_sort)


def test(sort):
    for _ in range(100):
        lst = [random.randint(0, 99) for _ in range(100)]
        sort(lst)
        assert is_sorted(lst), f'{sort.__name__} is incorrect.'


def is_sorted(lst):
    return all(lst[i] <= lst[i+1] for i in range(len(lst) - 1))


def bubble_sort(lst):
    """バブルソート"""
    n = len(lst)
    for i in reversed(range(n)):
        for j in range(i):
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]


def incorrect_sort(lst):
    """間違ったソート"""
    # ソートしたと見せかけて
    lst.sort()
    # 一部、値を入れ替えてソートを崩す。
    i, j = random.randint(0, 99), random.randint(0, 99)
    lst[i], lst[j] = lst[j], lst[i]


if __name__ == '__main__':
    main()
>>> # incorrect_sort は shuffle してるのでエラーになる
... 
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File "<stdin>", line 5, in test
AssertionError: incorrect_sort is incorrect.
>>> 

最適化オプション

実行時に最適化オプション -O を指定することで assert 文は実行されなくなります。

$ python -O
>>> assert 1 + 1 == 0, 'This is error.'
>>> # 何も起こらない。

-O ... 1.1.3. その他のオプション
Remove assert statements and any code conditional on the value of __debug__. Augment the filename for compiled (bytecode) files by adding .opt-1 before the .pyc extension (see PEP 488). See also PYTHONOPTIMIZE.


また、-O を指定した時、組み込み定数 __debug__ には False が代入されます。

$ python -O
>>> if __debug__:
...     print('Hello, world!')
...
>>> # 何も表示されない

__debug__ ... 3. 組み込み定数
この定数は、Python が -O オプションを有効にして開始されたのでなければ真です。 assert 文も参照して下さい。

なんで assert は文なの?

例えば最初の例なら、if 文と raise 文で、全く同じことができます。 なぜ、assert は関数ではなく文なのでしょうか?

def assertion(condition):
    if __debug__:
        if not condition:
            raise AssertionError

if assertion(1 + 1 == 3):
    raise AssertionError
>>> def assertion(condition):
...     if not condition:
...         raise AssertionError
... 
>>> if assertion(1 + 1 == 3):
...     raise AssertionError
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in assertion
AssertionError
>>> 


答え: 最適化のため

実行時に最適化オプション -O を指定することで assert 文は実行されなくなります。 もし関数だった場合、引数が評価されてから、関数が実行され、それから実行するかどうかの判定がなされます。 これは通常の関数では実装することができません。

def assertion(condition):
    if __debug__:
        if not condition:
            raise AssertionError

def f(x):
    print(x)
    return False

assertion(f('Hello, world!'))
assert f('Nihao, shijie!')
$ # 最適化オプション有効
$ python -O
>>>
>>> ...  # 中略
>>>
>>> assertion(f('Hello, world!'))
Hello, world!  <-------- 関数だと print(x) が実行されてしまう 
>>> assert f('Nihao, shijie!')
>>>

Are there any advantages to having assert be a statement (and reserved word) instead of a function?

  1. Cannot be reassigned to a user function, meaning it can be effectively disabled at compile time as @mgilson pointed out.

  2. The evaluation of the second, optional parameter is deferred until if/when the assertion fails. Awkward to do that with functions and function arguments (would need to pass a lambda.) Not deferring the evaluation of the second parameter would introduce additional overhead.

design of python: why is assert a statement and not a function? - stackoverflow

◯ なんで最適化(無効化)するの?

答え: 本番環境で例外を投げるために assert 文を乱用させないため

assert 文は最適化オプションをつけると評価されません。 ということは、assert 文は、テスト専用の文なのかなと思いました。 本番環境では使わないということかなと思っています。

ここで疑問なのは、テスト用のコードと本番用のコードがごっちゃになることってあるのかなという疑問があります。 混ざることがないのだから専用の文ではなくて関数でよかったんじゃないかなとも思いましたが。

結局は本番環境で例外を投げるために assert 文を乱用させないためかなと思いました。 以下の記事は Go 言語に関する記事ですが、参考になります。

アサート(assert)がない理由は?

Go言語では、アサートを提供していません。アサートが便利である点は疑う余地はありませんが、 我々の経験的には、プログラマはアサートを、適切なエラーハンドリングとエラーレポートを考慮せずに済ますためのツールとして使っています。 適切なエラーハンドリングとは、致命的ではないエラーが起きたときにクラッシュさせずに、処理を継続させることです。

また、適切なエラーレポートとは、そのレポートされたエラーが直接的かつ適切な内容で、 プログラマが巨大なクラッシュトレースに対して行う調査を手助けします。 正確なエラー情報は、エラーを調べているプログラマがコードを熟知していないときは、特に重要です。

その他の文と関数の例

1. print 文から print 関数に

反対に、Python 2 の頃は print 文がありましたが廃止されて関数になりました。これは print が文である必要がないからです。

The following arguments for a print() function are distilled from a python-3000 message by Guido himself [2]:

print is the only application-level functionality that has a statement dedicated to it. Within Python's world, syntax is generally used as a last resort, when something can't be done without help from the compiler. Print doesn't qualify for such an exception.

PEP 3105 - Make print a function

2. del 文

del がなぜ関数ではなく文であるかについて説明しています。

Python の変数と属性、代入とコピー - いっきに Python に詳しくなるサイト

全部テストをしないといけないの?

たまに本とかを読んでいると全部テストすることが、必要ですとか書けば書くほど良いいです。と書いてあったりして、一瞬戸惑います。

Django の公式チュートリアルでも最初の方にテストに関するページがでてきて詰まりました。 必要性が理解できないものを勉強するのは、苦心します。 どこを力点に置いて掘り下げていけばいいかわからないからです。

個人の趣味でやっているときは、いらないかなと... 手動で動作確認することが面倒になった時が、テストを書くタイミングかなと思ったりします。 個人の趣味の時はですが...

テストのための2つのツール

最初はペチペチ作れる pytest でいいんじゃないかなと思ったりもします。

1. pytest

いろんなテストを書いてたくさん assert 文が書いたテストができたら pytest でまとめ上げると便利かもしれません。最近、知りました。

2. unittest

pytest は外部ライブラリなので pip install pytest としないと使えません。 unittest は標準ライブラリなので、最初から使うことができます。

pytest は assert 文をつぎはぎして書けるのに対して unittest は、 わざわざクラスを定義したりしないといけなくて少しもっさりした印象です。

最初は、なんでわざわざクラスを書かないといけないのか?命名規則が PEP 8 に反するだったりするのはなんで?と色々と不思議だったのですが、 おそらく Javaフレームワークである JUnit から触発されたものだからかなと思います。

unittest ユニットテストフレームワークは元々 JUnit に触発されたもので、 他の言語の主要なユニットテストフレームワークと同じような感じです。
26.4. unittest - 標準ライブラリ


全然、わかっていないのですが Java は、クラスに属したメソッドしか定義できないため、そのフレームワークである JUnit に触発されたら自動的に Python の unittest もテスト関数をクラスに属したものにするような設計になったのでしょうか... もしかしたらクラスに所属させておいた方が、もっと複雑なことが色々できるのかもしれません。

最初はペチペチ作れる pytest でいいんじゃないかなと思ったりもします。