あなた（わたし）のための型ヒント - 型変数TypeVar入門

この記事は[JSL(日本システム技研) Advent Calendar 2021](https://qiita.com/advent-calendar/2021/jsl)のn日目の記事です！

# あなた（わたし）のための型ヒント - 型変数TypeVar入門

型に憧れています。
中でもJavascriptで **T** と書くあれ。そうジェネリクス。これがよくわかりません。

Pythonでも標準ライブラリのTypingモジュールに、TypeVarというものがあります。
ドキュメントを読んでまとめたのが本記事になります。型弱者なあなた（わたし）の理解の助けになれば幸いです。

[typing.TypeVar \-\-\- 型ヒントのサポート — Python 3 ドキュメント](https://docs.python.org/ja/3/library/typing.html#typing.TypeVar)

# 注意

Pythonは動的型付け言語です。どんなに型が間違っていても、 **実行** はできてしまいます。

```python
>>> age: int = "こんにちは！"  # int型の変数ageに文字列を代入
>>> print(age)
こんにちは！
```

正しく型が利用できているか確認するため、必ずmypy[^1]などの静的型チェックツールと合わせて利用しましょう。

実行できてしまうのに、なぜPythonで型をつけるのでしょうか？

それはコードの理解を助けるためです。
あの関数やあのメソッドの引数には何が指定できるのか、戻り値は何が返ってくるのか、型ヒント（とできればVS CodeやPyCharm）があると、それらを教えてくれます。

「あの人の書いた共通関数、どうやって使うんだっけ...」
「1週間前に書いた自分のコードを全く理解できない...」

みなさんも経験があるのではないでしょうか？
あなた（明日のわたし）のための型、それが型ヒントになります。

Pythonで型ヒントが導入される経緯などがDropbox社のブログにまとめらています。ぜひご一読ください。
[Python の型チェックが 400 万行に到達するまで -  Dropbox からのお知らせ](https://navi.dropbox.jp/our-journey-to-type-checking-4-million-lines-of-python)

# TypeVarとは
TypeVarは型変数と呼ばれる型を定義できます。

型変数で型を付けた場合、同じ型であるという意味になります（デフォルトで **不変** という性質を持ちます [^2] ）。
具体的な型（strやintなど）は **利用するときに** 指定するイメージです。

型変数は関数の引数や戻り値、またクラス（typingモジュールのGenericなどと合わせて定義）で利用し、 主に汎用的な機能やライブラリを作る場合に効果を発揮します。
本記事では、TypeVarのイメージを掴んでもらうのが目的のため、Genericの説明はしません。詳しくは公式ドキュメントを参照してみてください。
[ジェネリクス \-\-\- 型ヒントのサポート — Python 3 ドキュメント](https://docs.python.org/ja/3/library/typing.html#generics)


# TypeVarで型を定義

定義は次の例のように、第一引数に代入する変数名と同じ名前を渡す必要があります。

```python
from typing import TypeVar

# 正しい定義
T = TypeVar("T")
CHECK = TypeVar("CHECK")
SAME = TypeVar("SAME")

# 間違った定義
ONE = TypeVar("TWO")
```

# TypeVarを関数の型付けで利用する

2つの引数のうち、いずれかを1/2の確率で返すlottery()関数を定義してみましょう。

関数の引数、戻り値にTypeVarで定義した型変数「T」を指定します。

```python
import random
from typing import TypeVar

T = TypeVar("T")  # TypeVarを変数Tに定義

def lottery(ans_a: T, ans_b: T) -> T:  # 引数、戻り値の型にTを指定
    """1/2の確率でいずれかの引数を返す"""
    if random.random() <= 0.5:
        return ans_a
    else:
        return ans_b
```

TypeVarの不変という性質によりlottery()関数を実行した時に、intを渡した場合にはintが、strを渡した場合にはstrが返ってくるという定義になります。

## 正しい使い方

上記の`lottery()`関数を利用してみましょう。以下であればmypyに怒られません。

```python
# 数値を引数として渡し、戻り値として数値を受け取る
a_num: int = 1
b_num: int = 2
ans_num: int = lottery(a_num, b_num)  # 戻り値は数値

# 文字列を引数として渡し、文字列を受け取る
a_str: str = "win"
b_str: str = "loose"
ans_str: str = lottery(a_str, b_str)  # 戻り値は文字列

# リストを引数として渡し、リストを受け取る
a_list: list = ["みかん", "いちご", "バナナ"]
b_list: list = ["ヘーゼルナッツ", "クルミ", "アーモンド"]
ans_list: list = lottery(a_list, b_list)  # 戻り値はリスト
```

## 間違った使い方

以下の例が間違った使い方になります。mypyに怒られてしまいます。

```Python
# 2つの引数に異なる型の値を渡す
ng_num: int = 1
ng_str: str = "2"
ng_ans = lottery(ng_num, ng_str)
```

もしTypeVarを利用せずに前述の`lottery()`関数を定義する場合、以下の3つがそれぞれ必要になります。

* 数値を受け取り数値を返す関数
* 文字列を受け取り文字列を返す関数
* リストを受け取りリストを返す関数

# まとめ

処理は同じで引数や戻り値の型のみが異なる場合にTypeVarは効果を発揮します。
Pythonで型を書いていて共通処理を作らなければならない場合、TypeVarの利用を検討いただけると幸いです。


[^1]: mypyはOSSの静的型チェックツールです。Pythonの父、Guido van Rossum氏も開発に携わっている準公式とも言えるツールです。 [python/mypy: Optional static typing for Python](https://github.com/python/mypy)

[^2]: 他にも共変 `covariant=True` または反変 `contravariant=True` があります。詳細は[typing \-\-\- 型ヒントのサポート — Python 3 ドキュメント](https://docs.python.org/ja/3/library/typing.html#typing.TypeVar)や、[PEP 484 \-\- Type Hints \| Python\.org](https://www.python.org/dev/peps/pep-0484/#covariance-and-contravariance)を参照してください :pray: