type hints 迷い中

静的型付けの言語の経験がそれほどないというのと、動的型付けである python においてどれほど型に厳格であるべきかという悩みが個人的にはそれなりにある。特に type hints においては単なる静的チェックでしかなく実行時の強制力はない。そのため型のゆるさに目を向ければそれはもう青天井であるとも捉えられる。

python にはダブルアンスコで挟む特殊メソッドを使うことでプロトコルを定義して振る舞いを決めるという慣例があるため厳密に型を付けたいなら適切なメソッドを決める必要があるが、今回は type hints だけに焦点を当てて思いつくものを書きなぐる。

type alias か NewType

ある型を type alias とするか NewType とするかは悩ましいところではあるが、場合によっては NewType を用いることで文脈を明示することができる。

例えば openclose という状態をもつクラスを考えた時、 OpenClose という NewType を定義することで関数のインターフェースとして表現することができる。

import typing


class C:
    def __init__(self):
        self.state = 'open'

    def close(self):
        self.state = 'close'


StatefulC = C  # 無理やり気味


def typealias(c: StatefulC) -> StatefulC:
    c.close()
    return c


Open = typing.NewType('Open', C)
Close = typing.NewType('Close', C)


def newtype(c: Open) -> Close:
    c.close()
    return Close(c)

NewType の定義が煩雑ではあるがそういうもんだろう。

Dict の使い所

キー名が単なる str なのであれば正直 Dict は使いづらい。typescript であれば interface によってキー名を指定することが可能であるが、Dict ではそうはいかない。それをやるには python3.6 までは namedtuple、python3.7 以降であれば dataclass を使うが吉だろう。

dataclass が使えるならそれに越したこたないという気持ちではある。

import typing
from dataclasses import dataclass, asdict


@dataclass
class DC:
    x: int
    y: str


def f(x: DC) -> typing.Dict[str, typing.Union[str, int]]:
    return asdict(DC(x=1, y='hoge'))

Dict の使いどころとしては dataclass の属性にできないデータ型のキーを指定したい場合という事になるだろう。

例えばキーをタプルとした場合を考えてみる。

import typing


D = typing.Dict[typing.Tuple[str, str], str]
E = typing.Dict[typing.Tuple[str, str, str], int]


def f(x: D) -> E:
    return {(*k, v): 0 for k, v in x.items()}

全く実践的な例でないのでよくわからないが、こういう事だろう。

Optional をどこでどう使うべきなんだ

現時点の結論としてはプリミティブな値になりうる場合にのみ使うのがいいだろうと思う。その理由を以下につらつらと書く。

rust や scala などには標準の型として ResultOption がある。これと似たようなことを python の type hints で表現しようとすると Optional を使う事になるだろう。こんな感じ。

import typing
import enum


class E1(Exception):
    ...


class E2(Exception):
    ...


class Reason(enum.Enum):
    laala_missing_mirei = 'laala missing mirei error'
    mirei_quit_puri = 'mirei quit puri error'


Result = typing.Optional[Reason]


def f(func) -> Result:
    try:
        func()
    except E1:
        return Reason.laala_missing_mirei
    except E2:
        return Reason.mirei_quit_puri
    else:
        return None

ただこのような文脈であれば result is None という判定はなぜ None が正常系の分岐なのかというのが書いた本人が書いた当時にしかわからず非常に微妙。 このような凝った処理を行う際は素直に Result 型を定義するべきだろう。

そして Optional の使いどころとしては素直に先述した通りの使い方に留めるのがよろしかろう。