LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その17 モジュールのインポートと docstring

Last updated at Posted at 2023-10-08

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤の全てのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

前回までのおさらい

前回までの記事で、下記の処理を行う関数を定義しました。

  • 初期化されたゲーム盤を返す initialize_board
  • 指定したマスにマークを配置する place_mark

モジュールの作成とインポート

本記事では、毎回 新しい marubatsu.ipynb という名前のファイルを作成し、そこに各回の記事で紹介したプログラムを記述してプログラムを実行しています。

これまでは、initialize_board などの関数を 修正しながら説明 していたので、JupyterLab のセルに 毎回 initialize_board などの 関数の定義を記述 していましたが、完成した initialize_board などの関数の定義を 毎回記述 するのは 大変 です。

そのような場合は、別のファイル に関数の定義などを記述し、marubatsu.ipynb の中で、そのファイルに定義された 関数を利用 するという方法があります。

ファイル に記述された python のプログラムのことを モジュール と呼び、自分のプログラムで モジュール に記述されたプログラムを 利用する ことをモジュールを インポート すると呼びます。

モジュールの作成

モジュールの作成は Python のプログラムを ファイルに記述して保存 するだけで簡単に行うことが出来ます。その際に、拡張子が .py のファイルに Python のプログラムを保存します。

ファイルの名前がモジュールの名前になる ので、保存の際にはモジュールに記述したプログラムの 内容に合わせた名前 を付けるのが一般的です。

拡張子が .ipynb の JupyteLab のファイルをモジュールとして 利用することはできません。その理由は、JupyterLab のファイルには、プログラムの実行結果などの、Python のプログラム以外のデータが混じっている からです。

VSCode では、以下の手順でモジュールを作成することが出来ます。

  1. 「ファイル」メニュー→「新しいファイル」→「Python ファイル」を選択する
  2. 新しいファイルに Python のプログラムを記述する
  3. 「ファイル」メニュー→「保存」を選択(または Ctrl + S)して、ファイルの保存パネルを開く
  4. ファイルの名前を入力する
  5. ファイルを保存するフォルダを選択し、保存ボタンをクリックする

実際に、上記の手順で新しいファイルを作成し、前回の記事で定義した initialize_boardplace_mark が定義された下記のプログラムを入力して、marubatsu.py という名前 で marubatsu.ipynb と 同じフォルダ に保存して下さい。

python marubatsu.py
def initialize_board():
    return [[" "] * 3 for x in range(3)]

def place_mark(board, x, y, mark):
    if board[x][y] == " ":
        board[x][y] = mark
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")

モジュールのインポート

同じフォルダに保存 されたモジュールは、下記のように記述することでインポートすることが出来ます。

import モジュール名

インポートしたモジュールの中に記述された 変数や関数 などの 名前 は、下記のように記述することで利用することが出来ます。モジュール名の直後には 半角.(ピリオド) を記述します。

モジュール名.名前

下記のプログラムは、1 行目で先ほどファイルに保存した marubatsu というモジュールを インポート し、3 行目で、そのモジュールの 中で定義 された intialize_board を呼び出して います。実行結果から、正しくプログラムが動作することを確認することが出来ます。

import marubatsu

board = marubatsu.initialize_board()
print(board)

実行結果

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

モジュールをインポートすることによって、インポートした モジュールのグローバル名前空間 に登録された 名前を利用 することが出来るようになります。また、その際に 名前の前 に「モジュール名.」を記述するので、異なるモジュール のグローバル名前空間で 同じ名前 が使われていたとしても、それらの 名前を区別して利用 することが出来ます。

異なるフォルダに保存されたモジュールのインポート

異なるフォルダ に保存されたモジュールのインポートには、2 通りの方法があります。

モジュールのファイルが、フォルダの中に保存 されている場合は、以下のように記述することでモジュールをインポートすることが出来ます。

import フォルダ名.モジュールの名前

例えば、marubatsu.py を、marubatsu.ipynb が保存 されている フォルダ内lib という フォルダの中に保存 した場合は、下記のプログラムのように記述することでその marubatsu モジュールをインポートすることが出来ます。

モジュールのことを ライブラリ(library) と呼ぶことがあります。そのため、モジュール のファイルを 保存するフォルダの名前 に library を省略した lib という名前 を付けることがあります。

import lib.marubatsu

board = lib.marubatsu.initialize_board()
print(board)

実行結果

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

モジュールが保存されているフォルダが、より深い場所 に存在する場合は、同様の方法で、フォルダ名を半角の .(ピリオド)でつなげて 表記します。

もう一つの方法は、sys.path を使ってモジュールのファイルが保存されたフォルダのパス(フォルダの住所の事です)を指定するという方法です。本記事ではこの方法は使いませんので、ここではその方法については説明しません。興味がある方は調べてみて下さい。

標準ライブラリのインポート

Python には あらかじめ いくつかのモジュールが作成されており、それらのモジュールの事を 標準ライブラリ と呼びます。標準ライブラリは、自分のプログラムを記述する Python のファイルがどこに保存されている場合でも、単に import 標準ライブラリの名前 と記述してインポートすることが出来ます。

標準ライブラリの詳細については下記のリンク先を参照して下さい。

自分でインストールしたモジュールのインポート

他人が作成したモジュール の具体的なインストールの方法については今後の記事で説明しますが、Anaconda Navigator や pip 等のコマンドによって インストールしたモジュール をインポートする場合も、標準ライブラリと同様に、単に import モジュールの名前 と記述してインポートすることが出来ます。

モジュールのインポートの仕組み

モジュールをインポートすると、そのモジュールの グローバル名前空間が作成 されます。Python では 名前空間もデータの一種 なので、他のデータと同様に 名前空間のデータオブジェクトによって管理 されます。

import モジュール名 によってインポートされた モジュール名 は、変数名や関数名全く同じ仕組みメインモジュールのグローバル名前空間が管理 します。

具体的には、モジュールをインポートすると以下のような手順で処理が行われます。

  1. インポートしたモジュールのグローバル名前空間 のデータを 管理する新しいオブジェクトが作成 される

  2. モジュールの中 に記述された Python のプログラムがすべて実行 され、そのモジュールのグローバル名前空間 に、モジュールに記述された グローバル変数やグローバル関数 などの 名前が登録 される

  3. メインモジュールのグローバル名前空間インポートしたモジュールの名前登録 され、手順 1 で作成したオブジェクトに 対応づけられる

下図は、import marubatsu を実行した場合のオブジェクトの様子を図示したものです。

モジュール名は変数名同じ性質 を持つので、変数名や関数名の場合と同様に、モジュール名に対して 値を代入 することが出来ます。

下記のプログラムでは、3 行目でインポートした モジュール名 である marubatsuprint で表示 しています。モジュールの名前を print で表示すると、下記の実行結果のように、「<module `モジュール名` from `モジュールが保存されたファイルのパス`>」が表示されます。

4 行目で、marubatsu1 を代入すると、marubatsu の値が 1上書きされる ので、5 行目のように print(marubatsu) を実行すると 1 が表示 され、4 行目以降 で、marubatsuモジュールの名前として 利用することは できなくなります

import marubatsu

print(marubatsu)
marubatsu = 1
print(marubatsu)

実行結果

<module 'marubatsu' from 'c:\\Users\\ys\\ai\\marubatsu\\017\\marubatsu.py'>
1

上記のプログラムは、モジュール名に値を代入できることを 示すため に記述したプログラムです。実際には上記のように モジュール名に値を代入する ようなプログラムを 記述する ことは、ほとんどありません

このように、モジュール名に関する処理は、変数と同じ仕組み によって行われます。これまでに説明していないような、全く新しいことが行われているわけではありません。

以下は import モジュール名 に関するまとめです。

  • 同じフォルダに保存 された モジュール名.py というファイルに保存されたモジュールを インポート して自分のプログラムで 利用 できるようになる

  • インポートしたモジュールの プログラムが実行 され、その モジュールのグローバル名前空間 に、そのモジュールの グローバル変数名やグローバル関数名登録 される

  • 名前空間のデータ は、他のデータと同様に オブジェクトによって管理 される

  • メインモジュールのグローバル名前空間 に、モジュール名登録 され、インポートしたモジュールのグローバル名前空間 を管理するオブジェクトに 対応づけられる

  • インポートしたモジュールの グローバル名前空間に登録された名前 は、モジュール名.名前 と記述することで自分のプログラムで 利用 することが出来る

複数回のモジュールのインポート

下記のプログラムのように、同じモジュールを複数回インポート するようなプログラムを記述した場合に行われる処理について説明します。

import marubatsu
import marubatsu # 既にインポートされたモジュールをもう一度インポートする

このようなプログラムを記述した場合は、2 回目以降 のモジュールのインポートでは 何の処理も行われません。従って、2 回目以降 のモジュールのインポートによって、そのモジュールの プログラムが実行 されることは ありません

具体例を挙げます。下記の、1 という表示を行うプログラムを print_1.py という名前のファイルに保存することで、print_1 という名前のモジュールを作成 します。モジュールは Python のプログラムであれば その内容は何でもかまわない ので、下記のように、変数や関数が存在しない ファイルも モジュールです

python print_1.py
print(1)

次に、下記のプログラムのように、この print_1 というモジュールを 複数回インポート するプログラムを記述して実行します。このプログラムでは、モジュールを インポートした際 にそのモジュールのプログラムが 実行されているかどうかを確認 できるように、モジュールを インポートする前 の 1、3 行目で メッセージを表示 しています。

print("これから print_1 をインポートします")
import print_1
print("もう一度 print_1 をインポートします")
import print_1

実行結果

これから print_1 をインポートします
1
もう一度 print_1 をインポートします

実行結果からわかるように、一度目の import print_y を実行すると、print_y.py のプログラムが 実行されて 1 が表示されます が、二度目import print_y を実行しても print_y.py のプログラムは 実行されない のでもう一度 1表示されることはありません

同じモジュールを 複数回インポート しても、インポートされたモジュールの プログラムが実行される のは、最初に モジュールを インポートした時だけ である。

インポート後のモジュールの修正

上記の性質から、JupyterLab で モジュールを インポートした後 で、モジュールの ファイルの内容を修正 して保存した場合に、もう一度 そのモジュールを インポートしても修正後のモジュールはインポートされません

JupyterLab で 修正後のモジュールをインポート する方法には、以下の 2 通りの方法があります。

JupyterLab を再起動する

JupyterLab の上部にある「再起動」ボタンをクリックすることで、プログラムが強制的に終了 されるので、その後でモジュールをインポートするプログラムを実行すると、修正後のモジュールがインポート されます。

importlib.reload を使う

標準ライブラリには、importlib という モジュールのインポートに関する関数が定義 されたモジュールがあります。importlib 内で定義された reload という関数の 実引数にモジュールの名前 を記述して呼び出すことで、そのモジュールを 再読み込み(reload) することが出来ます。

liportlib の詳細については、下記のリンク先を参照して下さい。

importlib を利用するためには、下記のプログラムの 1 行目のように import importlib を記述します。下記のプログラムは、5 行目で importlib.reload を使って print_1 のモジュールを 再読み込み しているので、5 行目を実行した際に 1 が表示 されます。

なお、下記のプログラムは JupyterLab を再起動 してから実行して下さい。再起動せずに実行すると、下記のプログラムを実行する前に先ほど print_1 をインポートしてしまったので、3 行目で import print_1 を実行しても 1 は表示されません。

import importlib
print("これから print_1 をインポートします")
import print_1
print("print_1 を再読み込みします")
importlib.reload(print_1)

実行結果

これから print_1 をインポートします
1
print_1 を再読み込みします
1
<module 'print_1' from 'c:\\Users\\ys\\ai\\marubatsu\\017\\print_1.py'>

これまでのプログラムでは、print を使って 表示 を行っていましたが、JupyterLabセルの最後の文データを表す 場合、そのセルを実行すると 最後の文のデータが表示 されます。上記の実行結果の最後に「<module 'print_1' from 'c:\\Users\\ys\\ai\\marubatsu\\017\\print_1.py'>」が表示されているのは、importlib.reload返り値 が、再読み込みしたモジュールを表すデータ であるからです。

例えば、下記のプログラムは、最後の行1 が代入された a が記述 されているので、print を使わなくても 1 が表示 されます。

a = 1
a

実行結果

1

なお、データの種類 によっては、 print で表示 する場合と、最後の行にデータを記述して表示 する場合で 異なる表示 が行われる場合があります。下記のプログラムは 関数名print と、セルの最後の行に記述して表示するプログラムで、実行結果のように、異なる表示が行われます。

本記事では 特別な理由がない限り、print を使って表示を行う ことにします。

def print_1():
    print(1)
    
print(print_1)
print_1

実行結果

<function print_1 at 0x0000024E15002C00>
<function __main__.print_1()>

ちなみに、print で関数名を表示した場合に最後に表示されるのは、関数の定義を管理する オブジェクトの id です。関数名を最後の行に記述した場合に function の後に表示されるのは、「その関数が定義されたモジュールの名前.関数の名前(仮引数)」です。メインモジュールの名前は自動的に __main__ という名前が付けられるので上記のように表示されます。

モジュールのインポートに関する補足

上記がモジュールのインポートに関する 基本的な説明 ですが、モジュールのインポートには 他の良く使われる記述方法 があるので、それらについて説明します。

複数のモジュールをまとめてインポートする方法

import の後に、半角の , で区切って複数のモジュール名 を記述することで、複数のモジュールをまとめてインポート することが出来ます。下記のプログラムはその具体例です。

import marubatsu, print_1

別名によるモジュールのインポート

以下のように記述することで、モジュールを 別の名前でインポート することが出来ます。

import モジュール名 as モジュールの別名

別名によるモジュールのインポートは主に以下のような場合に使用します。

  • モジュールの名前が marubatsu のように 長い場合 で、mb のように 短い名前 でモジュールをインポートすることで、モジュールに関するプログラムを 短く記述 したい場合
  • 自分のプログラムグローバル変数やグローバル関数 に、インポートするモジュールの名前と 同じ名前を使いたい 場合

上記の 1 つ目の場合の具体例を挙げます。下記のプログラムは、marubatsu というモジュールを、maru と batsu の頭文字 をとって mb という名前 でインポートしています。そのため、2 行目では、mb.initialize_board() のように、モジュールの関数呼び出しを 短く記述 することが出来ます。

import marubatsu as mb
board = mb.initialize_board()
print(board)

実行結果

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

他の具体例として numpy という非常に良く使われるモジュールをインポートする際には、import numpy as np のように np という名前 でインポートするのが 一般的 です。

上記の 2 つ目の場合について補足します。先ほど説明したように、import モジュール名 でモジュールをインポートした場合、インポートした モジュールの名前 が、グローバル名前空間に登録 されます。従って、インポートしたモジュールの名前と 同じ名前グローバル変数グローバル関数使いたい場合 は、モジュールを別の名前でインポートする必要があります。

必要な名前だけをインポートする方法

モジュールの中の、必要な変数や関数だけを利用 したい場合は、以下のように記述します。この場合も 半角の , で区切って 記述することで、複数の名前をインポート することが出来ます。

from モジュール名 import モジュール内の名前

この方法は、以下のような性質を持ちます。

  • インポートした モジュール内の名前 は、プログラム内で先頭に モジュール名.つけずにそのまま 利用できる
  • インポートした モジュール内の名前 は、メインモジュールの名前空間に登録されるので、グローバル変数 または、グローバル関数 になる
  • 上記の方法で インポートした以外 のモジュール内の 名前を使用 することは できない

この方法は、他のモジュールで定義された関数を、モジュールの名前を先頭に付けず に、メインモジュールで定義された関数 のように短く記述できる ので、良く使われます。

下記のプログラムは、marubatsu というモジュールの中の、initialize_board という名前 のみ をインポートしています。この場合は、2 行目のように、単に initialize_board と記述するだけで、この関数を利用することが出来ます。

marubatsu.py の中には他にも place_mark が定義 されていますが、1 行目で place_mark がインポート されて いない ため、4 行目のように、place_mark を記述すると、place_mark という名前が定義されていないことを表す NameError が発生 します。

from marubatsu import initialize_board
board = initialize_board()
print(board)
place_mark(board, 0, 1, "")

実行結果

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\017\marubatsu.ipynb セル 11 line 4
      2 board = initialize_board()
      3 print(board)
----> 4 place_mark(board, 0, 1, "〇")

NameError: name 'place_mark' is not defined

この方法は、下記のプログラムの 2 行目のように、marubatsu をインポートしてから、グローバル変数 initialize_boardmarubatsu のモジュールのグローバル関数 initialize_board を代入 する ような 処理が行わていると考えると わかりやすい でしょう。

import marubatsu
initialize_board = marubatsu.initialize_board

ただし、上記のプログラムと from marubatsu import initialize_board同じ処理 を行っている わけではありません。上記のプログラムの場合は、1 行目で marubatsu というグローバル変数marubatsu というモジュールが代入 されるため、下記のプログラムの 5 行目のように marubatsu.place_mark を記述して place_mark を利用 することが出来ます。

import marubatsu
initialize_board = marubatsu.initialize_board
board = initialize_board()
print(board)
marubatsu.place_mark(board, 0, 1, "")

実行結果

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

一方、from marubatsu import initialize_board の場合は、グローバル名前空間marubatsu という名前は 登録されていない ので、下記のプログラムのように marubatsu.place_mark を実行すると NameError が発生 します。

なお、下記のプログラムは JupyterLab を再起動 してから実行して下さい。

from marubatsu import initialize_board
board = initialize_board()
print(board)
marubatsu.place_mark(board, 0, 1, "")

実行結果

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\017\marubatsu.ipynb セル 14 line 4
      2 board = initialize_board()
      3 print(board)
----> 4 marubatsu.place_mark(board, 0, 1, "〇")

NameError: name 'marubatsu' is not defined

github での本記事のモジュールの保存について

本記事では、以降は marubatsu.py というファイル に〇×ゲームに関する 関数の定義など保存し必要な関数をインポート してプログラムを記述することにします。

marubatsu.py は 今後の記事内容を修正したり増やしたり しながら 更新 します。そこで、github の marubatsu.py は、前回の記事までの内容 とし、その記事で 新しく marubatsu.py に追加した内容 は、marubatsu_new.py という ファイルに保存 することにします。従って、marubatsu_new.py の内容は、その次の回の記事の marubatsu.py と同じになります。

github の marubatsu.py は各回の記事で更新された結果、毎回異なる内容 になるので、今後の記事で marubatsu.ipynb をコピーせずに 自分で入力して実行する 場合は、github の 同じフォルダ内の marubatsu.py を毎回コピー し直してから実行してください。

なお、記事の中で marubatsu.py に対して 更新する内容が存在しない 場合は、marubatsu_new.py というファイルは github には 保存しません

関数の引数と返り値のデータ型のヒント(型アノテーション)の表記

関数呼び出しを記述する際 に、実引数どのようなデータ型 のデータを記述すればよいかが わかる と、関数呼び出しを 正しく記述しやすく なり、プログラムの バグを減らす ことにつながります。また、同様の理由で 関数の返り値のデータ型 がわかると便利です。

Python では、関数の定義 を行う際に、仮引数のデータ型 と、返り値のデータ型ヒント を以下のような方法で記述することが出来ます。

このデータ型のヒントの表記の事を、型アノテーション(annotation) と呼びます

def 関数名(仮引数: 仮引数のデータ型) -> 返り値のデータ型:
    関数のブロックのプログラム

型アノテーションの詳細については、下記のリンク先を参照して下さい。

具体例を説明する前に、Python の データ型の表記 について説明する必要があるので説明します。

Python のデータ型の英語表記

本記事ではこれまで、数値型や文字列型 などは、日本語でデータ型を表記 してきましたが、関数の定義で 型アノテーション記述する場合 は、英語 でデータ型を 表記する必要 があります。

また、本記事ではこれまで 数値 を表すデータを 数値型と表記 してきましたが、数値型には 整数 を表す int浮動小数点数(小数点以下の数値を含むような数値の事)を表す float複素数 を表す complex の 3 種類があり、プログラム内では それらの英語表記 を記述する 必要 があります。

下記は、Python の代表的なデータ型の、日本語表記と英語表記の 対応表 です。データが存在しないことを表す None 型のみ、頭文字が大文字である点に注意して下さい。なお、本記事でまだ説明していないデータ型については、必要になった時点で説明します。

日本語 英語
整数型 int
浮動小数点数型 float
複素数型 complex
文字列型 str
論理型 bool
リスト型 list
タプル型 tuple
辞書型 dict
集合型 set
None 型 None

なお、今後も 記事の中 では、整数型と浮動小数点数型を 区別する必要がない 場合は、引き続き 数値型 のように表記します。また、文字列型のデータも str ではなく、日本語で表記 します。

list の場合の型アノテーションの記述方法

Python の 3.8 以前のバージョン で下記のような記述を行うと エラーになります

バージョン 3.7 と 3.8 では、from __future__ import annotations を記述することで、下記のプログラムを実行することが出来るようになります。

それ以前のバージョンでは別の方法で記述する必要がありますが、Python を 3.9 以降にバージョンアップしたほうが早いと思いますので、その方法については省略します。

仮引数や返り値の型アノテーションが list の場合、その list の 要素のデータ型 のヒントがあると便利です。そのような型アノテーションは、list[データ型] のように表記します。例えば、[1, 2, 3] のような、整数型の要素 を持つ list の場合は list[int] のように表記します。

ゲーム盤のデータを表す 2 次元配列の list は、その 要素は list を、その 要素の要素 はマークを表す 文字列型 のデータ持つので、その場合は list[list[str]] のように表記します。

記述例

下記は、initialize_boardplace_mark に対して型アノテーションを記述したプログラムです。

def initialize_board() -> list[list[str]]:
    return [[" "] * 3 for x in range(3)]

def place_mark(board: list[list[str]], x: int, y: int , mark: str):
    if board[x][y] == " ":
        board[x][y] = mark
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")

initialize_board()文字列型の要素を持つ 2 次元配列の list返り値 として返す関数なので、上記のように -> list[list[str]]返り値型アノテーション として記述されています。place_mark の仮引数 board についても同様です。

place_mark の仮引数 xyマスの座標を表すデータ なので、整数型を表す int が型アノテーションとして表記されています。

place_mark の仮引数 markマークを表すデータ なので、文字列型を表す str が型アノテーションとして表記されています。

place_mark のように、返り値を返さない関数 の場合は、上記のように、関数の返り値の型アノテーションは一般的に 省略 します。何らかの理由で、返り値を省略しない場合は、-> None を記述します。

型アノテーションを記述するメリット

関数の定義で型アノテーションを記述することで、以下のようなメリットを得ることが出来ます。

関数呼び出しを記述する際に型アノテーションが表示される

VSCode では、関数呼び出しを記述する際にその付近 に仮引数のデータ型や、関数の返り値のデータ型の 型アノテーション表示 されます。

下図は、VSCode で place_mark の関数呼び出しを 記述し始めた場合 の図です。図のように、これから入力 しようとしている 実引数に対応する 仮引数 board型アノテーション が、すぐ上に 青い文字で表示 されています。

記述済の関数名にヒントを表示できる

VSCode では、記述済の関数名の上にマウスカーソルを移動 すると、その付近に関数に関する ヒントが表示 されます。

下図は、VSCode で、place_mark の上にマウスを移動した場合の図です。

型アノテーションに関する注意点

型アノテーションのことを、データ型の ヒント のように記述してきた理由は、データ型の記述はあくまで ヒントに過ぎず強制力を持たない からです。型アノテーション異なる データ型の 実引数 を記述して関数を呼び出したり、型アノテーション異なる データ型の 返り値 を return 文で返しても、エラーは発生しません。この 型アノテーションの情報 は、あくまで 目安にすぎない ことを忘れないで下さい。

具体例を挙げます。下記のプログラムで定義された add は、仮引数 x の型アノテーションには整数型の int、y の型アノテーションには浮動小数点数型の float、返り値の型アノテーションには文字列型の str が記述されています。

しかし、4 行目のように、型アノテーション異なる データ型の 実引数 を記述し、型アノテーション異なる データ型の 返り値 が返されても エラーにはなりません

def add(x: int, y: float) ->str:
    return x + y

print(add(0.5, 1))

実行結果

1.5

型アノテーションは目安に過ぎませんが、型アノテーション従った データ型を 記述 することは、プログラムのバグを減らす ことにつながります。関数を定義 する場合は、なるべく型アノテーションを記述 し、関数を利用 する場合は特別な理由がない限り、型アノテーション従った データ型を 記述するべき です。

docstring

上記で紹介した方法では、関数の仮引数や返り値の型アノテーション しか 表示されません。関数呼び出しを記述する際に、関数の「仮引数の意味」、「関数が行う処理」、「関数の返り値の意味」などの、より詳細なヒント が表示されれば、関数呼び出しのプログラムをより 記述しやすくなります

そのようなヒントは、関数の定義の def 関数名(仮引数):直後の行 に、関数の説明を表す docstring と呼ばれる 文字列を記述 することで表示することが出来るようになります。

トリプルクオートによる文字列のリテラル

docstring では 文字列 を使って、複数行にまたがって 関数の説明を 記述 しますが、これまで使ってきた半角の '(シングルクオート)や "(ダブルクオート)で文字列を囲うという文字列のリテラルでは、その間で 改行を行うことはできません

下記のプログラムは、a という変数に、改行が含まれた文字列 を代入しようとした結果、エラーが発生 しています。

a = "これは改行が
含まれた文字列です"
print(a)

実行結果

  Cell In[4], line 1
    a = "これは改行が
        ^
SyntaxError: unterminated string literal (detected at line 1)

上記のエラーメッセージは、以下のような意味を持ちます。

  • SyntaxError
    構文(文法のこと)(syntax)に関するエラー
  • unterminated string literal (detected at line 1)
    文字列(string)のリテラル(literal)が行内で終了していない(unterminated)

Python には、トリプルクオート という、' または "3 つ並べた ''' または """ を使って 文字列を囲う という リテラル があります。

シングル クオート である ' または、ダブル クオート である "3 つ並べる ので トリプル クオート(日本語では、三重引用符または三連引用符)と呼びます。トリプルクオートという 1 つの文字が 存在するわけではない 点に注意して下さい

トリプルクオートを使ったリテラルには以下のような特徴があります。

  • 改行を含める ことが出来る
  • 単独の '"文字列の中に含める ことが出来る

トリプルクオートによる文字列のリテラルでは、文字列を囲う ''' または """直後 から、同じトリプルクオート が記述されるまでの 間を文字列とみなします。従って、文字列を囲うトリプルクオートと 区別できる ので、文字列の中に 単独の '"記述することが出来ます。同様の理由でクオートが 2 つ連続 した ''"" も記述することができます。

トリプルクオートが文字列を囲う文字として採用された理由は、一般的に 文字列の中'''"""含まれる 場合が ほとんどない ためです。

下記のプログラムは、""" を使って改行が含まれた文字列を a に代入しています。

a = """これは改行が
含まれた文字列です"""
print(a)

実行結果

これは改行が
含まれた文字列です

docstring のスタイル

docstring の書き方には いくつかのスタイル (流儀)があります。以下に、代表的な docstring のスタイルのドキュメントのリンクを紹介します。なお、他にも docstring のスタイルは存在します。

どのスタイルを利用するかは、自分の好み に合わせて 選択 してかまいませんが、同じプログラムの中では docstring の スタイルを統一 することを 強くお勧めします。本記事では Google Style で docstring を記述することにします。

PEP257

Google Style

Numpy Style

Google Style の docstring の書き方

関数に対する1 docstring は以下のように記述します。Google Style では、トリプルクオートに """ を使用します。

""" 関数の概要

詳細な関数の説明

Args:
    仮引数の名前(仮引数のデータ型):
        仮引数の説明

Returns:
    返り値のデータ型: 返り値の説明
"""

上記の Args: のような部分のことを セクション と呼びます。セクションには上記の Args や Returns 以外にも、関数の使用例 を記述する Examples や、注意事項や注釈 を記述する Notes などがあります。それ以外のセクションについては必要になった時点で説明します。

セクションは 必要がなければ記述する必要はありません。例えば仮引数が存在しない関数の場合は Args を、返り値を返さない関数の場合は Returns のセクションを記述する必要はありません。

他にも、以下のようなルールがあります。docstring を長く記述するとわかりづらくなるので、できるだけ 簡潔に記述する ことになっています。下記のルール 1、4、6、7 はそのことを表しています。

  1. 関数の概要は基本的に 1 行で記述する
  2. 関数の概要の最後には半角の .?! のいずれかを記述する
  3. 関数の概要、詳細説明、セクションなどの は、1 行分の空行 で間を 空ける
  4. 説明の必要がないほど簡単な関数 には、関数の概要のみを記述した、1 行の docstring を記述する
  5. 1 行 に記述する長さは 最大で 80 文字 とする(全角の場合は 40 文字)
  6. 関数の定義に型アノテーションを記述した場合は、仮引数や、返り値の データ型省略する
  7. docstring 説明には、関数の機能や使い方 に関する記述を行う。関数の中で行われる 具体的な処理の手順 については 記述しない

docstring はその 関数を利用する人 に向けて記述された 取扱説明書のようなもの なので、関数の使い方に関する説明だけ を記述するべきです。これは、一般の家電製品の取扱説明書の中に、中に入っている機械の説明が記述されていないのと同様です。

関数の中で行われる 具体的な処理 についての説明を記述したい場合は、docstring ではなく、# によるコメントで記述 します。

下記は、initialize_boardplace_mark に docstring を記述したプログラムです。なお、上記のルール 6 で説明したように、関数の定義の中 で仮引数や返り値の 型アノテーションを記述している ので、docstring では仮引数や返り値の データ型は記述しません

def initialize_board() -> list[list[str]]:
    """ 初期化されたゲーム盤のデータを返す.

    Returns:
        初期化されたゲーム盤を表す 2 次元配列の list.
        全ての要素に半角の空白文字が代入されている.
    """

    return [[" "] * 3 for x in range(3)]

def place_mark(board: list[list[str]], x: int, y: int , mark: str):
    """ ゲーム盤の指定したマスに指定したマークを配置する.

    (x, y) のマスに mark で指定したマークを配置する.
    (x, y) のマスに既にマークが配置済の場合は、メッセージを表示する.

    Args:
        board:
            マークを配置する、ゲーム盤を表す 2 次元配列の list
        x:
            マークを配置するマスの x 座標
        y:
            マークを配置するマスの y 座標
        mark:
            配置するマークを表す文字列
    """

    if board[x][y] == " ":
        board[x][y] = mark
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")

docstring は本来は 英語で記述することが推奨 されますが、本記事では わかりやすさを重視 して 日本語で記述 します。

docstring を文字列で記述する理由

docstring を # によるコメントではなく、文字列で記述する点を不思議に思っている人がいるかもしれないので補足します。

データだけが記述された文 を実行した場合は 何の処理も行われません。例えば、下記のプログラムは、1 行目で "a" という文字列型のデータだけを記述していますが、実行しても何も 処理は行われません

"a"

従って、docstring のように、複数の行にまたがって文字列のリテラルだけを記述 した場合、何も処理は実行されないので docstring を コメントのように利用 することが出来ます。

実際には、上記のプログラムの場合は "a" を管理する 新しいオブジェクトが作られます が、そのオブジェクトは どの変数からも参照されていない ので、その後のプログラムで 利用することはできません。従って、実質的には何の処理も行われていません

上記で、データだけが記述された文を実行しても何の処理も行われないと説明しましたが、関数のブロックの先頭の行文字列のリテラルだけを記述 した場合は例外的に、その 文字列のデータ がその関数の定義を管理するオブジェクトの データに登録 され、この後で説明する組み込み関数 help を使うことで 表示することが出来る という仕組みになっています。

コメント は Python のデータとして認識されないので、コメントに記述した内容を オブジェクトが管理 することは ありません。そのため、help のような仕組み を利用することが 出来ない ので、docstring は 文字列で記述する必要 があります。

docstring を記述するメリット

docstring を記述することにって、以下のようなメリットがあります。

プログラムがわかりやすくなる

関数に docstring を記述することで、関数の定義がわかりやすくなる というメリットがあります。

help を利用できる

help という組み込み関数の 実引数 に、docstring を記述した関数名 を記述して呼び出すことで、その関数に記述された docstring を表示する ことが出来ます。help を利用することで、関数の定義の プログラムを見なくても、その関数の 使い方がわかる ようになります。

help(initialize_board)

実行結果

Help on function initialize_board in module __main__:

initialize_board() -> list[list[str]]
    初期化されたゲーム盤のデータを返す.
    
    Returns:
        初期化されたゲーム盤を表す 2 次元配列の list.
        全ての要素に半角の空白文字が代入されている.

実行結果の 1 行目のメッセージの意味は、「メインモジュール(module __main__)内(in)の initialize_board という関数(function)の(on)ヘルプ(Help)」です。

関数呼び出しを記述する際に docstring が表示される

VSCode では、docstring が記述された 関数呼び出しを記述 すると、その付近に docstring が表示 されます。下図は VSCode で place_mark を記述した場合の図です。

関数の上にマウスを移動すると docstring が表示される

VSCode では、関数名の上にマウスを移動 すると、その付近に docstring が表示 されます。表示される内容は、上図と同様なので図は省略します。

pydoc を使ってドキュメントを作ることができる

本記事では紹介しませんが、pydoc を使って、docstring に記述した内容を元に、プログラムで定義した関数の使い方の ドキュメントを作成 することが出来ます。興味がある方は pydoc をキーワードに調べてみると良いでしょう。参考までに、pydoc に関するページのリンクを下記に紹介しますが、このドキュメントは初心者にはわかりづらいので、別の pydoc の使い方を紹介するページを見たほうが良いかもしれません。

本記事での docstring の記述について

docstring は一般的に、関数が完成してから記述 するものです。その理由は、関数の作成途中に docstring を記述した場合に、関数を修正するたびに docstring を 書き直す 必要があるからです。

本記事でも、docstring 記事の途中では記述せず、記事で行われた変更をまとめた marubatsu_new.py のほうで記述することにします。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu_new.py です。

次回の記事

更新履歴

更新日時 更新内容
2023/11/25 データ型のヒントを、型アノテーションに修正しました
2023/10/09 set_markplace_mark に修正しました
for y in range(3)for x in range(3) に修正しました
  1. 関数以外に対する docstring の書き方については、必要になった時点で説明します

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0