Pythonのwith構文で例外を補足する実例
以前こういう記事を書きました。
withを出る時に呼ばれる__exit__
メソッドでは、引数として内部で生じた例外を受け取る事ができることを一応記載しているのですが、どういうシチュエーションでこの機能が使われるのか思い浮かばなかったので適当にはぐらかしていました。
が、Pythonのunittestモジュールで実際に使っていることが分かりましたので、どういう使い方なのか紹介したいと思います。
unittestモジュールのどこで使われているのか
unittestモジュールは、通常unittest.TestCaseクラスを継承する形で利用されます。継承したクラスでは、TestCaseクラスで定義されている各種assertメソッドを利用することが出来るようになります。そのassertメソッドの1つとして、例外が発生するかをテストできるassertRaisesメソッドがあります。
以下は、assertRaisesの利用例です。恐らく3.xでも同様だと思います。
#!/usr/bin/env python2.7 import unittest def print_not_empty_string(s): if type(s) is not str: raise TypeError("not str type!") if len(s) == 0: raise ValueError("length is 0!") print s class PrintTest(unittest.TestCase): def test_print(self): with self.assertRaises(TypeError): print_not_empty_string(100) with self.assertRaises(ValueError): print_not_empty_string("") print_not_empty_string("no error occured.") if __name__ == "__main__": unittest.main()
assertRaisesの仕組み
最初に述べたとおり、withで例外を捕捉しています。
以下は、Python2.7.6における該当部分のソースコードです。
class _AssertRaisesContext(object): """A context manager used to implement TestCase.assertRaises* methods.""" def __init__(self, expected, test_case, expected_regexp=None): self.expected = expected self.failureException = test_case.failureException self.expected_regexp = expected_regexp def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): if exc_type is None: try: exc_name = self.expected.__name__ except AttributeError: exc_name = str(self.expected) raise self.failureException( "{0} not raised".format(exc_name)) if not issubclass(exc_type, self.expected): # let unexpected exceptions pass through return False self.exception = exc_value # store for later retrieval if self.expected_regexp is None: return True expected_regexp = self.expected_regexp if isinstance(expected_regexp, basestring): expected_regexp = re.compile(expected_regexp) if not expected_regexp.search(str(exc_value)): raise self.failureException('"%s" does not match "%s"' % (expected_regexp.pattern, str(exc_value))) return True class TestCase(object): ... def assertRaises(self, excClass, callableObj=None, *args, **kwargs): """... """ context = _AssertRaisesContext(excClass, self) if callableObj is None: return context with context: callableObj(*args, **kwargs)
__exit__
に渡る引数は、exc_typeが例外の型、exc_valueが例外オブジェクトです。
_AssertRaisesContext.__exit__
では、まずexc_typeが指定された例外型かをチェックしています。その後、not expected_regexp.search(str(exc_value))
で値が正しいかをチェックし、結果がFalseなら、self.failureException
が呼ばれてテスト失敗となります。
exc_type, exc_valueを無駄なく使っており、withの良い実例だと思います。