11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python3.11~3.13で追加された主な型表現まとめ

Last updated at Posted at 2024-10-28

3.10までのまとめ ↓

知識のアップデートやっていきます。

3.11

可変長ジェネリクス

複数の値を持つことができる型変数TypeVarTupleが追加されました。
簡単な例をあげると…

from typing import TypeVarTuple

Ts = TypeVarTuple("Ts")

def variadic_generics(*args: *Ts) -> tuple[*Ts]:
    return args

のような関数を定義した場合、

ret1 = variadic_generics(1, "hoge")
ret2 = variadic_generics(2, "fuga", False)

ret1tuple[int, str]に、ret2tuple[int, str, bool]にそれぞれ正しく推論されますよという感じです。

が、この構文は3.12で早速使わなくなりました。後述

一部のキーが省略可能なTypedDict

厳格な型のdictがほしい場合に用いられるTypedDictは従来全てのキーが存在することを強制されていましたが、3.11からは省略可能なキーを設定することができるようになりました。

従来
from typing import TypedDict

class Movie(TypedDict):
   title: str
   year: int

m1: Movie = {"title": "Black Panther", "year": 2018}  # OK
m2: Movie = {"title": "Star Wars"}                    # Error
yearはなくてもいい場合
from typing import TypedDict, NotRequired

class Movie(TypedDict):
   title: str
   year: NotRequired[int]

m1: Movie = {"title": "Black Panther", "year": 2018}  # OK
m2: Movie = {"title": "Star Wars"}                    # OK!

とは言えデータクラスで済む場合は@dataclassで問題ありません。

データクラス版
@dataclass
class Movie:
   title: str
   year: int | None = None

m1 = Movie("Black Panther", 2018)  # OK
m2 = Movie("Star Wars")            # OK

あくまでもdictがほしい場合にのみ使用しましょう。

Self型

代替コンストラクタなど自分自身を返すメソッドの戻り値に雑にアノテーションをつけられなかったもやもやが解消されました。

from dataclasses import dataclass
from typing import Self

@dataclass
class MyInt:
    val: int

    @classmethod
    def fromhex(cls, s: str) -> Self:
        return cls(int(s, 16))

ちなみに通常メソッドの第一引数selfSelf、クラスメソッドのclstype[Self]型ですがそこまでするかどうかはプロジェクト次第ですね。私はやらないと思います1

文字列リテラル型

セキュリティの観点などからリテラルではない文字列を弾きたいような場面で使えるLiteralString型が追加されました。

from typing import LiteralString

def run_query(sql: LiteralString) -> ...:
    ...

run_query("SELECT * FROM students")  # OK
run_query(input())                   # Error
run_query(                           # f文字列もstr型のためError
    f"SELECT * FROM students WHERE name = {input()}"
)

データクラスへの変換

データクラスのようなインターフェースを持つサードパーティ製ライブラリのクラスをデータクラスと同じように型検査できるようにするためのデコレータ@dataclass_transform()が追加されました。

そもそものペインなど詳しくは ↓

ボトム型

従来PythonではNoReturnを転用してボトム型を表現していましたが、正式にNever型が使えるようになりました。

from typing import Never

def never_call_me(arg: Never) -> None:
    pass

def int_or_str(arg: int | str) -> None:
    match arg:
        case int():
            print("It's an int")
        case str():
            print("It's a str")
        case _:
            never_call_me(arg)  # OK

3.12

型パラメータ構文

3.12からは型エイリアスの書き方ジェネリクスの書き方が大きく変わります。

まずtypeが追加されたことにより型エイリアスがこのように書けるようになりました。

type Point = tuple[float, float]

更にこれをジェネリックにしたい場合はこうです。

type Point[T] = tuple[T, T]

一番のポイントは型変数を事前に定義しなくてもよいことで、これによってTypeVarTypeVarTupleParamSpecなどをインポートする必要がなくなりました。

用例
type IntFunc[**P] = Callable[P, int]                # ParamSpec
type LabeledTuple[*Ts] = tuple[str, *Ts]            # TypeVarTuple
type HashableSequence[T: Hashable] = Sequence[T]    # TypeVar with bound
type IntOrStrSequence[T: (int, str)] = Sequence[T]  # TypeVar with constraints

関数やクラスについても同様の書き方でジェネリックにすることが可能で、特にクラスについては明示的にGenericを継承する必要もなくなりました。

やったぜ
from typing import Iterable

def max[T](args: Iterable[T]) -> T:
    ...

class MyList[T]:
    def __getitem__(self, index: int, /) -> T:
        ...

    def append(self, element: T) -> None:
        ...

なおtypeはソフトキーワード(特定の文脈でのみ予約語)だそうです。

可変長キーワード引数の型付け

def print_(**kwargs: str):
    print(kwargs)

print_(hoge="hoge", fuga="fuga")  # OK
print_(hoge="hoge", piyo=1)       # `piyo`の値がstr型ではないためError

このように可変長キーワード引数は全ての値が同一の型でなければ正しくアノテーションすることができないので何とかしたいと。

そこまでするなら素直に構造体を受け取ればいいのに… と思いPEPを見にいったところ

As described in the Intended Usage section, using **kwargs is not always the best tool for the job. Despite that, it is still a widely used pattern. As a consequence, there has been a lot of discussion around supporting more precise **kwargs typing and it became a feature that would be valuable for a large part of the Python community. This is best illustrated by the mypy GitHub issue 4441 which contains a lot of real world cases that could benefit from this propsal.

要約: **kwargsを使うことがベストじゃないのは分かってるけどむっちゃ使われちゃってるししょうがない

などと書かれており悲しい気持ちになりました。紹介は割愛します。

明示的オーバーライド

@overrideデコレータが追加され、メソッドのオーバーライドを明示できるようになりました。

from typing import override

class Base:
    def log_status(self) -> None:
        ...

class Sub(Base):
    @override
    def log_statas(self) -> None:  # typoによりオーバーライドできてないためError
        ...

うっかりを防ぐ意味でも使った方がいいと思います。

3.13

デフォルト型引数

ジェネリクスにデフォルトの型を指定できるようになりました。

from dataclasses import dataclass
from typing import TypeVar

ET = TypeVar("ET", bound=Exception, default=SyntaxError)

@dataclass
class MyErrorEvent[ET]:
  error: ET
  type: str

ただし現在サポートされているのはTypeVar / TypeVarTuple / ParamSpecの引数に直接渡してあげる方法のみで、

from dataclasses import dataclass

@dataclass
class MyErrorEvent[T: Exception = SyntaxError]:
  error: T
  type: str

このようにはまだ書けないようです。

一部の値が読み取り専用なTypedDict

from typing import TypedDict, ReadOnly

class Movie(TypedDict):
   title: ReadOnly[str]
   year: int

def mutate_movie(m: Movie) -> None:
   m["year"] = 1999           # OK
   m["title"] = "The Matrix"  # Error

TypedDictの機能が拡張されまくっているのはどういう理由からなんでしょうか?一部だけReadOnlyにしたいという需要もよく分かりませんし普通に@dataclassでイミュータブルなオブジェクトを作ればいいだけのように思います。

from dataclasses import dataclass

# `frozen`を指定することで各値が変更不可になる
@dataclass(frozen=True)
class Movie:
   title: str
   year: int

def mutate_movie(m: Movie) -> None:
   m.year = 1999           # Error
   m.title = "The Matrix"  # Error

賢くなったユーザー定義型ガード

従来のTypeGuardは元々の型情報が考慮されないため、isinstance()のような分岐を書こうとしてもうまく型を絞り込めないということがよくありました。

isinstanceを使った場合
def typeguard(x: str | int) -> None:
    if isinstance(x, str):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # int
TypeGuardを使った場合
from typing import TypeGuard

def is_str(x: object) -> TypeGuard[str]:
    return isinstance(x, str)

def typeguard(x: str | int) -> None:
    if is_str(x):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # str | int ← ?

新しく追加されたTypeIsではこの点が解消され、isinstance()などと同じように型を絞り込むことが可能になっています。

TypeIsを使った場合
from typing import TypeIs

def is_str(x: object) -> TypeIs[str]:
    return isinstance(x, str)

def typeguard(x: str | int) -> None:
    if is_str(x):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # int ← !

もう少し詳しい挙動 ↓

from typing import TypeGuard, TypeIs, final, reveal_type

class Base: ...
class Child(Base): ...
@final
class Unrelated: ...

def is_base_typeguard(x: object) -> TypeGuard[Base]:
    return isinstance(x, Base)

def is_base_typeis(x: object) -> TypeIs[Base]:
    return isinstance(x, Base)

# TypeGuard[Base]がTrueの場合xはBase型として扱われる
def use_typeguard(x: Child | Unrelated) -> None:
    if is_base_typeguard(x):
        reveal_type(x)  # Base
    else:
        reveal_type(x)  # Child | Unrelated

# TypeIs[Base]がTrueの場合xは(Child | Unrelated)型とBase型との交差をとる
def use_typeis(x: Child | Unrelated) -> None:
    if is_base_typeis(x):
        reveal_type(x)  # Child
    else:
        reveal_type(x)  # Unrelated

なおdef guard(x: TypeA) -> TypeIs[TypeB]におけるTypeBTypeAのサブタイプである必要があるため、例えば

def is_str_list(val: list[object]) -> TypeIs[list[str]]:  # Error
    return all(isinstance(x, str) for x in val)

このように書くとエラーが出ます。この場合は従来通りTypeGuardを使用しましょう。

おわりに

というわけで3.13までのまとめでした。個人的にはジェネリクスの書き方が変わったのがかなりいいなと思っているので積極的に広めていきたいです。逆にTypedDict周りはちょっとやりすぎな気がしますね…。

それではまた3.16でお会いしましょう。

  1. 自明すぎるので

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?