はじめに
Pythonでキチンと型アノテーションを書くのであれば一度は読んでおきたいのがtypingライブラリの公式ドキュメントです。
前回の記事でも読んでくださいと(偉そうに)書いたわけですが、実のところこれは型アノテーションを解釈する側1に向けたドキュメントだったりもするのでアノテを書く側にとっては情報がごちゃごちゃしてるんですよね。加えて未翻訳の箇所もそれなりにあってま~~~~読みにくい。実際に読んでいただいた方々からの評判もすこぶる悪かったです。また
機能は分かったけど結局どんな時に使えばいいのか分からない
という致命的なコメントも聞きました。
そこで今回はこれらの不満を解消すべく、公式ドキュメントを一般ユーザー向けに再編し具体的なユースケースを盛り込んだ非公式ドキュメントをご用意しました。
なおPython 3.10は2021年10月04日リリース予定ですが、先駆けてドキュメントが公開されていますので当記事はこちらの3.10.0b2バージョンを元に執筆しています。
想定読者
- 型アノテーションは書き始めてるけどまだなんとなくふわっとしている
- 最後にtypingのドキュメントを読んでから時間があいている(~3.7くらい)
- 他の言語から来たけどPythonの型ってどうなってるの
- 漸進的型付けやっていくぞ
そもそも型アノテーションとは何ぞやという方はまずこちらやこちらの記事を一読されることをおすすめします。
解説
typingモジュールからインポートできるオブジェクトを
- 型アノテーションの拡張
- ユーティリティ
の2つに分類して紹介します。
いかんせん数がはちゃめちゃに多いので読み飛ばせるよう特に重要な項目には★をつけています。
型アノテーションの拡張
def hoge(x: int) -> str:
dx: float = x * 0.1
return str(dx)
上記で言うint
/str
/float
に該当する部分に書くことで型チェッカーに対して様々な情報を伝えることができる特殊な型です。
Python 3.9で多くが非推奨になり(理由は後述)公式ドキュメントが無駄に読み辛くなっているためここでは3.9以降OK/NGに分けて記載します。
3.9以降も使えるもの
Any
なんでもいいよ型です。
型に依らない汎用的なライブラリを作る際や型情報のないデータを取ってきて自分でこねこねしないといけない場合に使います。
from typing import Any, TypeVar
from collections.abc import Iterable
T = TypeVar("T")
# 配列(のようなオブジェクト)の中からt型の要素だけを残して返す関数
# Iterable[Any]はIterableと等価ですが、Anyを書いた方が使う人にやさしいので書いた方がいいです
def type_filter(itr: Iterable[Any], t: type[T]) -> list[T]:
return [elm for elm in itr if isinstance(elm, t)]
x = ["hoge", 1, "fuga", 2, "piyo", 3]
type_filter(x, str)
>>> ["hoge", "fuga", "piyo"]
NoReturn
実行時エラーで値が返らない関数に使います。というか使いません。
Pythonは返すものを明示しない場合暗黙的にNoneオブジェクトが返るので-> NoReturn
と-> None
は別物であることに注意してください。
return
を書かない時は-> None
です。
def hoge() -> None:
print("hoge!!!")
x = hoge()
>>> hoge!!!
x
>>> None
★ Union
from typing import Union
hoge: Union[int, str]
とした場合hoge
はint
/str
両方の値を取り得ます。「もしくは」です。
3.10からは|
演算子の適用が拡張されて
hoge: int | str
と書けるようになりますが、これは
- 見た目がスマート
- パッと見で意味が分かる(論理和なので)
-
Union
をインポートする必要がない - TypeScriptやScalaと同じ書き方
と良いことずくめなので是非Union
から乗り換えて欲しいなと思います。
★ Optional
hoge: Union[int, None]
を省略した形が
hoge: Optional[int]
です。そんなに省略できてなくて面白いですね2。
そしてこの略記というものがチーム開発をする上では非常にやっかいで、上記Unionの略記もあわせると
hoge: Union[int, None]
hoge: int | None
hoge: Optional[int]
と3パターンもの表記ゆれが発生することになります。特にNull許容型は発生頻度も高いのでプロジェクトのコーディング規約でどれを使うのか定めておきましょう。
なお公式ドキュメントではダメだよと言われている
def hoge(x: int = None) -> None:
...
この書き方ですが、どうやら3.10以降inspect.signature()
はこれをOptional[int]
と見なしてくれるらしく、型チェッカーも追随してくれないかなぁとひそかに期待しています。
★ Literal
ブラケットの中で指定したリテラル(文字列/整数)3のみを受け入れます。
from typing import Literal
x: Literal["hoge", "fuga", "piyo"]
x = "hoge" # OK
x = "foo" # NG
TSのuinon型ほど万能ではありませんが大抵のEnumを置換できるため非常に強力です。
Enumと違って反復処理ができない(x
から"hoge"
/"fuga"
/"piyo"
を取り出すことができない)のが弱点。
★ TypeGuard (3.10 ~)
要はTSのアレなのですが説明が難しいので1から解説します。
11行目でエラーが出ていますね。
l
の中身はis_str_list
関数によってstr
型であることが確定しているので我々人間からするといいんじゃないの?と思うのですが、型チェッカーくんは自作の型判定関数を理解しません。普段isinstance
で型の絞り込みが行えているのは型チェッカーがisinstance
やissubclass
に対して特別なプログラムを組んでいるからなのです4。
ということでTypeGuard
の出番なのですが…
ちょっと何言ってるか分からないですね。
押さえるべきTypeGuard
の仕様は3つです。
-
TypeGuard
を使う関数は引数を1つだけ取る - この関数は実際には
bool
値を返す - この関数が
True
を返した場合、型チェッカーは渡された引数がTypeGuard
のブラケット内に書かかれた型であると判定する
よって型チェッカーがl
の中身(elm
)をstr
型であると認識しエラーが消えたということになります。
from typing import Any, TypeGuard
def is_str_list(l: list[Any]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in l)
def hoge(l: list[int | str]):
if is_str_list(l):
for elm in l:
print(elm.lower())
★ Final
Final
がついた変数には再代入ができなくなります(強制的に定数になります)。
HOSHII_YEN: Final = 5000_0000_0000_0000
HOSHII_YEN += 1 # NG
クラス定数をサブクラスでオーバーライドすることも禁止されます。
class Hoshii:
YEN: Final[int] = 5000_0000_0000_0000
class MottoHoshii(Hoshii):
YEN = 5000_0000_0000_0000_0000 # NG
Annotated (3.9~)
事実上アノテーションの機能が型アノテーションに支配されたので、型以外のアノテーションもつけれる方法を再定義しようぜ、ということで生まれた構文です。
hoge: Annotated[type, x, y, ...]
とすることでhoge
という名前に対して型と後続のメタデータ(なんでも可、長さも自由)を付与することができるようになります。
型チェッカーが付与されたメタデータを元にいろいろ警告を出してくれるようになったりすると便利なんでしょうね。
今のところコメント以上の意味はありません🙃
★ プロトコル(一覧)
これらの型は通常の型(公称型)と異なり特定のメソッドが実装されているかどうかを元に型チェッカーが型の整合を判断します。(→Protocol)
from typing import Iterable, SupportsRound
from decimal import Decimal
def round_sum(nums: Iterable[SupportsRound]) -> int:
return sum(round(num) for num in nums)
round_sum([1, 1.3, Decimal("1.4")]) # これらの型に親子関係は存在しないが全員`__round__`メソッドを持つのでOK
>>> 3
プロトコル | 対応するメソッド | 備考 |
---|---|---|
SupportsBytes | __bytes__ |
|
SupportsInt | __int__ |
|
SupportsFloat | __float__ |
|
SupportsComplex | __complex__ |
|
SupportsAbs | __abs__ |
戻り値の型は共変 |
SupportsRound | __round__ |
戻り値の型は共変 |
SupportsIndex | __index__ |
__add__
などのプロトコルを自分で作りたい場合は同様にSupportsAdd
のような名前にすると統一感があっていいと思います。
またコレクションのABCはABCでありながらプロトコルと同様に扱うことが可能です。
from collections.abc import Sequence
def count_hoge(seq: Sequence[str]) -> int: # Sequenceは破壊的メソッドを持たないため渡したseqに副作用が起こらないことを保証できる
return seq.count("hoge")
count_hoge(["hoge", "fuga", "piyo"]) # OK
count_hoge([1, 2, 3]) # NG
count_hoge("hoge") # OK(str型はSequence[str]でもあるので)
3.9で非推奨になったもの
「int
のlist
」ように「型
の型'
」で表せる型'
のことをジェネリック型(総称型/汎用型)と言いますが、以前はこのジェネリックを型チェッカーへ通知するためにtypingからジェネリック専用の型をインポートする必要がありました。
from typing import List
any_list: list = [1, 2, "3"] # OK
int_list: List[int] = [1, 2, 3] # OK
str_list: list[str] = ["1", "2", "3"] # NG
ただこれはもう今の時代にそぐわなかろう、ということで3.9からはいよいよ元の型にも[]
(正確に言うと__class_getitem__)がサポートされます。
int_list: list[int] = [1, 2, 3] # OK!!
役目を終えたtypingのジェネリック専用型たちは**Python3.9のリリース(2020/10/05)から5年後のバージョンで削除されることが決まっている5**ので、今のうちに置き換えておきましょう。
★ Tuple / List / Dict / Set / FrozenSet
それぞれbuiltinsのtuple
/ list
/ dict
/ set
/ frozenset
に相当。
アノテーションの書き方が少し複雑なのでいくつか具体例を書きます。
int型の配列: list[int] = [1, 2, 3]
int型またはstr型の配列: list[int | str] = [1, 2, "3"]
int型3つのタプル: tuple[int, int, int] = (1, 2, 3)
int型int型str型のタプル: tuple[int, int, str] = [1, 2, "3"]
int型がいくつか入ったタプル: tuple[int, ...] = (1, 2, 3)
空のタプル: tuple[()] = ()
str型がkeyでint型をvalueとする辞書: dict[str, int] = {"alice": 1, "bob": 2, "carol": 3}
# 「1つ目の要素がint型、2つ目以降の要素がstr型の長さが不明なタプル」のようなアノテーションは書けない
# tuple[int, str, ...] ← これはダメ
# なのでその場合はこう↓
int型とstr型配列のタプル: tuple[int, list[str]] = (1, ["2", "3"])
またちょっと特殊な使い方としてGeneric[T]
は継承ができます。
例えば自作オブジェクトの配列に属性をつけたい時ってあるじゃないですか?ああいう時に
from typing import NamedTuple
class Line(NamedTuple):
min: int
max: int
class Lines(list[Line]):
@property
def bbox(self) -> Line:
return Line(min(l.min for l in self), max(l.max for l in self))
このような宣言を書くことによってLines
のインスタンスにはLine
以外の要素を追加できなくなります。
もちろん特別な属性がいらない場合は
Lines: TypeAlias = list[Line]
でいいので登場機会は少ないですが覚えておいて損はないです。(→Generic、TypeAlias)
★ Callable
collections.abcのCallableに相当。
関数を受け取ったり返したりする関数(=高階関数)を作るとき、最もメジャーな例で言うとデコレータを作るときに使います。
from typing import TypeVar
from collections.abc import Callable
from functools import wraps
R = TypeVar("R")
def with_good_morning(func: Callable[..., R]) -> Callable[..., R]:
@wraps(func)
def wrapper(*args, **kwargs):
print("おはよー!")
return func(*args, **kwargs)
return wrapper
@with_good_morning
def greet(*greets: str) -> None:
for g in greets:
print(g)
print("おやすみー!")
@with_good_morning
def greet_(afternoon: str, evening: str) -> None:
print(afternoon)
print(evening)
print("おやすみー!")
greet("こんちわー!", "こんばんわー!")
>>> おはよー!
>>> こんちわー!
>>> こんばんわー!
>>> おやすみー!
greet_("こんちわー!", "こんばんわー!")
>>> おはよー!
>>> こんちわー!
>>> こんばんわー!
>>> おやすみー!
ただ我々のような型愛好家にとってデコレータは長らく鬼門でありました。
なぜなら
デコレータをつけた関数は外部から引数の情報が見えなくなってしまうのです。
だからといってデコレータのアノテーションを
def with_good_morning(func: Callable[[P], R]) -> Callable[[P], R]:
...
にしてはgreet
とgreet_
のように引数の数が異なる関数に適用できなくなってしまいます。
そこで、3.10では以下の新しい仕組みが導入されました。
★ ParamSpec (3.10 ~)
引数のための特別な型変数です。
from typing import TypeVar, ParamSpec
from collections.abc import Callable
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def with_good_morning(func: Callable[P, R]) -> Callable[P, R]:
@wraps
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("おはよー!")
return func(*args, **kwargs)
return wrapper
@with_good_morning
def greet(*greets: str) -> None:
for g in greets:
print(g)
print("おやすみー!")
@with_good_morning
def greet_(afternoon: str, evening: str) -> None:
print(afternoon)
print(evening)
print("おやすみー!")
これで引数が見れるようになります。
悲願成就!
ちなみにfunctools.wrapsについてはこちらの記事に詳しいです。
Concatenate (3.10 ~)
ParamSpec
をConcatenate(=結合)します。
def with_good_morning(func: Callable[Concatenate[Logger, P], R]) -> Callable[P, R]:
...
とかなんとかすることによってデコレートした関数の引数を減らしたり増やしたりできるんですが、私はそもそもそういう目的でデコレータを使うべきではないと思っている6ので使いません。
Type
builtinsのtype
に相当。
Anyの説明でちょろっと使いましたがあんな感じで型そのものを受け入れる場合に使います。
def hoge(t: int):
...
def hoge(t: type[int]):
...
int
型の~と書きましたが、実際にはどちらもintとして扱えるものが対象なので例えば前者にはTrue
を、後者にはbool
を渡すことも可能です(関数内ではint
(型)として扱われます)。
受け取った型を戻り値に使いたい場合にはこうなります。
IntT = TypeVar("IntT", bound=int)
def make_int(t: type[IntT]) -> IntT:
...
(→TypeVar)
Iterator / Generator
collections.abcのIterator / Generatorに相当。
ジェネレータのアノテーションはGenerator[yieldで返す値の型, sendで受ける値の型, returnで返す値の型]
ですが、send
もreturn
も使わない場合にはGenerator[yieldで返す値の型, None, None]
となり、これは事実上Iterator[yieldで返す値の型]
と同じです。
どちらで書くかは好みのレベルですがプロジェクト内でバラバラになると気持ち悪いので規約なりで定めておくといいでしょう7。
from collections.abc import Generator, Iterator
def fibonacci_max(max: int) -> Generator[int, None, str]:
a, b = 0, 1
while a <= max:
yield a
a, b = b, b + a
return "done"
def fibonacci_n(n: int) -> Iterator[int]: # -> Generator[int, None, None] でも○
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, b + a
その他(一覧)
ユーティリティ
型にまつわる様々な便利機能を提供してくれるオブジェクトたちです。
★ TypeVar
型を入れる変数、型変数です。
これを引数の型に指定することで呼び出し時に型が決まる汎用的(=ジェネリック)な関数/クラスを作ることができます。
from typing import TypeVar
T = TypeVar("T") # なんでも取れる
S = TypeVar("S", str, int) # strかintを取れる
IntT = TypeVar("IntT", bound=int) # intかintのサブクラスを取れる
from typing import TypeVar, TypeAlias
T = TypeVar("T", str, int)
U: TypeAlias = str | int
def hoge(x: T, y: T): # ジェネリック関数
...
def fuga(x: U, y: U): # 普通の関数
...
hoge(0, 0) # OK
fuga(0, 0) # OK
hoge("", "") # OK
fuga("", "") # OK
hoge(0, "") # NG!!
fuga(0, "") # OK
共変や反変な型を使いたい場合はそれぞれcovariant
/contravariant
にTrue
を渡します8。
from typing import TypeVar
T_co = TypeVar("T_co", covariant=True)
T_contra = TypeVar("T_contra", contravariant=True)
AnyStr
AnyStr == TypeVar("AnyStr", str, bytes)
です。なんかよく使うから用意したらしい。
TypeAlias (3.10 ~)
複雑になってしまったアノテーションにはエイリアスを設定できるのですが、その書き方がグローバル変数への代入と区別がつき辛いという意見がありエイリアスをキチンと明示するために導入されました。
from typing import TypeAlias
Mutables = [list, set, dict] # グローバル変数への代入
Mutable: TypeAlias = list | set | dict # 型エイリアス
def hoge(m: Mutable):
...
ちなみにこの型エイリアスもジェネリックにできます。
一番わかりやすい例としてOptional(のようなもの)を自前で実装してみましょう。
from typing import TypeVar, TypeAlias
T = TypeVar("T")
Optional: TypeAlias = T | None
def hoge(x: Optional[int] = None):
...
def fuga(x: Optional[T] = None) -> T: # エイリアスを使ってジェネリック関数も作れる
...
とは言え所詮エイリアスはエイリアスであって型ではない(つまりisinstance
による判定ができない)ので過信は禁物です。あくまでも簡易的な方法として。
NewType
サブクラスのように振舞うサブクラスではないなにかを生み出します。
得られる計算コストの減少量に対してコードの複雑化というデメリットが大きすぎるので素直にサブクラスを作ってください。
★ Generic
ジェネリックな型を1から自作したい時に継承するABCです。
from typing import Generic, TypeVar
from dataclasses import dataclass
T = TypeVar("T")
@dataclass
class Hoge(Generic[T]):
x: T
def hoge(self, t: T) -> T:
...
hoge: Hoge[int] = Hoge(0) # OK
hoge_: Hoge[str] = Hoge(0) # NG
hoge.hoge(0) # OK
hoge.hoge("") # NG
Generic[T]
を継承するという一見ぎょっとするような書き方ですが、なんとこの継承方法は全てのジェネリック型で共通です。
またその際型変数に型変数を代入することも出来るので
from typing import TypeVar
T = TypeVar("T")
# collections.Counterと同じ
class Counter(dict[T, int]):
...
x: Counter[str]
x = Counter({"hoge": "h", "fuga": "f", "piyo": "p"}) # NG
x = Counter({"hoge": 1, "fuga": 2, "piyo": 3}) # OK
このように型の一部だけを固定、ということが可能です。
★ Protocol
構造的部分型を定義するための基底クラスです。
from typing import Protocol
class Greeter(Protocol):
def greet(self, name: str) -> str:
...
class Caller:
def greet(self, name: str) -> str:
return f"{name}!おはよー!"
def greet(g: Greeter) -> None:
print(g.greet("alice"))
greet(Caller()) # OK
Greeter
とCaller
に直接の継承関係はありませんが、Greeter
がProtocol
を継承していることによって型チェッカーはCaller
をGreeter
であると見なせるようになります。
(それがなぜ嬉しいのかについては前回の記事を読んでいただけると嬉しいです。)
もう少し具体的に説明するとプロトコルは
- メソッド名
- 引数名
- 引数の型
- 戻り値の型
の4つが一致するメソッドを持つクラスを派生型と見なします9。よって
class Repeater:
def greet(self, n: int) -> str:
return "おはよー!" * n
greet(Repeater()) # NG
引数名と引数の型が異なるこれはダメです。
ただし逆に言えばメソッド名が一致していても他の3つが一致しなければ別物として扱われる(関係ないクラスを巻き込まない)ので、PEP544にあるように複雑なコールバック関数を複雑な__call__メソッドを持つプロトコルとして定義してしまうことも可能です。
from typing import Protocol, Optional
class Combiner(Protocol):
def __call__(self, *vals: bytes, maxlen: Optional[int] = None) -> list[bytes]:
...
def hoge(cb: Combiner): # これをCallableで表現しようと思ったら大変
...
(→Callable)
@runtime_checkable
実行時にチェックできる。
プロトコルを@runtime_checkable
でデコレートするとisinstance
/issubclass
が反応するようになります。
from typing import Protocol, runtime_checkable
@runtime_checkable
class Greeter(Protocol):
def greet(self, name: str) -> str:
...
class Caller:
def greet(self, name: str) -> str:
return f"{name}!おはよー!"
issubclass(Caller, Greeter)
>>> True
注意点がありまして、型チェッカーはメソッドのシグネチャ全ての一致を確認するのに対してプロトコルに対するruntime_checkはメソッド名の一致しか確認しません。よって引数名と引数の型が異なるRepeater
は
class Repeater:
def greet(self, n: int) -> str:
return "おはよー!" * n
issubclass(Repeater, Greeter)
>>> True
Greeter
の派生型になります。大嘘です。なんか上手く言葉にできないんですが、構造的部分型ってそういう風に使うものじゃないような気がするのでこれでいいんだと思います…。
そしてそういう感じなので私は@runtime_checkable
を使わないことをおすすめします。
ClassVar
from typing import ClassVar
class Hoge:
x: ClassVar[int] = 1 # これはクラス変数!
y: int # こっちはインスタンス変数
こんな感じでクラスを定義するとインスタンスでx
の値を変更しようとした際にエラーを出してくれます。
hoge = Hoge()
hoge.y = 1 # OK
hoge.x = 2 # NG
Hoge.x = 3 # OK
と、いうのが本来の(公式ドキュメントに書かれている)目的なのですが、私は事実上@dataclassにクラス変数を通知するためのものだと思っています。
from typing import ClassVar
from dataclasses import dataclass
@dataclass
class Hoge:
x: ClassVar[int] = 1
y: int
hoge = Hoge(3)
hoge.x
>>> 1
hoge.y
>>> 3
大変かしこいですね10。
★ NamedTuple
collections.namedtupleという名前で要素にアクセスできるtuple
に型を追加して最強になった姿です。
from typing import NamedTuple
class RGB(NamedTuple):
r: int
g: int
b: int
RGB("hoge", "fuga", "piyo") # NG
saxe_blue = RGB(65, 139, 137) # OK
saxe_blue[0]
>>> 65
saxe_blue.b
>>> 137
面倒なクラス宣言をすっ飛ばせて大変便利ですがあくまでもtuple
なのでtuple
が欲しい時だけ使うようにしましょう。
tuple
である必要がないのであれば@dataclassで十分です。
TypedDict
項目の存在と型を型チェッカーによって強制されるdict
を作ります。
でもそれって悪意のある言い方をすれば属性へのアクセスが面倒になった構造体だと思うんですけど…
from typing import TypedDict
from dataclasses import dataclass
class Point2D(TypedDict): # 実行時はdictになる
x: int
y: int
label: str
@dataclass
class Point3D:
x: int
y: int
z: int
label: str
td: Point2D
td = {"x": 1, "y": 2} # NG
td = {"x": 1, "y": 2, "label": 3} # NG
td = {"x": 1, "y": 2, "label": "ok"} # OK
td = Point2D(1, 2, "ng") # NG
td = Point2D(x=1, y=2, label="TypedDict") # OK
td["label"]
>>>TypedDict
dc: Point3D
dc = Point3D(1, 2, 3, "ok") # OK
dc = Point3D(x=1, y=2, z=3, label="dataclass") # OK
dc.label
>>>dataclass
一応TypedDict
はコンストラクタ引数が強制的にキーワード専用になること、dict
のメソッドを扱えること、dictが持つ属性以外の属性が存在しないことを保証できることが@dataclassにはないメリットですが、このメリットが美味しく見える場面はかなり限定的です。
使い分けを意識しなければいけない億劫さを考えると一律使わないで問題ないと思います。
cast
どうしても正規の方法では型チェッカーに型を認識させられない場合の緊急避難として変数の型情報を上書きするための関数でした。
3.10からはTypeGuardを使うのでcast
の使用は禁止しましょう。
from typing import Any, cast
def is_str_list(l: list[Any]) -> bool:
return all(isinstance(x, str) for x in l)
def hoge(l: list[int | str]):
if is_str_list(l):
for elm in l:
elm = cast(str, elm) # ここで型チェッカーから見たelmの型がint | strからstrに変化する
print(elm.lower())
★ @overload
なんちゃってオーバーロードです。
合併型や型変数では対応しきれないような複雑な引数と戻り値の組み合わせを持つ関数にはこっちを使います。
from typing import overload
@overload
def hoge() -> None:
...
@overload
def hoge(x: int) -> tuple[int, str]:
...
@overload
def hoge(x: bytes) -> str:
...
def hoge(x=None): # 実際の分岐はダラダラやるしかない(_hoge_int()/_hoge_bytes()みたいな関数を作ってもいいですが…)
if x is None:
return x
if isinstance(x, int):
return x, str(x)
return str(x)
エディタでhoge
関数を見てみると…
キチンと3パターンの情報が載っています。
@final
Finalのデコレータ版です。
メソッドのオーバーライドとクラスの継承を禁止します。
from typing import final
class Hoge:
@final
def hoge(self): # @finalのついたメソッドをオーバーライドすることは出来ない
...
class HogeHoge(Hoge):
def hoge(self): # NG
...
@final
class Fuga: # @finalのついたクラスを継承することは出来ない
...
class FugaFuga(Fuga): # NG
...
@no_type_check / @no_type_check_decorator
型ではないアノテーションを書きたい時に使うものだったので3.9以降は使いません。
代わりに → Annotated
@type_check_only
スタブファイル(.pyi)にプライベートクラスを書くために使います(.pyの中では使えません)。
公式ドキュメントにもある通りこのデコレータを使うような状況はそもそも設計として推奨されないので存在自体を忘れてしまって大丈夫です。
おわりに
以上Introspection helpers(型アノテーションを解釈するためのヘルパー関数群)を除いた全オブジェクトの解説でした。
Pythonの型システムは3.9を迎えてほぼ完成したなと去年は思っていたのですが、こうしてまとめてみると3.10でも大きく進化していることが分かります。特にParamSpecやTypeGuardなどは待望だった方も多いのではないでしょうか?
使わないものをバッサリ切ったりでかなり私見(というか思想)の強い解説にはなっていますが、この記事が世のPythonコードを堅牢に、そして綺麗にすることに貢献できれば幸いです。
-
型チェッカーとかあるいはそういったライブラリを作る人々 ↩
-
普通に
int?
じゃダメだったんでしょうか? ↩ -
その他に
True
/False
/None
を使うことが出来ますが解説が蛇足っぽかったので割愛 ↩ -
なのでよく見る
if type(hoge) is type:
も型の絞り込みには使えません ↩ -
PEP 585より引用; The deprecated functionality will be removed from the typing module in the first Python version released 5 years after the release of Python 3.9.0. ↩
-
コードが複雑になるので ↩
-
一応参考程度に私は
Iterator[yield]
派です ↩ -
ここの解説を入れるとそれだけで1記事分になってしまうのですみませんが省略させてください ↩
-
公式ドキュメントではこれをシグネチャと呼んでいるのでPythonの言うシグネチャには戻り値の型も含まれるということが分かりました ↩
-
それはそうとアノテーションが実行に影響を与えるのは思想的にどうなんだろうという疑問はあります ↩