型ヒント
型ヒントの基本と標準表記
Pythonは動的型付け言語であり、実行時に変数の型が決定されます。
そのため、実行するまで型の不一致によるエラーに気付きにくいという課題があります。
この課題を解決するために、プログラム中に変数の意図した型を記述する型ヒントと呼ばれる機能が導入されました。
型ヒントを記述しておくことで、統合開発環境やエディタの支援機能が働き、記述ミスを未然に防ぐことができます。
Python 3.10以降では、list[int] のような組み込み型を用いたジェネリクス表記や、int | str のような複数の候補を表す | 記号が標準でサポートされています。
変数、引数、戻り値に型ヒントを適用したプログラムは以下のようになります。
type_hints_basic.py
def greet_users(names: list[str], age_limit: int | None) -> list[str]:
valid_names: list[str] = []
for name in names:
if len(name) > 0:
valid_names.append(name)
return valid_names
users: list[str] = ["Alice", "Bob", ""]
result = greet_users(users, None)
print(result)
プログラムの実行結果は以下のようになります。
実行結果
['Alice', 'Bob']
型ヒントはコメントのようなものであり、Pythonの実行エンジン自身はこれらを無視して動作します。 したがって、誤った型の値を渡しても実行時にエラーになるわけではありませんが、エディタ上で警告が表示されることで開発が円滑になります。
Protocolによる静的ダックタイピング
ダックタイピングは非常に柔軟ですが、型ヒントを記述する際に「特定のメソッドを持つオブジェクト」という条件をどのように指定するかが課題となります。
Pythonでは、typing モジュールに用意されている Protocol クラスを継承することで、ダックタイピングを型ヒントとして表現できます。
これを構造的部分型と呼びます。
まず、型ヒントがなく、どのようなオブジェクトでも渡せてしまう素朴なプログラムは以下のようになります。
untyped_duck.py
import io
class ConsoleWriter:
def write(self, text):
print(text)
def log_message(writer, message):
writer.write(message)
console_writer = ConsoleWriter()
log_message(console_writer, "Hello!")
プログラムの実行結果は以下のようになります。
実行結果
Hello!
このコードでは、writer 引数の型が指定されていないため、write メソッドを持たない不正なオブジェクトが渡されても、実行するまでエラーに気付けません。
typing.Protocol を用いて、write メソッドを持つオブジェクトに限定する型ヒントを追加したプログラムは以下のようになります。
protocol_writer.py
from typing import Protocol
import io
class Writable(Protocol):
def write(self, text: str) -> int:
...
class ConsoleWriter:
def write(self, text: str) -> int:
print(text)
return len(text)
class InvalidWriter:
def print_text(self, text: str) -> None:
print(text)
def log_message(writer: Writable, message: str) -> None:
writer.write(message)
console_writer = ConsoleWriter()
log_message(console_writer, "Hello with Protocol!")
string_buffer = io.StringIO()
log_message(string_buffer, "StringIO is also Writable.")
invalid_writer = InvalidWriter()
# エディタや静的解析ツールが、Writableに適合しないことを警告します。
# log_message(invalid_writer, "This will fail at type check.")
print(string_buffer.getvalue())
プログラムの実行結果は以下のようになります。
実行結果
Hello with Protocol!
StringIO is also Writable.
Writable は Protocol を継承し、中身を持たない ... を使って定義されています。
ConsoleWriter は Writable を明示的に継承していません。
しかし、Writable が要求する write メソッドを実装しているため、Writable 型として適合していると見なされます。
このように、クラスの継承関係を強制することなく、振る舞いの有無に基づいて安全な型ヒントを記述できます。
mypyによる静的型チェック
Pythonのコードに付与した型ヒントを検証するためには、静的解析ツールを使用します。 代表的なツールとして、公式でも推奨されている mypy があります。 mypyに関する詳細や使い方は、mypyの公式ドキュメントを参照してください。
mypyコマンドが実行できる環境において、上記の protocol_writer.py に対して型チェックを実行するコマンドは以下のようになります。
実行結果
$ mypy protocol_writer.py
Success: no issues found in 1 source file
このように、ConsoleWriter や io.StringIO は Writable プロトコルに適合しているため、型エラーは発生しません。
一方で、protocol_writer.py の中でコメントアウトされている以下の行のコメントを解除したとします。
log_message(invalid_writer, "This will fail at type check.")
この状態で再び mypy を実行すると、以下のように型エラーが検出されます。
実行結果
$ mypy protocol_writer.py
protocol_writer.py:28: error: Argument 1 to "log_message" has incompatible type "InvalidWriter"; expected "Writable" [arg-type]
Found 1 error in 1 file (checked 1 source file)
このように、静的解析ツールを実行することで、プログラムを実行する前に関数の引数の型が不適合であることを検知し、バグを未然に防ぐことができます。
演習問題
演習1
接続先の名前を受け取り、接続終了時にメッセージを表示する DatabaseConnection クラスを作成しなさい。
コンストラクタは文字列型の db_name を受け取り、close(self) -> None メソッドは標準出力に指定のフォーマットで接続終了を通知するものとします。
クラス定義におけるすべてのメソッドの引数と戻り値に型ヒントを明記しなさい。
演習2
typing.Protocol を継承して、戻り値なしの close(self) メソッドを持つことを要求する Closer プロトコルを定義しなさい。
また、close メソッドの代わりに shutdown(self) -> None メソッドを持つ InvalidResource クラスを定義しなさい。
演習3
Closer プロトコルに適合するオブジェクトを引数として受け取る safe_close 関数を実装しなさい。
引数 resource と戻り値に型ヒントを指定し、関数内部で close メソッドを呼び出すこと。
作成した各クラスのインスタンスや、標準ライブラリの socket.socket オブジェクトを safe_close 関数に渡した際の静的解析ツールによる検証結果の違いについて、プログラム内のコメントで説明しなさい。
解答例
演習問題のプログラムの実装例は以下の通りである。
実装の方針として、まず接続状態と終了処理を表現する DatabaseConnection クラスを定義し、引数と戻り値に適切な型アノテーションを付与する。
次に typing.Protocol を継承した Closer プロトコルを定義し、safe_close 関数の引数に対する静的な型制約として利用する。
最後に safe_close 関数に対してプロトコルに適合するオブジェクトと適合しないオブジェクトをそれぞれ渡し、静的解析ツールによって適合性の判定が行われることを確認する。
resource_protocol.py
from typing import Protocol
import socket
class Closer(Protocol):
def close(self) -> None:
...
class DatabaseConnection:
def __init__(self, db_name: str) -> None:
self.db_name: str = db_name
self.is_open: bool = True
def close(self) -> None:
self.is_open = False
print(f"{self.db_name} への接続を閉じました。")
class InvalidResource:
def __init__(self, name: str) -> None:
self.name: str = name
def shutdown(self) -> None:
print(f"{self.name} をシャットダウンしました。")
def safe_close(resource: Closer) -> None:
resource.close()
# 動作検証
db_conn = DatabaseConnection("production_db")
safe_close(db_conn)
# 標準ライブラリのsocket.socketもclose()を持つため、Closerに適合する
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
safe_close(sock)
invalid = InvalidResource("web_server")
# InvalidResourceはclose()を持たないため、Closerに適合しない
# 静的解析ツール(mypyなど)を実行すると、以下の行で型エラーが警告される
# safe_close(invalid)
プログラムの実行結果は以下のようになります。
実行結果
production_db への接続を閉じました。
※ socket オブジェクトに対する close の呼び出し結果は標準出力には表示されません。