Python Type Hinting 入門入門

# Python って型書けるの？

__書けます！！！__
Pythonは別にコンパイルするわけではないので、単体で実行前に静的解析ができるわけではないです......
が、サードパーティ制のソフトウェアを活用することでいい感じ(※要検証)にすることができます。

(あと、実は実行時にも書いた型は影響しません)

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

[Pythonの公式ドキュメント](https://docs.python.org/ja/3/library/typing.html)を読んでください。

__完__

......いやまあこれが真理ではあるんですが、簡単に説明しつつ個人的なハマりポイントを紹介していきます。

## typing のつけかた

```python
hoge: str = "Hello, World!"
```

変数の場合、変数名の後に `: <型名>` と記述することで型を書くことができます。

```python
def func(hoge: str) -> str:
    return "Hello, World!"
```

関数の戻り値は、後ろに `-> <型名>` をつけることで指定できます。

## 使える型

- `List` や `Callable` など、 `typing.py` に記載された型
    - https://github.com/python/cpython/blob/3.9/Lib/typing.py#L1631-L1709
- `int` や `str` などの組み込みクラス
- 自作のクラス
- 型変数

など (ちなみに上2つは下2つに含まれるので、分ける必要があるかといえばない)

## ジェネリクス

[ドキュメント](https://docs.python.org/ja/3/library/stdtypes.html#types-genericalias)を読め

意訳：別の回で話でもしようかな

## Ellipsis

ググラビリティが~~カスな~~悪い組み込みオブジェクトに `...` というものがあります。

```python
t: Tuple[str, ...] = ("H", "e", "l", "l", "o", "w")
```

型を省略するときに使う記号で、以下のように実装に(内部的に)利用されています。

```python
if len(params) == 2 and params[1] is ...:
    msg = "Tuple[t, ...]: t must be a type."
    p = _type_check(params[0], msg)
    return self.copy_with((p, _TypingEllipsis))
```

# ハマりどころ

## 循環インポート

### 返り値を自分自身にする (メソッドチェーン)

```python
class A:
    def func(self) -> A: # エラー
        return self
```

なんかこんな感じのやつを書きたくなることってあるじゃないですか？
[fluent interface](https://bliki-ja.github.io/FluentInterface/)みたいなことしたくなった時とか (こっちはこっちで長くなりそうだから別で作ろう)
ただ、クラス定義内で自身の名前を使うことはできないので、

> NameError: name '\<type\>' is not defined

みたいなエラーになります。(もちろんIDEにも怒られます)

こんな場合には [forward-reference](https://www.python.org/dev/peps/pep-0484/#forward-references) という機能を使います。

```python
class A:
    def func(self) -> 'A':
        return self
```

文字列として型名を記述することで、解決を後回しにすることができます。 (IDEでは依存関係が解決できた場合、実際に型を記述したときと同じ挙動をします)

### 相互的に参照を持つ

__これはそもそも設計が悪いから見直せ__ みたいな記事が散見されたので、良くないんだろうなあとは思いつつ、必要なものは必要なので使わせてくれ！みたいな人向け

(代替案的な設計があればぜひコメントください)

```python:main.py
from a import A
from b import B

b: B = B()
a: A = A(b)
b.set_a(a)
```

```python:a.py
from b import B


class A:
    def __init__(self, b: B):
        self.b: B = b
```

```python:b.py
from typing import Optional

from a import A


class B:
    def __init__(self):
        self.a: Optional[A] = None

    def set_a(self, a: A):
        self.a = a
```

このように、A, Bの両方で相手を参照するようなインポートを行おうとすると以下のようなエラーが出ます

> ImportError: cannot import name 'A' from partially initialized module 'a' (most likely due to a circular import) (a.py)

これを解決するためには、 `typing.TYPE_CHECKING`  を利用します
https://docs.python.org/ja/3/library/typing.html#typing.TYPE_CHECKING

```python:a.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from b import B


class A:
    def __init__(self, b: 'B'):
        self.b: B = b
```

```python:b.py
from typing import Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from a import A


class B:
    def __init__(self):
        self.a: Optional[A] = None

    def set_a(self, a: 'A'):
        self.a = a
```

`TYPE_CHECKING` は型検査中のみに `True` になる (※非保証) 値。
if文などでインポートをチェックすることにより、エラーを回避できる。
ただ、この状態だと Type Hinting に使っている型名が存在しないのでエラーとなる。

> NameError: name 'B' is not defined

ため、前述した forward-refarence と組み合わせることにより評価も遅延して解決することができる。

## 複雑な(再帰的)データを型付けする

```json
{
    "type": "or",
    "filters": [{
        "type": "or",
        "filters": [...]
    }, {
        "type": "range",
        "from": 1,
        "to": 10
    }]
}
```

なんかこんな感じで再帰的にデータが入りうるような型を作りたい！ → __型変数を使いましょう__

```python
TypeOp = Dict[str, Union[str, int, List['TypeOp']]]

json_: TypeOp = {
    "type": "or",
    "filters": [{
        "type": "or",
        "filters": [...]
    }, {
        "type": "range",
        "from": 1,
        "to": 10
    }]
}
```

## 戻り値がvoid

Python に `void` なんてものはない。

```python
def func()
    pass
    
print(func()) # None
```

Python では `return` を省略すると  `None` が返るので、型も同様に指定すると良い

## 上限つきワイルドカード型

親クラスを直接書いてやれば動きます

`Type.TypeVar` を使うことで無理矢理 Java のときみたいにも書ける (書いてた) んですが、必要はなさそうです。

##  キャストしたい

`isinstance` でチェックすればよしなにしてくれます。
ただ、直接触っても予測変換がでないので一旦正しい型の変数に代入しましょう。

```python
a: Any = 123

if isinstance(a, A):
    b: int = a
```

#参考文献

- [PythonとType Hintsで書くバックエンド](https://engineering.mercari.com/blog/entry/20201105-0a4057b2ba/)
- [PyCharm の型ヒント](https://pleiades.io/help/pycharm/type-hinting-in-product.html)
- [Pythonではじまる、型のある世界](https://qiita.com/icoxfog417/items/c17eb042f4735b7924a3)
- [今から始める型安全 Python](https://speakerdeck.com/yamitzky/start-python-type-hints)