目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤の全てのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
- 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
- 先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
前回までのおさらい
前回までの記事で、下記の処理を行う関数を定義しました。
- 初期化されたゲーム盤を返す
initialize_board
- 指定したマスにマークを配置する
place_mark
モジュールの作成とインポート
本記事では、毎回 新しい marubatsu.ipynb という名前のファイルを作成し、そこに各回の記事で紹介したプログラムを記述してプログラムを実行しています。
これまでは、initialize_board
などの関数を 修正しながら説明 していたので、JupyterLab のセルに 毎回 initialize_board
などの 関数の定義を記述 していましたが、完成した initialize_board
などの関数の定義を 毎回記述 するのは 大変 です。
そのような場合は、別のファイル に関数の定義などを記述し、marubatsu.ipynb の中で、そのファイルに定義された 関数を利用 するという方法があります。
ファイル に記述された python のプログラムのことを モジュール と呼び、自分のプログラムで モジュール に記述されたプログラムを 利用する ことをモジュールを インポート すると呼びます。
モジュールの作成
モジュールの作成は Python のプログラムを ファイルに記述して保存 するだけで簡単に行うことが出来ます。その際に、拡張子が .py のファイルに Python のプログラムを保存します。
ファイルの名前がモジュールの名前になる ので、保存の際にはモジュールに記述したプログラムの 内容に合わせた名前 を付けるのが一般的です。
拡張子が .ipynb の JupyteLab のファイルをモジュールとして 利用することはできません。その理由は、JupyterLab のファイルには、プログラムの実行結果などの、Python のプログラム以外のデータが混じっている からです。
VSCode では、以下の手順でモジュールを作成することが出来ます。
- 「ファイル」メニュー→「新しいファイル」→「Python ファイル」を選択する
- 新しいファイルに Python のプログラムを記述する
- 「ファイル」メニュー→「保存」を選択(または Ctrl + S)して、ファイルの保存パネルを開く
- ファイルの名前を入力する
- ファイルを保存するフォルダを選択し、保存ボタンをクリックする
実際に、上記の手順で新しいファイルを作成し、前回の記事で定義した initialize_board
と place_mark
が定義された下記のプログラムを入力して、marubatsu.py という名前 で marubatsu.ipynb と 同じフォルダ に保存して下さい。
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 モジュール名
によってインポートされた モジュール名 は、変数名や関数名 と 全く同じ仕組み で メインモジュールのグローバル名前空間が管理 します。
具体的には、モジュールをインポートすると以下のような手順で処理が行われます。
-
インポートしたモジュールのグローバル名前空間 のデータを 管理する新しいオブジェクトが作成 される
-
モジュールの中 に記述された Python のプログラムがすべて実行 され、そのモジュールのグローバル名前空間 に、モジュールに記述された グローバル変数やグローバル関数 などの 名前が登録 される
-
メインモジュールのグローバル名前空間 に インポートしたモジュールの名前 が 登録 され、手順 1 で作成したオブジェクトに 対応づけられる
下図は、import marubatsu
を実行した場合のオブジェクトの様子を図示したものです。
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3524802%2F6bcc8234-69d7-e784-cb55-9ffc436a595c.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=476de7274401184882a55918b51f1993)
モジュール名は変数名 と 同じ性質 を持つので、変数名や関数名の場合と同様に、モジュール名に対して 値を代入 することが出来ます。
下記のプログラムでは、3 行目でインポートした モジュール名 である marubatsu
を print
で表示 しています。モジュールの名前を print
で表示すると、下記の実行結果のように、「<module `モジュール名` from `モジュールが保存されたファイルのパス`>」が表示されます。
4 行目で、marubatsu
に 1
を代入すると、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 のプログラムであれば その内容は何でもかまわない ので、下記のように、変数や関数が存在しない ファイルも モジュールです。
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_board
に marubatsu のモジュールのグローバル関数 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_board
と place_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
の仮引数 x
と y
は マスの座標を表すデータ なので、整数型を表す int が型アノテーションとして表記されています。
place_mark
の仮引数 mark
は マークを表すデータ なので、文字列型を表す str が型アノテーションとして表記されています。
place_mark
のように、返り値を返さない関数 の場合は、上記のように、関数の返り値の型アノテーションは一般的に 省略 します。何らかの理由で、返り値を省略しない場合は、-> None
を記述します。
型アノテーションを記述するメリット
関数の定義で型アノテーションを記述することで、以下のようなメリットを得ることが出来ます。
関数呼び出しを記述する際に型アノテーションが表示される
VSCode では、関数呼び出しを記述する際に、その付近 に仮引数のデータ型や、関数の返り値のデータ型の 型アノテーション が 表示 されます。
下図は、VSCode で place_mark
の関数呼び出しを 記述し始めた場合 の図です。図のように、これから入力 しようとしている 実引数に対応する 仮引数 board
の 型アノテーション が、すぐ上に 青い文字で表示 されています。
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3524802%2F73626e58-903e-2395-fc37-f13a6629d254.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=09532d023dec9ba04995ba9d8e1f3050)
記述済の関数名にヒントを表示できる
VSCode では、記述済の関数名の上にマウスカーソルを移動 すると、その付近に関数に関する ヒントが表示 されます。
下図は、VSCode で、place_mark
の上にマウスを移動した場合の図です。
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3524802%2F725dcdb4-0a3f-b9a4-92fe-04e8fab8d5d4.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=0d1ed221471e6b1a65ce1ec19e6d5092)
型アノテーションに関する注意点
型アノテーションのことを、データ型の ヒント のように記述してきた理由は、データ型の記述はあくまで ヒントに過ぎず、強制力を持たない からです。型アノテーション と 異なる データ型の 実引数 を記述して関数を呼び出したり、型アノテーション と 異なる データ型の 返り値 を 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 行分の空行 で間を 空ける
- 説明の必要がないほど簡単な関数 には、関数の概要のみを記述した、1 行の docstring を記述する
- 1 行 に記述する長さは 最大で 80 文字 とする(全角の場合は 40 文字)
- 関数の定義に型アノテーションを記述した場合は、仮引数や、返り値の データ型 は 省略する
- docstring 説明には、関数の機能や使い方 に関する記述を行う。関数の中で行われる 具体的な処理の手順 については 記述しない
docstring はその 関数を利用する人 に向けて記述された 取扱説明書のようなもの なので、関数の使い方に関する説明だけ を記述するべきです。これは、一般の家電製品の取扱説明書の中に、中に入っている機械の説明が記述されていないのと同様です。
関数の中で行われる 具体的な処理 についての説明を記述したい場合は、docstring ではなく、#
によるコメントで記述 します。
下記は、initialize_board
と place_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
を記述した場合の図です。
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3524802%2Ff499e23b-ee2f-58ef-184e-3f01147c4222.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=b77839a6b9e98599d5b1c6a83c6ed6e3)
関数の上にマウスを移動すると 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_mark を place_mark に修正しましたfor y in range(3) を for x in range(3) に修正しました |
-
関数以外に対する docstring の書き方については、必要になった時点で説明します ↩