2
2

はじめに

おはようございます、しなもんです。

Pythonは、その簡潔さと柔軟性を生かすため
基本的に動的型付け言語として設計されています。

動的型付けとは、変数の型が実行時に決定され、必要に応じて変更できることを意味します。
そのため、割と無茶なコードが通ったりすることがあります。

x = 5  # xは整数型
x = "Hello"  # xは文字列型に変更可能

めちゃくちゃです。

一方、静的型付けは変数の型を事前に宣言し、コンパイル時にチェックする方式です。
これにより、型関連のエラーを早期に発見できます。

単純にエラーが発生する可能性がめちゃくちゃ増えるので面倒くさそうですが、
しっかり動作するプロダクトを作る上では非常に大切な考え方であるわけです。

そこでPythonは、バージョン3.5から型ヒント(Type Hints)を導入しました。
これにより、動的型付け言語でありながら、静的型チェックの恩恵を受けられるようになりました。

def greet(name: str) -> str:
    return f"Hello, {name}!"

なんでそんな厳密に型付けしなきゃいけないのか

厳密な型付けは先述した通り、バグを減らすために開発において重要な役割を果たします。

  • 型による安全が保証できる
    型の不一致によるバグを事前に防ぐことができます。
    intとfloatの違いによるエラーとかはよくありますよね。それを事前に防げます。

  • コードの可読性が上がる
    関数の入力と出力の型が明確になり、コードの意図が理解しやすくなります。
    共同作業するときに特に威力を発揮します。

  • 開発効率が爆上がりする
    IDEによる補完機能が強化され、開発スピードが向上します。
    VScodeだとCtrl+spaceで出るアレです。
    個人的には一番これがありがたいです。

  • リファクタリングが簡単になる
    型情報があることで、大規模な変更を安全に行えます。

例えば、以下のような関数があるとします。
関数名からは引数がintなのかfloatなのか、はたまた別の型なのか分かりませんが
このように型ヒントを付けることでそこを明確にできます。

def calculate_area(length: float, width: float) -> float:
    return length * width

他の静的型付け言語と比べてPythonはどういう扱いなのか

静的型付け言語としてTypeScriptを挙げます。

  • 型付けは任意
    Pythonの型ヒントはあくまで任意で、型ヒントがなくてもコードは動作します。

    def add(a, b):  # 型ヒントなし
        return a + b
    
    def add_typed(a: int, b: int) -> int:  # 型ヒントあり
        return a + b
    

TypeScriptで言うany地獄になりかねないという意味でもありますが
初心者が全てを完全に理解しなくても書くこともできるという点ではあります。

  • 実行時に型チェックをしない
    Pythonは実行時に型チェックを行いません。型ヒントは静的解析ツールのためのものです。

  • 適当 柔軟性がある
    Pythonは依然として動的型付け言語であり、必要に応じて型を変更できます。

TypeScriptと比較すると
Pythonは、あくまで型チェックを別のステップ(静的解析ツールの使用)で行うわけです。

Pythonの型システムは、動的型付けの柔軟性を維持しつつ、
静的型チェックの利点を取り入れるハイブリッドでいい感じな言語になっているわけです。

Pythonにおける厳密な型付けの実現

型ヒントの使用

def get_full_name(first_name: str, last_name: str) -> str:
    return f"{first_name} {last_name}"

これに関してはどのプロジェクトに関わらず
関数の引数や戻り値を示す型ヒントはできるだけ付けるようにしましょう。
めちゃくちゃ便利です。

静的型チェッカーの活用

$ mypy your_script.py

mypyというのは、型の整合性を実行前に静的状態でチェックしてくれるツールです。便利。

高度な型ヒント技法

ジェネリクス

from typing import List, TypeVar
     
T = TypeVar('T')
def first(l: List[T]) -> T:
    return l[0]

Union型とOptional型

from typing import Union, Optional

def process_input(value: Union[int, str]) -> Optional[str]:
    if isinstance(value, int):
        return str(value)
    elif isinstance(value, str):
        return value
    else:
        return None

Unionはそのリストの中にある型のいずれかを使えることを示すものです。
この例ではintstrどちらでも良いというわけです。

Optionalというその名前から分かるように、
Noneを返す場合もあるという表現ができます。

簡単に使えるので覚えておくといいと思います。

Literal型:

from typing import Literal

def move(direction: Literal['left', 'right', 'up', 'down']) -> None:
    print(f"Moving {direction}")

イミュータブルな値に対して、具体的な値しか受け付けないようにする型です。

メリットと注意点

ぶっちゃけ個人的にはIDEのサポート強化が最も大きい利点だと思います。
補完が楽になるだけでなく、Copilotとかも型をヒントに候補を提示してくれるようになるため
単純にコーディングが爆速になる気がします。

また、コードを中身まで読まなくても、入口と出口を把握できるようになるというだけで効果があります。
数日前に自分が書いたコードが分からないなんてことありませんか?
アレをどうにかできます。

また、潜在的なバグを発見することもできるようになります。

注意点としては、プロジェクトによりますが
Pythonの大きな特徴である柔軟性が低下することです。
個人的には柔軟性が低下して困ったことはありませんが

また、単純に初心者に対しては学習コストが増加するという点もあります。

ベストプラクティスと実践例

  • 適切な粒度での型指定
    すべての変数に型ヒントを付ける必要はないので、重要な部分に集中しましょう。

  • gradual typingの活用
    プロジェクトの一部から徐々に型付けを導入していくアプローチを取ります。


最終的にこんなコードが書かれていたら個人的にはうれしいです。

from typing import List, Dict, Optional

class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

def get_user_by_id(user_id: int, users: List[User]) -> Optional[User]:
    for user in users:
        if user.id == user_id:
            return user
    return None

def group_users_by_age(users: List[User]) -> Dict[int, List[User]]:
    groups: Dict[int, List[User]] = {}
    for user in users:
        if user.age not in groups:
            groups[user.age] = []
        groups[user.age].append(user)
    return groups

さいごに

Pythonで行う厳密な型付けは、動的型付けの柔軟性と静的型チェックの安全性を両立させることができる特別な仕組みです。
Pythonを学んでいる人には是非実践してみてほしい機能です。

2
2
0

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
2
2