ひょっとして docstring と doctest、あまり浸透してない?

Python 使いにとっては空気のようなもの、と思い込んでたし、ワタシが同僚に Python を伝えるなら、かなり真っ先に教えるものなので、ちょっとわかってないのかしら、という記事を見つけてしまってヘコむ。

Sphinxは、Doxygenや javadoc、perldoc などの「コメントからドキュメントを自動生成するツール」ではない。本来。あくまでも「ドキュメントを作るツール」ね。まずは。そして、Doxygenのように使うことも出来る。これが sphinx-apidoc ね。けどこれはあくまでもオカズ。主食じゃないです。

名言されてはいないけれど、ワタシはSphinxが目指すことには一つには「Doxygen的自動生成ドキュメントへの反旗」があるんじゃないかと思っている。確かに「コードからドキュメント自動生成」は、開発者の手間だけは減らせるけれど、わたしがみるに、9割の「Doxygen的ドキュメント」は、利用者目線に全く立ってはおらず、まずほとんどが使いにくい。なんでこうなるかといえば、「リファレンスしか作れない(というよりは作らない)」から。プログラムに対応付くドキュメントしかドキュメントされないなら、そらそーなるわい。(出来ないという話ではなく、開発者はまずほとんどがサボるので、書かないのだ。)

だからね、Sphinx = sphinx-apidoc としてしまうような紹介は大変よろしくない、とワタシは思う。確かにそれも一面、だけれども、それじゃぁ多分 Doxygen のほうがいーぜ、てことにならあね。(ワタシも事実そう思う。自動ドキュメント付けなら Doxygen の方が楽と思うよ。)

さて。「docstring と doctest の浸透度への懸念」。あぁ…。「Sphinx ではこのようなコメントの書き方を」…。えーっと、これ、コメントじゃなくて docstring です。

docstring とコメントの違い、こそが、Python を Python たらしめている、と言えるほどにイケてる特徴、なのよ。というのもね…:

 1 >>> import pyproj
 2 >>> print(pyproj.Proj.__new__.__doc__)
 3 
 4         initialize a Proj class instance.
 5 
 6         Proj4 projection control parameters must either be given in a
 7         dictionary 'projparams' or as keyword arguments. See the proj
 8         documentation (http://trac.osgeo.org/proj/) for more information
 9         about specifying projection parameters.
10 
11         Example usage:
12 
13         >>> from pyproj import Proj
14         >>> p = Proj(proj='utm',zone=10,ellps='WGS84') # use kwargs
15         >>> x,y = p(-120.108, 34.36116666)
16         >>> 'x=%9.3f y=%11.3f' % (x,y)
17         'x=765975.641 y=3805993.134'
18         >>> 'lon=%8.3f lat=%5.3f' % p(x,y,inverse=True)
19         'lon=-120.108 lat=34.361'
20         >>> # do 3 cities at a time in a tuple (Fresno, LA, SF)
21         >>> lons = (-119.72,-118.40,-122.38)
22         >>> lats = (36.77, 33.93, 37.62 )
23         >>> x,y = p(lons, lats)
24         >>> 'x: %9.3f %9.3f %9.3f' % x
25         'x: 792763.863 925321.537 554714.301'
26         >>> 'y: %9.3f %9.3f %9.3f' % y
27         'y: 4074377.617 3763936.941 4163835.303'
28         >>> lons, lats = p(x, y, inverse=True) # inverse transform
29         >>> 'lons: %8.3f %8.3f %8.3f' % lons
30         'lons: -119.720 -118.400 -122.380'
31         >>> 'lats: %8.3f %8.3f %8.3f' % lats
32         'lats:   36.770   33.930   37.620'
33         >>> p2 = Proj('+proj=utm +zone=10 +ellps=WGS84') # use proj4 string
34         >>> x,y = p2(-120.108, 34.36116666)
35         >>> 'x=%9.3f y=%11.3f' % (x,y)
36         'x=765975.641 y=3805993.134'
37         >>> p = Proj(init="epsg:32667")
38         >>> 'x=%12.3f y=%12.3f (meters)' % p(-114.057222, 51.045)
39         'x=-1783486.760 y= 6193833.196 (meters)'
40         >>> p = Proj("+init=epsg:32667",preserve_units=True)
41         >>> 'x=%12.3f y=%12.3f (feet)' % p(-114.057222, 51.045)
42         'x=-5851322.810 y=20320934.409 (feet)'

つまり docstring は Python オブジェクトの「属性」。なので、(Python を -OO で起動しない限りは)プログラムからいつでも参照出来る。かなり前に「Python初学者が最初に学ぶべきこと」で触れた pydoc も、対話モードでの help も、結局のところ、この __doc__ を参照している。これはもう「コメント」ではなく「ランタイム機能」、なのよ。だからこそ「空気のような存在」、なのだと。

ゆえに、Sphinxの sphinx-apidoc が「認識するコメントは」ではなくて、「認識する docstring は」という説明じゃないとマズい。(なお、epydoc も docstring から API ドキュメントを生成するが、これも docstring の特定のルールベースである。)

docstring そのものについてはこれだけなんだけれど、「Python 大好き!」になるきっかけになるかもしれない話はまだ続きがある。

上で引用した pyproj の docstring には、「Python 対話セッションで示した例」が書かれているでしょう? この部分ね:

1 Example usage:
2 
3 >>> from pyproj import Proj
4 >>> p = Proj(proj='utm',zone=10,ellps='WGS84') # use kwargs
5 >>> x,y = p(-120.108, 34.36116666)
6 >>> 'x=%9.3f y=%11.3f' % (x,y)
7 'x=765975.641 y=3805993.134'

このね、「Python 対話セッションで示した例」を「テスト出来る」、というのが、doctest。これがさ、「スゲーよ Python」なわけよ。実際にやってみる?

doctest_example.py
 1 # -*- coding: utf-8 -*-
 2 def my_nanika(s):
 3     """
 4     Example:
 5 
 6     >>> my_nanika(10)
 7     20
 8     >>> my_nanika(100)
 9     200
10     >>> my_nanika("a")
11     'aa'
12     """
13     return s * 2
14 
15 
16 if __name__ == '__main__':
17     import doctest
18     doctest.testmod()

これを、「実行」することでテスト出来ます:

テストが全部パスすると何も起こりませんよ
1 me@host: ~$ python doctest_example.py
2 me@host: ~$ 

間違ったテスト、もしくは実装の間違い、を試してみろ:

doctest_example.py
 1 # -*- coding: utf-8 -*-
 2 def my_nanika(s):
 3     """
 4     Example:
 5 
 6     >>> my_nanika(10)
 7     20
 8     >>> my_nanika(100)
 9     200
10     >>> my_nanika("a")
11     'aa'
12     """
13     return s * 3  # 誤った実装
14 
15 
16 if __name__ == '__main__':
17     import doctest
18     doctest.testmod()

今度はこうなる:

 1 me@host: ~$ python doctest_example.py
 2 **********************************************************************
 3 File "doctest_example.py", line 6, in __main__.my_nanika
 4 Failed example:
 5     my_nanika(10)
 6 Expected:
 7     20
 8 Got:
 9     30
10 **********************************************************************
11 File "doctest_example.py", line 8, in __main__.my_nanika
12 Failed example:
13     my_nanika(100)
14 Expected:
15     200
16 Got:
17     300
18 **********************************************************************
19 File "doctest_example.py", line 10, in __main__.my_nanika
20 Failed example:
21     my_nanika("a")
22 Expected:
23     'aa'
24 Got:
25     'aaa'
26 **********************************************************************
27 1 items had failures:
28    3 of   3 in __main__.my_nanika
29 ***Test Failed*** 3 failures.
30 me@host: ~$ 

doctest はテストそのものの代わり、には使えません。例えば網羅性テストをここでやっちゃったらさ、読むほうがたまったもんじゃないでしょう? だけどこれは、「実装を書き始める」時点からの「テストファースト(いわゆるTDD)」の足しになるし、「ドキュメントと実装の乖離を防ぐ」ためには十二分に役に立つ。

いまや Ruby にも「doctest的なこと」が出来るようにする動きさえもあるぞ。これが Python の発明なのかどうかは定かではないけれど、これがあってこその Python だ、と思っている Python ユーザは間違いなく多いと思う。

公式ドキュメントがもちっと大々的に伝えてもいいのかもなぁ。明らかに「コアなPythonコミュニティ」とそうでない周辺の温度差を感じる。