rcmdnk's blog

20231109_inheritdocstring_200_200

Pythonでクラスを継承する際にdocstringにあるパラメーターの説明などに関して、 親クラスのものを継承しつつ必要な部分を変更したり追加したりしたい、ということを 実現するライブラリを作りました。

Pythonのdocstringをクラス間で継承する

Pythonでクラスや関数を定義した最初の行からコメントを書くと docstring(PEP 8, PEP 257) という形で処理され、helpで見る際にその説明が表示されるようになります。

クラス継承した際にはクラスの属性値や関数の変数などは同じものがあるので それらは親クラスの説明をそのまま使いたいことが多いです。

ただ、通常のPythonのクラス継承ではdocstringは継承されないので 子クラスでは説明をすべて各必要があります。

特にdataclassを使うと__init__に渡せるパラメーターとして 子クラスで変数を追加したい場合は追加分だけ書けば済むので、 その際にdocstringだけ親の方で定義されたものも全部書くのはちょっと微妙です。

多くの重複を産んでしまい、ちょっとした説明変更でも全部変更しようにも大変なことになるので できればdocstringもいい感じに継承して欲しいところ。

というわけで以前ちょっと色々調べてみました。

いくつか既存のライブラリもあるわけですが、結局完全に自分のやりたいことを満たせるものがなかったので 自分で作ってみることに。

既存のライブラリでうまくいかないところ

親のクラスにメタクラスとして与えることでそれを継承したクラスではdocstringを自動的に継承できるようにしたもの。

上で調べてから 上のポストにも書いたようにこれにちょっと手を加えて これを主に使ってました。

ただ手を入れたとこに加えて色々とうまく行かない部分もあり、 更に手を加えようとすると自分用に一から作った方が良いのでは、と。

また、メタクラスだと、中身を全部理解してれば良いのですがライブラリとして使うと 何かしら副作用が出たときに分かりづらいのが辛いところ。

なのでシンプルにデコレーターで使いたいときに与えるだけ、の形の方が管理はしやすいです。

他のものはデコレーターのものでも使い方がちょっと面倒だったり 関数のみでクラスのものは引き継げなかったり。

作ったもの: inherit-docstring

pipを使って

1
$ pip3 install inherit-docstring

とかでインストールできます。

対応しているのは Numpy Style のdocstringになります。

使い方は継承した子クラス側に@inherit-docstringというデコレーターをつけるだけです。

inherit-docstringで対応しているセクション

Numpy StyleのdocstringではParametersなどセクションに区切ってドキュメントが書かれています。

基本的にセクションは-で下線を引かれたセクション名で始まります。

例外として、ヘッダー部分(最初の書き出し部分、セクション名なし)と、廃止をお知らせする.. deprecated:: x.y.zで始まるセクションがあります。

また、Parametersなどは値の名前とその型、及びその説明、といった形式になっています。 inherit-docstringでは以下のセクションに関してはパラメーターセクションとして 値、型、説明に分ける解析を行います。

  • Attributes
  • Parameters
  • Returns
  • Yields
  • Receives
  • Raises
  • Warns
  • Warnings

これらのセクションでは子クラス側同じセクションを定義した場合、 親クラスで定義したものをすべて引き継いだ上で 新たな値があればそれを加え、 同じ値がある場合は説明を子クラスで定義したものに置き換えます。[

パラメーターセクションではないセクションに関しては 親クラスにのみあるものはそのまま引き継ぎ、 子クラスで追加したものはそのまま追加し、 両方に存在する場合は小クラスのものにセクション全体を置き換えます。

例として以下のような親クラス(Parent)とそれを継承した子クラス(Child)がある場合に使ってみます。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from inherit_docstring import inherit_docstring

class Parent:
    """Parent class.

    This is an explanation.

    Attributes
    ----------
    name: str
        The name of
        the parent.
    age:
        The age. w/o type.

    Notes
    -----
    This is parent's note.
    """

    name: str = 'parent'
    age: int = 40

    def func1(self, param1: int, param2: int) -> int:
        """Parent's func1.

        Parameters
        ----------
        param1: int
            First input.
        param2: int
            Second input.

        Returns
        -------
        ret: int
            param1 + param2
        """

        return param1 + param2

    def func2(self) -> None:
        """Parent's func2.

        Returns
        -------
        ret: str
            something
        """

        return 'Something'

@inherit_docstring
class Child(Parent):
    """Child class.

    Attributes
    ----------
    sex: str
        Additional attributes.
        girl or boy.
    """

    sex: str = "boy"

    def func1(self, param1: int, param2: int) -> int:
        """Child's func1.

        Returns
        -------
        ret: int
            param1 - param2
        """

        return param1 - param2

これの子クラスの方のをhelp(Child)で見てみると 以下のような出力になります。

class Child(Parent)
 |  Child class.
 |
 |  Attributes
 |  ----------
 |  name: str
 |      The name of
 |      the parent.
 |  age:
 |      The age. w/o type.
 |  sex: str
 |      Additional attributes.
 |      girl or boy.
 |
 |  Notes
 |  -----
 |  This is parent's note.
 |
 |  Method resolution order:
 |      Child
 |      Parent
 |      builtins.object
 |
 |  Methods defined here:
 |
 |  func1(self, param1: int, param2: int) -> int
 |      Child's func1.
 |
 |      Parameters
 |      ----------
 |      param1: int
 |          First input.
 |      param2: int
 |          Second input.
 |
 |      Returns
 |      -------
 |      ret: int
 |          param1 - param2
 ...

やってみて

一応これでhelpに対するdocstringという意味ではいい感じに出来たと思ってますが、 コードを書くに当たってはやっぱり直接書いて無いと分かりづらいのではないかとも思ったり。

エディタの側で親のdocstringをぱっと見れるようにしておいたりする拡張作ればという話もあるかもしれませんが あまりにピンポイントなので微妙。

そう入っても重複は嫌なので、

dask-jobqueue/dask_jobqueue/core.py at main · dask/dask-jobqueue

ここにあるような感じで変数定義して突っ込むようにしてあげればその変数を追えば良いのでは 書く際にも追いやすくはなるかも。

こういったdocstring用の説明分をある程度まとめて作っておけば、 例えば親子関係ではない別のクラスでも全く同じ名前の変数で同じ説明の変数を使いたいときにも 共通して使えて、メンテナンスする際にも同時に変更できるので便利かと思ったり。

クラスに対するdocstringとクラス内の関数に対するものでインデントの量などをちょっと注意しないといけないですが、 その辺ちょっとしたdecoratorを作っていい感じに変数をdocstringに与えられるような感じにして やる方がもっとわかりやすくできるのではないかな、と思ったりしてます。

その場合は変数をimportしたり書く量は増えますが、わかりやすさを考えるとうーん、といったところ。

Sponsored Links
Sponsored Links

« シェルスクリプトでgetoptsで解析する引数をポジショナルな引数と混合して使えるようにする Pythonでキーワード引数の展開(**kw)に対するアノテーション »

}