Pythonで関数やクラスの説明を関数名などの直下に"""
で囲んだ形で書くと
docstring
として保存され、help
などで説明を見る際に表示できます。
docstringに引数などの説明を与えた場合、その関数などを含むクラスを継承して、 関数に引数をいくつか足して再定義することもあるかと思います。
その際に、通常はもともとある引数の説明も全部書かないと新しいクラスでhelp
したときに表示されませんが、
ドキュメントが重複してしまうのでそこをうまく継承して重複をなくしたい、という場合にどうするか。
docstring
Pythonで、関数の最初に、
1 2 3 |
|
みたいに書いておくと、
1
|
|
とすると、
1 2 |
|
みたいな表示をしてくれます。
これは、func
が__doc__
という名前の値を持っていてそこに保存してあります。
1
|
|
1
|
|
このdocstringがhelp
で表示される際には文字列そのままではなく、インデント等をいい感じに直してから表示するようになっています。
1 2 3 |
|
と
1 2 3 4 5 |
|
は、それぞれ、
1
|
|
と
1 2 |
|
で、改行が含まれていたり先頭に空白文字が付いたり違いますが、help
では共に同じ出力になります。
これらのdocstringは"""
で始まるコメントが何らかの処理を行うより前に最初に書かれていると__doc__
に収納されます。
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 |
|
また#
から始まるコメント行はdocstringにはなりません。
#
から始まる部分は実行時に完全に無視される形です。
1 2 3 4 5 6 |
|
1 2 3 |
|
逆に、"""
から始まる行は処理として認識されるので、
1 2 3 4 5 6 |
|
と、#
のコメントだけだと中身無しの関数を定義しようとすると
IndentationError: expected an indented block after function definition
のエラーが出ますが
1 2 3 4 5 6 |
|
であればエラーになりません。
docstringのスタイル
docstringに関しては PEP 8、および PEP 257に記述がありますが、ざっくりと
- すべてのpublicなモジュール、関数、クラス、メソッドに書く。
- 1行目に短くサマリーを書く
- サマリーだけの場合には
"""
で前後を閉じる1行で書く。
- サマリーだけの場合には
- 詳細を追記する場合には1行空けてその後に書く。
- 複数行にした場合は最後の行は
"""
だけの行で閉じるようにする。
また、PEP 8の方に、doscrtingなどは1行最大72文字まで、と書いてあります。 通常のコードは79文字まで。
短く定義されているのは、古いFortranなどでも見られる72文字制限同様、パンチカード時代の名残とか 1。
また、クラス内の関数とかだと、docstringを書く位置が通常8文字スペースの後になるし、
help
の表示時にも8文字前に加わる事から80幅になる程度、という感じ。
(ただ、これだと80になるのでcodeの79の制限とはちょっと辻褄が合わない?)
この辺はcodeの文字幅も色々と議論があるところなのでわかりやすいように書けばよいかと。
PEP 257には何を書くべきか、などは書かれていますが、具体的に関数の引数や返り値の説明などを含め、どの様に書くべきか、は定義されていません。
それらに関しては、主に以下のものが使われているようです。
- NumPy: Style guide — numpydoc v1.6.0rc1.dev0 Manual
- Google: styleguide Style guides for Google-originated open-source projects
- Sphinx: Writing docstrings — Sphinx-RTD-Tutorial documentation
SphinxはPython製のドキュメント生成ツールですが、Pythonコードをドキュメントすることも出来て、その際にdosctringをいい感じにリンクとかつけて引数などを書いてくれます。 そのため、ちょっとコードチックな書き方になっています。 なお、SphinxではNumPy/Google Styleのdocstringでも拡張機能を使うことでドキュメント化することができるとのこと。
NumPyやGoogleのものは同じような感じで、引数の書き方などが少し違う感じです。
引数や返り値に関する記述の仕方としては
Numpy:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Google:
1 2 3 4 5 6 7 8 9 10 11 |
|
引数を表すセクションが、NumPyはParameter
、GooleがArgs
となっているのが大きな違い。
細かい違いとして、Googleの方は引数などがインデントされて書かれているところや、:
の後に1行にまとめても良い、となっているところ。
また、NumPyは引数の名前の後に型を書きますが、Googleの方は型に関しては
The description should include required type(s) if the code does not contain a corresponding type annotation
という感じで説明文中に書くように書かれています。
コードの動作自体に影響があるわけではないのでこの辺りは好きなものを。
継承先のクラスでのdocstring
クラス内の関数のdocstringは、継承先のクラスで再定義された場合、docstringが書かれていれば上書きされます。 再定義されなかったり、再定義されてもdocstringが書かれてなければ元のクラスで定義されたものがそのまま使われます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
1 2 3 4 5 6 7 8 |
|
こんな感じ。
クラス自体にもdocstringがつけられますが、こちらは継承先で書かれなければ何も出ません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
ParametersやReturnsを引き継ぎたい
クラスを継承した際、クラス内の関数を再定義する場合でも基本的には 引数などは同じもので、内部処理が違うといったことが多いかと思います。
その場合、docstringの最初の説明部分やReturnsの説明部分は変更するために書く必要がありますが、 Parameters(Args)に関しては、親のクラスと全く同じものであることもあります。
それを重複して書くのは無駄なのでできれば変数みたいにして引き継いだりしたいところです。
あらかじめ変数を定義して中で使う方法
よく使う変数など、複数の関数で同じ説明を書くこともあるかと思います。 これはクラス継承などなくとも起こることです。
そのようなときはあらかじめそれを変数に入れておいて使えれば便利です。
ただし、docstringではformat
やfstringのような文字列の代入は出来ません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 |
|
両方ともこれらの行は何らかの処理とみなされるだけで__doc__
には入りません。
ここで無理やり使うためには__doc__
を後から直接変更します。
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 |
|
fstringはそのままだと無理なので、eval
とかを使えば出来ないことはないですが
2
この場合はformat
を使う方が無難です。
Classのdocstringに関しては、__doc__
に代入する形で直接formatやfstringで定義することが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
こちらはfstringでも可能です。
同じように関数で直接__doc__
を指定してもうまくいきません。
このように変数が使えるなら、必要な変数をまとめて定義しておいて、継承先のクラスでも それを使って書けば無駄が省けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
インデントをきちんと考慮してあげないとずれてしまうので、最初の変数を作るときに 少し気をつける必要があります。
あらかじめ変数を定義して中で使う方法 with decorator
上と同じことをデコレーターを使って実現することも出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
こんな関数を用意しておいて、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
とすれば
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
こんな感じになります。
クラス内関数ではインデントの量を直したものを別途用意している部分は もうちょっとスマートに出来ると思いますが、とりあえずはこんな感じで必要な事が出来るかと思います。
上の例ではグローバル変数を直接デコレーターの中で使ってますが、 引数を持つデコレーターにして、必要な変数だけ入力するようにしてあげることも出来ます 3。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
みたいにして、
1 2 3 4 5 6 7 8 9 |
|
こんな感じ。
__init_subclass__を利用する
Python 3.6から導入された__init_subclass__
は、
クラスが継承された時に実行される関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 |
|
これを利用して、関数を再定義した際、
親クラスの関数などの__doc__
を引き継ぐようにすることができます。
1 2 3 4 5 6 7 8 9 10 |
|
Inherit docstrings in Python class inheritance - Stack Overflow)
この例は再定義した際にdocstringを書かなかった場合、親クラスの同じ関数のdocstringを そのまま引き継ぐようにしています。
Parametersの部分だけ引き継ぎたかったりする場合には、 新しい関数にもdocstringを書いて、それを良い感じで元のdocstringと合わせる様な 処理を入れる必要があります。
また、同じ様なことをメタクラスを使って__new__
関数の中で色々とやることも出来ます。
上のような簡単な例であれば__init_subclass__
の方がすっきりと出来ますが、
複雑なことをやろうと思ったらメタクラスを定義したほうがきれいに書けることもあるかと思います。
既存のライブラリを利用する
PyPiで公開されているライブラリで使えそうなものは以下のもの
docstring-inheritance
親クラスにメタクラスを導入し、クラスのdocstringやクラス内の関数のdocstringで、ParametersやReturnsなどを 引き継げる様にしてくれるライブラリ。
基本的な考え方としては上の__init_subclass__
と同じですが、必要なものを必要なだけ引き継いで
整形してくれるので便利です。
関数にParameters
の欄がある場合、引き継ぎ先のクラスではParameters
を書く際に
親クラスで定義されたものは再度書く必要がなく、新たなものだけを書けば良いようになっています。
また、引数の名前を変えたり削除した場合にはdocstringからも削除されるようになっています。
NumpyDocstringInheritanceMeta
、GoogleDocstringInheritanceMeta
という2つのメタクラスが用意されていて
NumPyとGoogleのdocstringスタイルにそれぞれ対応しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
こんな感じで、
- クラスのAttributesにParentで書かれたものが追加されている。
- methのParametersは、yが削除されているのでdocstringからも消えている。代わりにzが追加され、新たに書いたものが加えられている。もともとあったxも残っている。
- その他、書かれなかった部分に関してはParentのものがそのまま残っている。
という感じで、Parametersとかを追加する際に追加分だけ書けば元の文章とマージして表示してくれます。
クラスのdocstringも関数のdocstringについてもやってくれます。
ただ、使ってて1つ上手くいかないところがあって、クラスのdocstringでParameters
を使うと引き継ぎが出来ません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
だと
1 2 3 4 5 6 7 8 |
|
こんな感じで新たに書いたものも含めて全部消えてしまいます。
Parametersと言っているのは__init__
の引数にあたるものですが、
上で__init__
を書いても変わりません。
これはIssueにもなってました。
Inheritance in Constructor Does Not Work · Issue #1 · AntoineD/docstring-inheritance
できそうではあるけどちょっと色々と面倒なので時間をくれ、とのこと。
これに対して、とりあえずの対処法として、
https://github.com/AntoineD/docstring-inheritance/blob/bc5f94300e3661d5ead36730738e9621639a2f97/src/docstring_inheritance/processors/numpy.py#L48
にある、Parameters
を_ARGS_SECTION_ITEMS_NAMES
の欄から_SECTION_ITEM_NAMES
の欄の方に持っていけばとりあえずクラスのdocstringでもParameters
を引き継いでくれるようになります。
以下の様なメタクラスを新たに定義してやってNumpyModDocstringInheritanceMeta
をNumpyDocstringInheritanceMeta
の代わりにつかってやればOK。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
ただし、これをやると関数の方も含め、Parameters
のところで、変数が削除された場合にも
そのまま表示されるようになってしまいます。
上の_ARGS_SECTION_ITEMS_NAMES
のグループに入っていると
変数の削除とかのチェックが行われるようです。
Attributes
の方はもともと継承先のクラスで減ることは想定されてないので
もともと下にあるAttributes
は実際追加しかされません。
で、それがクラスのdocstringと関数のところで何が違うのか、というところまでちゃんと理解しきれてないので 一旦上の状態を考えてます。
関数の引数も基本的には追加することはあれど名前を変えたり削除する必要は ないので大丈夫かな、とは。
少し気になった点として、mypyを使って型チェックを行うと、
error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [misc]
みたいなエラーが継承先のクラスの定義の部分で出てしまいます。 実際にはエラーに出ているような複数のメタクラスが定義されていてconflictしている、といった状態ではないはずなのですが。
しかも二回目以降は
AssertionError: Should never get here in normal mode, got Var:apaf.docstring.NumpyModDocstringInheritanceMeta instead of TypeInfo
といったエラーが起きて解析自体ができなくなる状態。
この状態になったら一旦.mypy_cacheを消してやると治ります。
ただ、上のエラーがまた出てしまうので、classの定義の部分で # type: ignore[misc]
を入れて無視するようにするしかありません。
これに関してはおそらくmypy側の問題で下のIssueに関係がありそう。
mypy Type error for a class with an imported metaclass · Issue #9185 · python/mypy
docrep
こちらはデコレーターを使って関数生成時にその関数のdocstringから情報を抽出して 別の関数で再利用する、といったもの。
なのでクラスの継承とか関係なく使えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
@docstrings.get_sections
をかけることでdocstringからsectionごとの情報を取得します。
取得されるsectionはデフォルトではParameters
、Other Parameters
なのでここでは
Parameters
とReturns
に変更して取得。
取得すると、@docstrings.dedent
のデコレーターを次に与える時に
上の様に変数としてsectionごと与える事が出来ます。
この場合は引数の削除などは見てないので上のように削除されてもそのまま表示されます。
クラスのdocstringに使うには少し工夫が必要で、__doc__
を直接つくるような形で
取り込む必要があります。
また、クラス内の関数に関しても必要な部分全てにdedent
などのデコレーターを与えないといけません。
逆に必要な部分にだけ必要なパラメーターを与えるような調整はしやすい面はあるので、 使い方によっては使いやすいかもしれません。
Ref:
custom_inherit
こちらはクラスに対してメタクラスを与えるかデコレーターをつけて 継承先のクラスでParametersなどを引き継げるようにしたもの。
こちらは関数のdocstringだけでクラスのdocstringは引き継がれません。
その他
他にもdocstring周りではこんな議論があったらしい
inherit-docstring
追記: 2023/11/09
既存のライブラリがどうもフィットしない部分があったので自作してみました。
decoratorを使った方法でdocstring以外に副作用なく、 引き継ぎ先のクラスにつければ良いだけのもの。
追記ここまで
-
python - How can I use f-string with a variable, not with a string literal? - Stack Overflow ↩
-
この最後にある
I’m using Python 3.8 Simple string formatting worked for me. “"”This is {}””“.format(“StackOverflow”)
はなぜか2票入ってますが、3.8で試してもダメですし、3.10とかでもやはりダメでした。