Org-babelを使いつつ、doctestを使って、Pythonコードをテストする。
また長い見出しになってしまった。 ややこしいことをやろうとするから、こうなる。
ここ最近、Org-babelを使ってコードを書いている。 かなり可能性がありそうなプラットホームだと感じているので、修行中だ。 もう少し勉強すれば、データ前処理→分析→レポート作成まで、orgのなかで完結できそうだ。
論文も基本的には、Orgで書いてしまおうと思う。 多くのジャーナルはWordで提出だが、図表が別なことが多いので、本文はほとんどテキストのみだ。 Wordで書く理由はどこにもない。
講義のメモやレジュメは以前からOrgで書いている。 図もmatplotlibを勉強してみたところ、思ったより楽にきれいなものが書けそううなので、この際スライドも移行しようと思っている。
さて、こうなると、Orgの世界でほとんどの作業が完結するのだが、一つ気になることがある。 それがテストだ。
研究やちょこっとした作業用にプログラムのコードを書くことがよくある。 そういうコードをしばらくして再利用しようとすると、どこをどう変更してよいか分からなくなってしまったりする。 ちゃんとテストを書いておけばと思うのだが、あとの祭りだ。
そこで、今回いくつか検索して、参考になる方法を見つけたので、ワークフローをまとめておく。
テストを書くということ
まず、テストというのは、関数を書く際にそれが想定した仕様に合っているか、自動でチェックする仕組みのことだ。 研究では使うことが少ないけれど、やはり書くにこしたことはない。
テストコードやドキュメントを整備することでプログラムの品質が向上することは広く知られています。その有効性は、研究用のプログラムであっても変わりません (出所:「忙しい研究者のためのテストコードとドキュメントの書き方 - Qiita」、https://qiita.com/NaokiHamada/items/0689cd85fb3e1adcda1a )
僕もこの意見に全面的に賛成で、ドキュメントは以前から気をつかって書いていた。 が、テストは保守がめんどうで、書いたり書かなかったりだった。 Rに至っては書きかたもよくわからず、書かないままになっている。
今回、分析関係を全面的にPythonに移行することにしたので、ドキュメントとテストをちゃんと整備しつつ開発を進めることにした。
文書の中にコードを入れたい
さて、rmarkdownやJupyterでは、平文とコードを混在させながら分析を進めていける。固まり(チャンク)ごとにコードか平文かを指定しながら、文書を作成していけばよい。 必要に応じて、htmlにもpdfにも出力できる。 ツールを使えば、Wordとして出力も簡単だ。
一方、上記の方法だと、Pythonファイルにテストを書いていくので、Pythonファイルに直接ドキュメントを書く。 このばあいは、コメントとして書くために、出力時の形式がやや限定される。
別にそれでもよいのだが、もう少し前書き的なものや分析も書いておきたいこともある。
特に僕のばあいは、ライブラリを開発したいのではなく、自分の分析の際に必要な下働き的コードをちょこちょこと書くので、関数のドキュメントというよりも、全体の流れをきちんと説明したい。
そういうばあいは、Jupyterなどが選択肢になり、一般的にはテストを諦めざるをえない。
しかし、テストを諦めるかドキュメントを諦めるかの選択肢を迫るのは、上記サイトの意図するところではない。
むしろ、そこをなんとかするソリューションを考えるべきだ。
org-modeを使うとそのややこしい要求がクリアできる。org-babelはrmarkdownと同じようなものだと思えばよい。 どちらがいいのか?というと、ここは好みの問題かなと感じる。 僕は、長く使っているorg-modeを選択した。
org-modeからmarkdownへの変換は簡単なので、今はとりあえずそうというだけで、そこまで深刻な選択ではない。 (ここで、Wordを選択するならば、コードと文書は完全には別となるため、大きな選択だ。)
orgファイルを使えば、一つのファイルのなかで、分析コードとテストを書き、テストも実行できる。 コードだけを切りだしてPythonファイルを作ることも簡単だ。
サイトの紹介
今回とった方法は、以下のサイトに紹介されていた。当該サイトでは、Jupyterカーネルを使用することが想定されているが、僕はとりあえず、普通のPythonコードで利用することにした。 そのため、当該サイトでipythonとされているところをpythonと変えて、セッションも明示的に指定する。
https://kitchingroup.cheme.cmu.edu/blog/category/noweb/
Jupyterカーネルは近いうちに使用するようになるとは思うのだが、今は普通のPython環境で分析に不自由していない。
具体的な方法
ファイルの準備
ここでは、拡張子をorgとしたファイルを作成する。 そして、テストを関数に挿入するコードとテストを実行するコード、コードをpyhtonコードに出力するコードを記入しておく。 テンプレート化してしまってもよい。 参考サイトのものをそのままコピペだ。
見出しを二つ作っておく。
一つはテスト、もう一つは本文だ。本文の方は、見出し名に分析テーマでも書いておく。 また、テストは出力には含めなくてよいので、以下のようにしておく。
#+EXCLUDE_tags[]: test * テスト :test: * org-babelにdoctestを組みこむ 動機など。 コード本体もこちらに
次に、ざっくりと、ファイルの説明を書いてしまおう。なんの分析を行うのか、目的と期待される結果あたりを書いておく。
関数の仕様を決めて、外形をつくる
例えば、データを読む関数だ。
ここで、テストに名前をつけて、テストの挿入ポイントをつくってしまう。
doctest("のあとの部分にテストの名前を記入する。 例えば、my1sttestとしておこう。 このテストはいくつでも書ける。
def myfunc(x): """A sample function <<doctest("my1sttest")>> <<doctest("my2ndtest")>> Returns x+1 """ return x+1
テストに名前をつけて、テストを書く
テストは、先ほどつけた名前のブロックを作成して書きこむ。
この時点では、テストが成功するかどうかは気にしない。 (本体がないから、失敗する)
参照サイトそのままだが、一つ注意がある。:results drawerをつけておかないと、結果のブロックをBEGIN-ENDで囲んでくれない。
#+name: my1sttest #+BEGIN_SRC python :session :results drawer myfunc(1) == 4 #+END_SRC
読みこんだデータが期待しているデータなのかをチェックしておく。 行数だけでもいいし、変数の平均あたりをチェックしておいてもいい。 データが意図せず入れ替わっている可能性を排除できる。
関数を書く
関数本体を書く。
ここはがしがしと書いていこう。
今回は、return x+1とするだけだ。 実際には、もっと複雑なものになるはずだ。
テストを出力する
org-babel-tangleにカーソルをあわせて、C-cC-cする。 (C-cはコントロールを押しながら、cという意味)
テストする
テストは出力したファイルをpython -v moge.pyなどとすればよい。
メインのチャンクを実行すれば、すぐに結果が返ってくる。
メインのチャンクは最初にテストを行う処理を書いて、そのあとに実際の処理を書いていく。 こうすると、テストを最初に実行して、その後、実際の分析を行う。
テストと処理は別にしてもよいのかもしれないけれど、今のところ、その方法は分からないし、そこまで両者を分離する需要もないので、このままにしている。
#+BEGIN_SRC python :noweb yes :tangle mytest.py :session if __name__ == "__main__": import doctest doctest.testmod() x = 3 print(myfunc(3)) #+END_SRC
注意点
ここで紹介した方法は、docstringのテストの書式を気にしなくてよいので、とても楽だ。
だが、テスト対象のコードを変更したあとに、テストを「評価」してしまうと、挿入されるべきテスト結果も変わってしまう。 一度テスト結果を作成したら、そこはもう評価してはならない。
むしろ、一度テストを出力したらもうそこは、コピーペーストして書き変えた方がよいと思う。
例えば、与えられた数に1を追加する関数add_meを作る。このテストは、add_me(3) == 4として、結果はTrueとなるべきだ。
だが、なぜか気分が変わって、add_meは1ではなく、2を追加することにしてしまった。 (お酒を飲みながら、コードを書いていたのかもしれない)
ここで、テストコードを評価すると、結果はFalseとなる。 そしてこれは、関数の正しい挙動だ。
だが、当初の予定では、1を追加しなくてはならない。つまりテストはパスしてはならない。 酔っているときになにをしてしまったのか分からなくても、その結果は自動でチェックできたほうがいいに決まっている。
回避方法
やっぱり、org-modeはなんでもできる!
こんなときのために、org-babel-execute-subtree (C-c C-v s)がある。
テストとコードを別のツリーで管理して、コードの含まれるツリーだけ評価するようにする。
テストを評価するのは、なんらかの事情で関数の仕様を変えるときだけだ。
応用
こちらを見ると、orgファイルのなかにいちいち、docstringを置き変えるコードを書かずに、外部から読みこませる方法も紹介されている。
https://kdr2.com/tech/emacs/1805-approach-org-ref-code-to-text.html