😽

Python Type Hinting 入門入門

2021/10/04に公開

Python って型書けるの?

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

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

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

Pythonの公式ドキュメントを読んでください。

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

typing のつけかた

hoge: str = "Hello, World!"

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

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

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

使える型

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

ジェネリクス

ドキュメントを読め

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

Ellipsis

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

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

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

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

ハマりどころ

循環インポート

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

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

なんかこんな感じのやつを書きたくなることってあるじゃないですか?
fluent interfaceみたいなことしたくなった時とか (こっちはこっちで長くなりそうだから別で作ろう)
ただ、クラス定義内で自身の名前を使うことはできないので、

NameError: name '<type>' is not defined

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

こんな場合には forward-reference という機能を使います。

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

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

相互的に参照を持つ

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

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

main.py
from a import A
from b import B

b: B = B()
a: A = A(b)
b.set_a(a)
a.py
from b import B


class A:
    def __init__(self, b: B):
        self.b: B = b
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

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
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 と組み合わせることにより評価も遅延して解決することができる。

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

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

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

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 なんてものはない。

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

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

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

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

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

キャストしたい

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

a: Any = 123

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

#参考文献

Discussion