Qlitre's Blog

2022.08.13 Python

型を意識したPython、typingライブラリの基本的なまとめ

今回は自分の勉強も兼ねてPythonの型ヒントについて簡単にまとめました。
動作バージョン、Python 3.8.6

なぜ型ヒントをつけるべきなのか

前提として、Pythonは型を定義しなくても動きます。
正直に言うと型を定義するのは面倒ですし、指定しないでも動くのがPythonの良いところだとも思います。
ただし、保守性、可読性などを考えると、Pythonでも型を意識した方が圧倒的にいいです。

例えば適当な関数を用意しました。

def add_func(a, b):
    return a + b


説明するまでもないですが、二つの引数を足して返すだけの関数です。
これに型ヒントをつけて、整数を受け取ってほしい場合にはこうします。

def add_func(a: int, b: int) -> int:
    return a + b


a: int の部分が引数の型、-> intの部分が返り値の型です。

使ってみましょう。

some_value = add_func(a=5, b=2)
print(some_value)
>>>
7


問題なく動作します。
次に試しに文字列を渡してみましょう。

some_value = add_func(a="John", b="Lennon")
print(some_value)
>>>
JohnLennon


直感的に言って驚くべき部分ですが、Pythonは型ヒントと矛盾する形で引数を渡してもエラーになりません。
とはいえ、全く意味がないかというとそうではありません。

例えば今どきのIDEですと、関数を使おうとした際に、型ヒントを表示してくれます。



int型を指定するんだな、ということが分かります。
試しに無視して、文字列を入れてみましょう。



警告を表示してくれました。
引数にint型が指定されてるのに、str型が指定されてるから直せよって教えてくれるわけです。

このように型ヒントを付与することで、関数が意図しない形で使われる危険を減らすことができそうです。

mypyでテストをする

上記に加えて、mypyというモジュールを使うと事前に型のテストをすることが可能です。

pip install mypy


使うのは簡単で、例えば以下のような型エラーが発生しているコードをわざと書いてみます。

# type_test.py
def add_func(a: int, b: int) -> int:
    return a + b


add_func('str', 'str')


次にターミナルからtype_test.pyを呼び出します。

mypy type_test.py
>>>
type_test.py:6: error: Argument 1 to "add_func" has incompatible type "str"; expected "int"
type_test.py:6: error: Argument 2 to "add_func" has incompatible type "str"; expected "int"
Found 2 errors in 1 file (checked 1 source file)


このようにエラーを検出してくれました。

いろいろな型のつけ方

次に公式の型ヒントライブラリtypingを参考に、型ヒントの使い方をまとめていきます。

typing --- 型ヒントのサポート

基本形


まずはシンプルな型です。

# intを引数
def add_func(a: int, b: int) -> int:
    return a + b


# strを引数
def fullname_func(last_name: str, first_name: str) -> str:
    return f'{last_name} {first_name}'


引数の値を指定する

場合によっては引数に特定の値を指定したいケースがあるかもしれません。
そういう場合はLiteralを使用します。

from typing import Literal


def greeting(language: Literal["python", "javascript"]) -> None:
    print(f'Hello, {language}')


引数のlanguageにpythonもしくはjavascriptを強制するような形です。
試しにrubyを指定すると警告が出ます。



数値を指定する場合。

def accepts_only_four(x: Literal[4]) -> None:
    pass


複数の型を指定する

こういう場合はUnionを使います。
例えばある掛け算をする関数に、int型、もしくはfloat型で受け取りたいような場合。

from typing import Union


def multiplication_func(a: Union[int, float], b: Union[int, float]) -> Union[int, float]:
    return a * b

# ok
some_value1 = multiplication_func(1, 1.2)
some_value2 = multiplication_func(2, 2)
some_value3 = multiplication_func(2.2, 2.2)


ちなみに型は変数っぽく使うこともできます。

from typing import Union

MyUnionType = Union[int, float]


def multiplication_func(a: MyUnionType, b: MyUnionType) -> MyUnionType:
    return a * b


リストやタプル、辞書の指定


from typing import Union, List, Tuple, Dict


def list_sum_func(numbers: List[Union[int, float]]) -> Union[int, float]:
    """
    intもしくはfloatで構成されたリストを受け取る
    """
    return sum(numbers)


def tuple_full_name_func(name: Tuple[str, str]) -> str:
    """
    (str, str)のタプルを受け取る
    """
    return f'{name[0]} {name[1]}'


def dict_person_profile_func(profile: Dict[str, str]) -> str:
    """
    キーと値がstrの辞書を受け取る
    """
    return f'{profile.get("name")}さん {profile.get("age")}'


list_sum_func([1, 2, 3.1])
# >>> 6.1

tuple_full_name_func(("柴田", "聡子"))
# >>> 柴田 聡子

prof = {"name": "柴田聡子", "age": "36歳"}
dict_person_profile_func(prof)
# >>> 柴田聡子さん 36歳


複雑な辞書を指定する

上記の辞書の例で年齢のところは36と整数を入れたいところです。
こういう風にキーと値が統一されていない、ちょっと複雑な辞書を指定する場合はTypedDictを用います。

from typing import TypedDict

Profile = TypedDict("Profile", {"name": str, "age": int})


def dict_person_profile_func(profile: Profile) -> str:
    """
    Profile型を受け取る
    """
    return f'{profile.get("name")}さん {profile.get("age")}歳'


prof = Profile(name="柴田聡子", age=36)
# >>> {'name': '柴田聡子', 'age': 36}
dict_person_profile_func(prof)
# >>> 柴田聡子さん 36歳


クラスで型を指定する

TypedDictよりももう少し複雑な型を指定したい場合はdataclassの使用を検討した方がいいでしょう。
例えば先ほどのProfileの例と同じようなことをする場合。

from dataclasses import dataclass


@dataclass
class SingerProfile:
    name: str
    age: int


def singer_profile_func(profile: SingerProfile) -> str:
    return f'{profile.name} {profile.age}歳'


prof = SingerProfile(name="カネコアヤノ", age=29)
print(prof)
# >>> SingerProfile(name='カネコアヤノ', age=29)
print(singer_profile_func(prof))
# >>> カネコアヤノ 29


クラスにすると、少し複雑な型も指定がしやすくなります。
例えばあるシンガーと紐づく複数のアルバムを情報として持ちたい場合。

from dataclasses import dataclass
from typing import List


@dataclass
class Album:
    title: str
    price: int


@dataclass
class SingerInformation:
    name: str
    age: int
    albums: List[Album]


def show_singer_data(data: SingerInformation) -> None:
    """SingerInfomationを受け取り、情報を表示する"""
    print('===Profile===')
    print(f'name: {data.name}')
    print(f'age: {data.age}')
    print('===Album===')
    for album in data.albums:
        print(f'title: {album.title}')
        print(f'price: {album.price}')


albums = [
    Album(title="しばたさとこ島", price=1919),
    Album(title="いじわる全集", price=3018)
]

singer_information = SingerInformation(name="柴田聡子", age=36, albums=albums)

show_singer_data(singer_information)
# >>>
# ===Profile===
# name: 柴田聡子
# age: 36
# ===Album===
# title: しばたさとこ島
# price: 1919
# title: いじわる全集
# price: 3018


おわりに

今回はPythonの型ヒントについて簡単にまとめました。
型ヒントをしっかりと書くと、保守性や可読性の向上もさることながら、何となくコードがかっこよく見えるというメリットもありますね!
引き続き意識していきたい部分です。