ウェブサイト検索

Python コードのコメント、ドキュメント文字列、およびタイプ ヒント


プログラムのソースコードは人間が読めるものでなければなりません。正しく実行することは、目的の半分にすぎません。コードに適切にコメントを付けないと、将来のあなたも含めて、コードの背後にある理論的根拠や意図を理解するのが難しくなります。また、コードの保守も不可能になります。 Python では、コードに説明を追加してコードを読みやすくしたり、意図をより明確にしたりする方法が複数あります。以下では、コードを理解しやすくするためにコメント、docstring、およびタイプヒントを適切に使用する方法を見ていきます。このチュートリアルを完了すると、次のことがわかります。

  • Pythonでコメントを使用する適切な方法は何ですか
  • 場合によって文字列リテラルまたは docstring がコメントを置き換える方法
  • Python の型ヒントとは何ですか? コードをよりよく理解するのにどのように役立ちますか?

私の新しい本『Python for Machine Learning』 でプロジェクトを開始しましょう。これにはステップバイステップのチュートリアルとすべてのPython ソース コード ファイルが含まれています。例。

概要

このチュートリアルは 3 つの部分から構成されており、次のとおりです。

  • Python コードにコメントを追加する
  • docstring の使用
  • Python コードでの型ヒントの使用

Python コードへのコメントの追加

ほとんどすべてのプログラミング言語には、コメント用の専用の構文があります。コメントはコンパイラやインタプリタによって無視されるため、プログラミング フローやロジックには影響しません。ただし、コメントがあるとコードが読みやすくなります。

C++ などの言語では、先頭に二重スラッシュ (//) を付けた「インライン コメント」を追加したり、/**/ で囲まれたコメント ブロックを追加したりできます。 。ただし、Python には「インライン」バージョンのみがあり、先頭のハッシュ文字 (#) によって導入されます。

コードの各行を説明するコメントを書くのは非常に簡単ですが、通常は無駄です。人がソースコードを読むとき、コメントは簡単に注意を引くことが多いため、コメントを入れすぎると読むのが妨げられてしまいます。たとえば、次のようなものは不必要であり、気が散ります。

import datetime

timestamp = datetime.datetime.now()  # Get the current date and time
x = 0    # initialize x to zero

このようなコメントは、コードの動作を繰り返しているだけです。コードが不明瞭でない限り、これらのコメントはコードに価値を追加しません。以下の例は、「ppf」 (パーセント点関数) という名前が「CDF」 (累積分布関数) という用語ほどよく知られていない、限界的なケースである可能性があります。

import scipy.stats

z_alpha = scipy.stats.norm.ppf(0.975)  # Call the inverse CDF of standard normal

良いコメントでは、なぜ私たちが何かをしているのかを伝える必要があります。次の例を見てみましょう。

def adadelta(objective, derivative, bounds, n_iter, rho, ep=1e-3):
    # generate an initial point
    solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
    # lists to hold the average square gradients for each variable and
    # average parameter updates
    sq_grad_avg = [0.0 for _ in range(bounds.shape[0])]
    sq_para_avg = [0.0 for _ in range(bounds.shape[0])]
    # run the gradient descent
    for it in range(n_iter):
        gradient = derivative(solution[0], solution[1])
        # update the moving average of the squared partial derivatives
        for i in range(gradient.shape[0]):
            sg = gradient[i]**2.0
            sq_grad_avg[i] = (sq_grad_avg[i] * rho) + (sg * (1.0-rho))
        # build a solution one variable at a time
        new_solution = list()
        for i in range(solution.shape[0]):
            # calculate the step size for this variable
            alpha = (ep + sqrt(sq_para_avg[i])) / (ep + sqrt(sq_grad_avg[i]))
            # calculate the change and update the moving average of the squared change
            change = alpha * gradient[i]
            sq_para_avg[i] = (sq_para_avg[i] * rho) + (change**2.0 * (1.0-rho))
            # calculate the new position in this variable and store as new solution
            value = solution[i] - change
            new_solution.append(value)
        # evaluate candidate point
        solution = asarray(new_solution)
        solution_eval = objective(solution[0], solution[1])
        # report progress
        print('>%d f(%s) = %.5f' % (it, solution, solution_eval))
    return [solution, solution_eval]

上記の関数は AdaDelta アルゴリズムを実装しています。最初の行では、変数 solution に何かを代入するときに、「bounds[:,0] とbounds[:,1] の間のランダムな補間」のようなコメントは書きません。コードを繰り返します。この線の意図は「初期点を生成する」ことだと言えます。同様に、関数内の他のコメントについては、単に特定の回数繰り返すのではなく、for ループの 1 つを勾配降下アルゴリズムとしてマークします。

コメントを書いたりコードを変更したりするときに覚えておきたい重要な問題の 1 つは、コメントがコードを正確に説明しているかどうかを確認することです。矛盾してしまうと、読者を混乱させてしまいます。したがって、コードが初期解を明らかにランダム化している間に、上記の例の最初の行に「初期解を下限に設定する」というコメントを追加すべきではなかったでしょうか。逆も同様です。これが意図したものである場合は、コメントとコードを同時に更新する必要があります。

例外は「To-Do」コメントです。コードを改善する方法についてのアイデアはあるものの、まだ変更していない場合、コードに To-Do コメントを追加することがあります。これを使用して、不完全な実装をマークすることもできます。例えば、

# TODO replace Keras code below with Tensorflow
from keras.models import Sequential
from keras.layers import Conv2D

model = Sequential()
model.add(Conv2D(1, (3,3), strides=(2, 2), input_shape=(8, 8, 1)))
model.summary()
...

これは一般的な方法であり、キーワード TODO が見つかった場合、多くの IDE はコメント ブロックを異なる方法で強調表示します。ただし、これは一時的なものであるはずであり、問題追跡システムとして乱用すべきではありません。

要約すると、コードのコメント化に関する一般的な「ベスト プラクティス」を以下に示します。

  • コメントはコードを再記述するのではなく、それを説明する必要があります
  • コメントは混乱を引き起こすものではなく、混乱を解消するものでなければなりません
  • 理解するのが簡単ではないコードにはコメントを付けます。たとえば、構文の独特な使用法を述べたり、使用されているアルゴリズムの名前を述べたり、意図や前提を説明したりする
  • コメントは簡潔かつシンプルである必要があります
  • コメントでは一貫したスタイルと言語の使用を維持してください
  • 追加のコメントを必要としない、より適切に記述されたコードを常に好む

ドキュメント文字列の使用

C++ では、次のような大きなコメント ブロックを記述することがあります。

TcpSocketBase::~TcpSocketBase (void)
{
  NS_LOG_FUNCTION (this);
  m_node = nullptr;
  if (m_endPoint != nullptr)
    {
      NS_ASSERT (m_tcp != nullptr);
      /*
       * Upon Bind, an Ipv4Endpoint is allocated and set to m_endPoint, and
       * DestroyCallback is set to TcpSocketBase::Destroy. If we called
       * m_tcp->DeAllocate, it will destroy its Ipv4EndpointDemux::DeAllocate,
       * which in turn destroys my m_endPoint, and in turn invokes
       * TcpSocketBase::Destroy to nullify m_node, m_endPoint, and m_tcp.
       */
      NS_ASSERT (m_endPoint != nullptr);
      m_tcp->DeAllocate (m_endPoint);
      NS_ASSERT (m_endPoint == nullptr);
    }
  if (m_endPoint6 != nullptr)
    {
      NS_ASSERT (m_tcp != nullptr);
      NS_ASSERT (m_endPoint6 != nullptr);
      m_tcp->DeAllocate (m_endPoint6);
      NS_ASSERT (m_endPoint6 == nullptr);
    }
  m_tcp = 0;
  CancelAllTimers ();
}

ただし、Python には、区切り文字 /**/ に相当するものはありませんが、代わりに以下を使用して複数行のコメントを書くことができます。

async def main(indir):
    # Scan dirs for files and populate a list
    filepaths = []
    for path, dirs, files in os.walk(indir):
        for basename in files:
            filepath = os.path.join(path, basename)
            filepaths.append(filepath)

    """Create the "process pool" of 4 and run asyncio.
    The processes will execute the worker function
    concurrently with each file path as parameter
    """
    loop = asyncio.get_running_loop()
    with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
        futures = [loop.run_in_executor(executor, func, f) for f in filepaths]
        for fut in asyncio.as_completed(futures):
            try:
                filepath = await fut
                print(filepath)
            except Exception as exc:
                print("failed one job")

これが機能するのは、Python が三重引用符 (""") で区切られている場合、複数行にまたがる文字列リテラルの宣言をサポートしているためです。また、コード内の文字列リテラルは、宣言された単なる文字列であり、影響はありません。したがって、機能的にはコメントと何ら変わりません。

文字列リテラルを使用する理由の 1 つは、コードの大きなブロックをコメント アウトするためです。例えば、

from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
"""
X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
                           n_redundant=0, n_repeated=0, n_classes=2,
                           n_clusters_per_class=1,
                           weights=[0.01, 0.05, 0.94],
                           class_sep=0.8, random_state=0)
"""
import pickle
with open("dataset.pickle", "wb") as fp:
    X, y = pickle.load(fp)

clf = LogisticRegression(random_state=0).fit(X, y)
...

上記は、機械学習の問題を実験することで開発できるサンプル コードです。最初にデータセットをランダムに生成しましたが (上記の make_classification() の呼び出し)、別のデータセットに切り替えて、後で同じプロセスを繰り返したくなる場合があります (ピクル部分など)。その上)。コードのブロックを削除するのではなく、単にそれらの行にコメントを付けて、後でコードを保存できるようにすることもできます。最終的なコードとしては良好な状態ではありませんが、ソリューションを開発する際には便利です。

Python のコメントとしての文字列リテラルは、関数の最初の行にある場合、特別な目的を持ちます。この場合の文字列リテラルは、関数の「docstring」と呼ばれます。例えば、

def square(x):
    """Just to compute the square of a value
    
    Args:
        x (int or float): A numerical value

    Returns:
        int or float: The square of x
    """
    return x * x

関数の下の最初の行はリテラル文字列であり、コメントと同じ目的を果たしていることがわかります。これによりコードが読みやすくなりますが、同時にコードからコードを取得することもできます。

print("Function name:", square.__name__)
print("Docstring:", square.__doc__)
Function name: square
Docstring: Just to compute the square of a value
    
    Args:
        x (int or float): A numerical value

    Returns:
        int or float: The square of x

docstring は特殊なステータスであるため、適切なドキュメント文字列の記述方法についてはいくつかの規則があります。

C++ では、Doxygen を使用してコメントからコード ドキュメントを生成することがあります。同様に、Java コードには Javadoc があります。 Python で最もよく一致するのは、Sphinx または pdoc のツール「autodoc」です。どちらも docstring を解析してドキュメントを自動的に生成しようとします。

docstring を作成する標準的な方法はありませんが、一般に、docstring では関数 (またはクラスまたはモジュール) の目的と、引数と戻り値が説明されることが期待されます。一般的なスタイルの 1 つは、Google が提唱している上記のようなスタイルです。 NumPy とは異なるスタイルがあります。

def square(x):
    """Just to compupte the square of a value
    
    Parameters
    ----------
    x : int or float
        A numerical value

    Returns
    -------
    int or float
        The square of `x`
    """
    return x * x

autodoc などのツールは、これらの docstring を解析し、API ドキュメントを生成できます。しかし、それが目的ではない場合でも、関数の性質、関数の引数と戻り値のデータ型を説明する docstring があれば、確実にコードが読みやすくなります。 C++ や Java とは異なり、Python は変数や関数の引数が特定の型で宣言されないダックタイピング言語であるため、これは特に当てはまります。 docstring を使用してデータ型の前提を詳しく説明することで、ユーザーが関数をより簡単に理解したり使用したりできるようになります。

Python コードでのタイプ ヒントの使用

Python 3.5 以降、タイプヒント構文が許可されています。名前が示すように、その目的はタイプを示すことであり、それ以外は何もありません。したがって、たとえ Python を Java に近づけようとしても、変数に格納されるデータを制限することにはなりません。上記の例は、タイプ ヒントを使用して書き換えることができます。

def square(x: int) -> int:
    return x * x

関数では、引数の後に : type 構文を続けて、目的の型を指定できます。関数の戻り値は、コロンの前の -> type 構文によって識別されます。実際、型ヒントは変数に対しても宣言できます。

def square(x: int) -> int:
    value: int = x * x
    return value

型ヒントの利点は 2 つあります。使用されているデータ型を明示的に記述する必要がある場合に、これを使用してコメントを削除できます。また、静的アナライザーがコードをより深く理解し、コード内の潜在的な問題を特定できるようにすることもできます。

場合によっては型が複雑になる場合があるため、Python では構文をクリーンアップするために標準ライブラリに typing モジュールを提供しました。たとえば、Union[int,float] を使用すると、int 型または float 型、List[str] はすべての要素が文字列であるリストを意味し、Any は何かを意味します。次のように:

from typing import Any, Union, List

def square(x: Union[int, float]) -> Union[int, float]:
    return x * x

def append(x: List[Any], y: Any) -> None:
    x.append(y)

ただし、型ヒントはヒントのみであることを覚えておくことが重要です。コードにいかなる制限も課しません。したがって、以下は読者を混乱させますが、まったく問題ありません。

n: int = 3.5
n = "assign a string"

タイプヒントを使用すると、コードの可読性が向上する可能性があります。ただし、型ヒントの最も重要な利点は、mypy などの静的アナライザーがコードに潜在的なバグがあるかどうかを教えてくれることです。上記のコード行を mypy で処理すると、次のエラーが表示されます。

test.py:1: error: Incompatible types in assignment (expression has type "float", variable has type "int")
test.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")
Found 2 errors in 1 file (checked 1 source file)

静的アナライザーの使用については、別の投稿で説明します。

コメント、docstring、およびタイプヒントの使用を説明するために、固定幅ウィンドウで pandas DataFrame をサンプリングするジェネレーター関数を定義する例を以下に示します。これは、いくつかの連続した時間ステップを提供する必要がある LSTM ネットワークのトレーニングに役立ちます。以下の関数では、DataFrame 上のランダムな行から開始し、それに続くいくつかの行をクリップします。 1 つの完全なウィンドウを正常に取得できる限り、それをサンプルとして取り上げます。バッチを作成するのに十分なサンプルが収集されると、バッチが発送されます。

関数の引数に型ヒントを提供できると、より明確になることがわかります。これにより、たとえば、data が pandas DataFrame であることがわかります。ただし、docstring に日時インデックスが含まれることが期待されていることをさらに説明します。コメントを使用して、入力データから行のウィンドウを抽出する方法と、内部 while ループ内の「if」ブロックの意図についてアルゴリズムを説明します。このようにして、コードは非常に理解しやすくなり、他の用途での保守や変更も非常に簡単になります。

from typing import List, Tuple, Generator
import pandas as pd
import numpy as np

TrainingSampleGenerator = Generator[Tuple[np.ndarray,np.ndarray], None, None]

def lstm_gen(data: pd.DataFrame,
             timesteps: int,
             batch_size: int) -> TrainingSampleGenerator:
    """Generator to produce random samples for LSTM training

    Args:
        data: DataFrame of data with datetime index in chronological order,
              samples are drawn from this
        timesteps: Number of time steps for each sample, data will be
                   produced from a window of such length
        batch_size: Number of samples in each batch

    Yields:
        ndarray, ndarray: The (X,Y) training samples drawn on a random window
        from the input data
    """
    input_columns = [c for c in data.columns if c != "target"]
    batch: List[Tuple[pd.DataFrame, pd.Series]] = []
    while True:
        # pick one start time and security
        while True:
            # Start from a random point from the data and clip a window
            row = data["target"].sample()
            starttime = row.index[0]
            window: pd.DataFrame = data[starttime:].iloc[:timesteps]
            # If we are at the end of the DataFrame, we can't get a full
            # window and we must start over
            if len(window) == timesteps:
                break
        # Extract the input and output
        y = window["target"]
        X = window[input_columns]
        batch.append((X, y))
        # If accumulated enough for one batch, dispatch
        if len(batch) == batch_size:
            X, y = zip(*batch)
            yield np.array(X).astype("float32"), np.array(y).astype("float32")
            batch = []

さらに読む

さらに詳しく知りたい場合は、このセクションでこのトピックに関するさらなるリソースを提供します。

記事

  • コード コメントを記述するためのベスト プラクティス、https://stackoverflow.blog/2021/12/23/best-practices-for-writing-code-comments/
  • PEP483、型ヒントの理論、https://www.python.org/dev/peps/pep-0483/
  • Google Python スタイル ガイド、https://google.github.io/styleguide/pyguide.html

ソフトウェア

  • Sphinx ドキュメント、https://www.sphinx-doc.org/en/master/index.html
  • Sphinx の Napoleon モジュール、https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html

    • Google スタイルの docstring の例: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
    • NumPy スタイルの docstring の例: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html
  • pdoc、https://pdoc.dev/
  • タイピング モジュール、https://docs.python.org/3/library/typing.html

まとめ

このチュートリアルでは、Python でコメント、ドキュメントストリング、およびタイプヒントを使用する方法を説明しました。具体的には、次のことがわかります。

  • 有益で良いコメントを書く方法
  • docstring を使用して関数を説明する際の規則
  • 型ヒントを使用して Python のダックタイピングの可読性の弱点に対処する方法

関連記事