Pythonのtypingモジュールを効果的に使用する – 型チェッカーを効果的に活用する

Python 3.5以降、Pythonのtypingモジュールは、静的型チェッカーやリンターがエラーを正確に予測するのに役立つ型のヒントを提供する試みです。

ランタイム中にPythonがオブジェクトの型を特定しなければならないため、開発者がコード内で正確に何が起こっているのかを見つけることが非常に困難になることがあります。

スタックオーバーフローの回答によると、PyCharm IDEのような外部のタイプチェッカーでも最高の結果を出せないことがあります。平均して、エラーを正しく予測するのは時間の50%ほどです。

Pythonは、エラーを検出するための外部の型チェッカーが利用できるように、タイプヒント(型注釈)と呼ばれるものを導入することで、この問題を緩和しようとしています。これは、プログラマがコンパイル時に使用されるオブジェクトの型を示し、型チェッカーが正しく機能することを保証するための良い方法です。

これによって、Pythonのコードは他の読者にとっても非常に読みやすく、頑強になります!

注意:これはコンパイル時に実際の型チェックを行いません。もし戻ってきた実際のオブジェクトが示された型と異なる場合、コンパイルエラーは起こりません。そのため、私たちはmypyのような外部の型チェッカーを使用して、型エラーを特定する必要があります。


推奨される前提条件

効果的にtypingモジュールを使用するためには、静的な型の一致を確認するために外部の型チェッカー/リンターを使用することをおすすめします。Pythonで最も広く使用されている型チェッカーの一つはmypyですので、この記事を読む前にインストールすることをおすすめします。

私たちはすでにPythonでの型チェックの基本を説明しました。最初にこの記事を見てください。

この記事では、静的型チェッカーとしてmypyを使用します。mypyのインストール方法は以下の通りです。

pip3 install mypy

Pythonファイルに対して、型の一致を確認するためにmypyを実行できます。これは、Pythonコードを「コンパイル」しているかのようなものです。

mypy program.py

エラーのデバッグが完了した後に、通常通りプログラムを実行することができます。

python program.py

必要な前提条件が整ったので、モジュールのいくつかの機能を使ってみましょう。


型のヒント / 型の注釈

関数についての説明

関数の返り値の型やパラメーターの型を指定するために、私たちはアノテーションを使用することができます。

def print_list(a: list) -> None:
    print(a)

このコードは、型チェッカー(私の場合はmypy)に、引数としてリストを受け取りNoneを返す関数print_list()があることを伝えます。

def print_list(a: list) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

まずは、私たちのタイプチェッカーであるmypyでこれを実行しましょう。

vijay@JournalDev:~ $ mypy printlist.py 
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)

予想通り、エラーが発生します。行5では引数がリストではなく、整数です。

変数について

Python 3.6以降、変数の型にも注釈を付けることができるようになりました。しかし、関数が戻り値を返す前に変数の型を変更したい場合は、これは必須ではありません。

# Annotates 'radius' to be a float
radius: float = 1.5

# We can annotate a variable without assigning a value!
sample: int

# Annotates 'area' to return a float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# Print all annotations of the function using
# the '__annotations__' dictionary
print('Dictionary of Annotations for area():', area.__annotations__)

マイパイの出力結果:

vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}

mypyの推奨される使い方は、まず型アノテーションを記述してから、タイプチェッカーを使用することです。


型エイリアス

タイピングモジュールは、型エイリアスを提供しており、エイリアスに型を割り当てることで定義されます。

from typing import List

# Vector is a list of float values
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)

結果を日本語で自然に言い換えてください、任意の1つのオプションのみを必要します。

vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]

上記のスニペットでは、Vectorは浮動小数点値のリストを指すエイリアスです。上記のプログラムでは、エイリアスに対して型ヒントを記述することができます。

以下では、認められる別名の完全なリストが示されています。

もう一つ例を見てみましょう。この例では、辞書内のすべてのキーと値のペアをチェックし、名前:メールアドレスの形式に一致するか確認します。

from typing import Dict
import re

# Create an alias called 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # Check if name and email are strings
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # Check for email xxx@yyy.zzz
        if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
            return False
    return True


print(check_if_valid({'vijay': 'vijay@sample.com'}))
print(check_if_valid({'vijay': 'vijay@sample.com', 123: 'wrong@name.com'}))

私のPythonスクリプトからの出力

vijay@JournalDev:~ $ mypy validcontacts.py 
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)

ここでは、mypyにおいて2番目の辞書のnameパラメータが整数(123)であるため、静的なコンパイル時エラーが発生します。そのため、エイリアスはmypyから正確な型チェックを強制する別の方法です。


NewType()を使用して、ユーザー定義のデータ型を作成する。

私たちはNewType()関数を使用して、新しいユーザー定義の型を作成することができます。

from typing import NewType

# Create a new user type called 'StudentID' that consists of
# an integer
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

静的な型チェッカーは、新しい型を元の型のサブクラスとして扱います。これは論理的なエラーを見つけるのに役立ちます。

from typing import NewType

# Create a new user type called 'StudentID'
StudentID = NewType('StudentID', int)

def get_student_name(stud_id: StudentID) -> str:
    return str(input(f'Enter username for ID #{stud_id}:\n'))

stud_a = get_student_name(StudentID(100))
print(stud_a)

# This is incorrect!!
stud_b = get_student_name(-1)
print(stud_b)

マイパイからの出力

vijay@JournalDev:~ $ mypy studentnames.py  
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)

どんなタイプ

これは特殊なタイプであり、静的型チェッカー(私の場合はmypy)に、すべてのタイプがこのキーワードと互換性があることを通知するものです。

私たちの古いprint_list()関数を考えてみましょう。今はどんな型の引数でも受け入れるようになりました。

from typing import Any

def print_list(a: Any) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

今後、mypyを実行する際にはエラーは発生しなくなります。

vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1

戻り値の型またはパラメータ型が指定されていない全ての関数は、自動的に Any を使用するようにデフォルトに設定されます。

def foo(bar):
    return bar

# A static type checker will treat the above
# as having the same signature as:
def foo(bar: Any) -> Any:
    return bar

したがって、Anyを使用することで、静的および動的に型付けされたコードを組み合わせることができます。


結論

この記事では、Pythonのtypingモジュールについて学びました。このモジュールは、型のチェックにおいて非常に便利であり、mypyなどの外部の型チェッカーが正確にエラーを報告できるようにしています。

これによって、設計上は動的型付けの言語であるPythonで静的型付けのコードを書く方法が提供されます。


参考文献

  • Python Documentation for the typing module (This contains extensive detail on more methods in this module, and I recommend this as a secondary reference)
  • StackOverflow question on type hints (This provides a very good discussion on the topic. I highly recommend you to read this topic as well!)

コメントを残す 0

Your email address will not be published. Required fields are marked *