RでPython風docstringを実装してみる

by
カテゴリ:
タグ:

Pythonでは以下のように関数内にドキュメントを記述できます。

def add_one(x: float):
    """Add one to x.
    
    Parameter
    ---------
    x: float
    
    
    Return
    ------
    int
      x + 1
    """
    return x + 1

これを記述しておくとJupyter Notebookなんかでは、?add_oneとするだけでドキュメントが見られて便利です。

RでもRmdファイル内で同じようなことがしたいなーという方、できますよ。

関数のbody先頭にある文字列を取り出す

まずは.add_one関数として、先頭にroxygen2風のドキュメントを記述した関数を用意してみましょう。自由ドキュメント内で引用符や括弧などをエスケープせずに使えるように、 r"---(文字列)---"の記法を使ってます。これについてはhelp('"')を参照してください。

.add_one <- function(x) {
  r"---(Add one.
  
  @param x: A numeric vector.
  
  @value
  x + 1
  )---"
  x + 1
}

この関数の処理内容を取り出すには、body関数を使います。

body(.add_one)
## {
##     "Add one.\n  \n  @param x: A numeric vector.\n  \n  @value\n  x + 1\n  "
##     x + 1
## }

str関数で構造を確認するとlanguageオブジェクトです。特殊。

str(body(.add_one))
##  language {  "Add one.\n  \n  @param x: A numeric vector.\n  \n  @value\n  x + 1\n  "; x + 1 }
##  - attr(*, "srcref")=List of 3
##   ..$ : 'srcref' int [1:8] 1 25 1 25 25 25 1 1
##   .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x55d19c96eec8> 
##   ..$ : 'srcref' int [1:8] 2 3 8 7 3 7 2 8
##   .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x55d19c96eec8> 
##   ..$ : 'srcref' int [1:8] 9 3 9 7 3 7 9 9
##   .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x55d19c96eec8> 
##  - attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x55d19c96eec8> 
##  - attr(*, "wholeSrcref")= 'srcref' int [1:8] 1 0 10 1 0 1 1 10
##   ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x55d19c96eec8>

でもリスト化してあげると、ASTっぽい感じに処理が保存されてる感じだと分かります。

as.list(body(.add_one))
## [[1]]
## `{`
## 
## [[2]]
## [1] "Add one.\n  \n  @param x: A numeric vector.\n  \n  @value\n  x + 1\n  "
## 
## [[3]]
## x + 1

x + 1の部分をリスト化するとより分かりやすいですね。

as.list(body(.add_one)[[3L]])
## [[1]]
## `+`
## 
## [[2]]
## x
## 
## [[3]]
## [1] 1

というわけで、別にas.listしなくても2番目の要素を取り出せば、先頭の処理を抽出できます

body(.add_one)[[2L]]
## [1] "Add one.\n  \n  @param x: A numeric vector.\n  \n  @value\n  x + 1\n  "

あとはcatしてやると良い感じに表示できますね。

cat(body(.add_one)[[2L]])
## Add one.
##   
##   @param x: A numeric vector.
##   
##   @value
##   x + 1
## 

docstringを扱いやすくする

.add_one関数のままでは、ドキュメントの抽出にcat(body(.add_one)[[2L]])とせねばならず、直感的ではありません。

ちょっと改造したadd_one関数を定義して、help関数で良い感じに表示できるようにしてみましょう。

関数から直感的にdocstringを取り出せるようにする

まずはwith_docstring関数を用意し、関数の処理の先頭が文字列なら、該当部分を関数自身の__doc__属性に保存するwith_docstring関数を定義してみましょう。ついでに、後々のことを考えてこの文字列にはdocstringクラスを与え、関数にはwith_docstringクラスを与えておきます。

これでattr(add_one, "__doc__")すればヘルプを取り出せるようになり、取り出し方が直感的になります。

with_docstring <- function(f) {
  doc <- body(f)[2L][[1L]]
  attr(f, "__doc__") <- structure(
    `if`(is.character(doc), doc, ""),
    class = "docstring"
  )
  class(f) <- c("with_docstring", class(f))
  return(f)
}

add_one <- with_docstring(.add_one)
attr(add_one, "__doc__")
## [1] "Add one.\n  \n  @param x: A numeric vector.\n  \n  @value\n  x + 1\n  "
## attr(,"class")
## [1] "docstring"

.add_one関数を定義してからadd_one関数にするのが面倒であれば、最初から関数定義をwith_docstring関数でラップしましょう。 Pythonで言うところのデコレータ的発想ですね。

add_one <- with_docstring(function(x) {
  r"---(Add one.
  
  @param x: A numeric vector.
  
  @value
  x + 1
  )---"
  x + 1
})

docstringを良い感じにprintする

docstringの抽出が簡単になりましたが、表示がイマイチです。

attr(add_one, "__doc__")
## [1] "Add one.\n  \n  @param x: A numeric vector.\n  \n  @value\n  x + 1\n  "
## attr(,"class")
## [1] "docstring"

というわけでdocstringクラス用printメソッドを実装してあげましょう。単にcatするだけでOKです。

print.docstring <- function(x, ...) {
  cat(x)
  invisible(x)
}

attr(add_one, "__doc__")
## Add one.
##   
##   @param x: A numeric vector.
##   
##   @value
##   x + 1
## 

良い感じになりましたね。

help関数をマスクする

あとはhelp関数を実装するだけです。色んな方法が考えられますが、一番シンプルな方法はhelp関数を総称関数にする手です。そして、help.with_docstringメソッドでwith_docstringクラスオブジェクトだけの専用helpを用意します。ついでにhelp.defaultを用意すれば、その他のオブジェクトに対しては従来のutils::help関数を呼べます。

以下ではhelphelp.with_docstring関数にたいして引数を定義せず、後からutils::help関数の引数をformals関数を使ってパクってます。このあたりについてはJapan.R 2018で発表した「関数魔改造講座 (formals編)」を参照してください。

help <- function() {
  UseMethod("help")
}
help.default <- utils::help
help.with_docstring <- function() {
  print(attr(topic, "__doc__", exact = TRUE))
}
  
formals(help) <- formals(help.with_docstring) <- formals(utils::help)
help(add_one)
## Add one.
##   
##   @param x: A numeric vector.
##   
##   @value
##   x + 1
## 

printrパッケージを使えばR Markdown内でヘルプを使えるので、docstringがない関数についてもちゃんとヘルプを呼べます。

library(printr)
## Registered S3 method overwritten by 'printr':
##   method                from     
##   knit_print.data.frame rmarkdown
help(identity)
identityR Documentation

Identity Function

Description

A trivial identity function returning its argument.

Usage

identity(x)

Arguments

x

an R object.

See Also

diag creates diagonal matrices, including identity ones.

Enjoy more?

__doc__属性に加える時に文字列をうまく編集すれば、色んなことができます。

  • roxygen2として処理してちゃんとしたヘルプっぽくする
  • マークダウンなどとして処理して太字とかを表現する

など。

楽しいですねー。