Pythonの最新の型エイリアス(TypeAliasType)でisinstanceを使うとエラーになるよ
はじめに
Python3.12 から導入された新しい型エイリアスの定義方法に乗り換えてみたところ、isinstance
関数の処理がエラーで落ちたのでその対処法を調べました。
忙しい人向け
__value__
プロパティを使うとエラー回避できます。
type U = int | str
val = "hoge"
# これはエラー!
isinstance(val, U) # TypeError
# これならエラーなく型の確認ができる!!
isinstance(val, U.__value__) # True
Python の型エイリアス
Python では、TypeAlias
を使うことで任意の型に別名(エイリアス)を付けることができます。
from typing import TypeAlias
# 型エイリアスの定義
T: TypeAlias = int | str
# 以下のように書いてもpythonが型エイリアスだと認識してくれるので同じ意味
T = int | str
ただし、紹介しておいていきなりですが、上記のTypeAlias
を使った型エイリアスの定義は python3.12 から非推奨になっています。
バージョン 3.12 で非推奨: TypeAlias is deprecated in favor of the type statement, which creates instances of TypeAliasType and which natively supports forward references. Note that while TypeAlias and TypeAliasType serve similar purposes and have similar names, they are distinct and the latter is not the type of the former. Removal of TypeAlias is not currently planned, but users are encouraged to migrate to type statements.
上記の引用文のとおり、python3.12 から推奨されている型エイリアスの定義方法が以下のようなtype
を使った定義方法です。
# python3.12から推奨される型エイリアスの定義
type T = int | str
書き方だけをぱっと見ても、新しい記法の方が型を定義しているのが分かりやすくて良さそうです。
(あと個人的には TypeScript の型定義の構文と似ているのも気に入った)
新しい型エイリアスで isinstance による型の確認はできません
タイトルの件ですが、
型の一致を確認するisinstance
関数の第二引数に対して、最新の型エイリアスを使うとエラーが発生します。
from typing import TypeAlias
OldAlias: TypeAlias = str | int
type NewAlias = str | int
val = "hoge"
# 古い型エイリアスではisinstanceを使ってもエラーにならない
isinstance(val, OldAlias)
# 新しい型エイリアスではisinstanceを使うとエラーになる
isinstance(val, NewAlias) # TypeError
なぜ??
上記の事象を理解するには、まず型エイリアスの新旧の定義方法で何が異なるのかを理解する必要があります。
まず、古い方法(TypeAlias
を使った方法)では型エイリアスは紐づけた型に応じた型オブジェクトになります。
from typing import TypeAlias
H: TypeAlias = str
print(type(H)) # <class 'type'>
J: TypeAlias = int | str
print(type(J)) # <class 'typing.UnionType'>
K: TypeAlias = list[int]
print(type(K)) # <class 'types.GenericAlias'>
一方、新しい方法(type
を使った方法)では型エイリアスはTypeAliasType
というオブジェクトになります。
type H = str
print(type(H)) # <class 'typing.TypeAliasType'>
type J = int | str
print(type(J)) # <class 'typing.TypeAliasType'>
type K = list[int]
print(type(K)) # <class 'typing.TypeAliasType'>
つまり、TypeAliasType
を使って定義された型エイリアスは型オブジェクトではないため、isinstance
関数の第二引数には渡せないという訳です!
ちなみに、TypeAliasType
自体は型オブジェクトなのでisinstance
関数の第二引数に渡すことができます。
from typing import TypeAliasType
type J = int | str
# これはエラーにならない
isinstance(J, TypeAliasType) # True
# TypeAliasType自体は型オブジェクト
print(type(TypeAliasType)) # <class 'type'>
理屈はわかった、でもなんでこのような仕様になってるの?
上記のようなTypeAliasType
の仕様はPEP 695で策定されたものです。
ただ、この仕様に困惑?している人は他にもいるようで、python のコミュニティフォーラムでも既に議論がなされていました。
参考:Run-time behaviour of `TypeAliasType
上記を読んでいくと、
あるユーザーからの投稿でTypeAliasType
による型エイリアスもisinstance
でサポートされるようにしたい、という声がありましたが、
CPython の Core Developer がそれはすべきでは無い、と意見を述べています。(参考)
その理由としては、TypeAliasType
による型エイリアスは以前のものと比べ以下のような機能的な違いがあるためだそうです。
- 型パラメータの明確なスコープの提供
- 古い記法と比べ、
type SomeGeneric[T] = ...
のようなジェネリクス型が含まれた場合にスコープが明確になるそうです。(正直あまり理解できていない。。。)
- 古い記法と比べ、
- 遅延評価
-
type X = <something>
が実行された時点では<something>
の部分は評価されず、<something>
の部分が明示的に必要になった場合に評価されるそうです。 - このような機構をもつことで、再帰型の実装などがより簡単になるそうです。
-
これらの機能とisinstance
関数との相性が悪く、TypeAliasType
による型エイリアスがisinstance
関数でサポートされない理由になっているようです。
対応方法は無いの??
一番簡単な対応方法としては、TypeAliasType
を使わずに古い記法のTypeAlias
を使うことです。
ただ、TypeAlias
は python3.12 から非推奨になっているのであまり気乗りする方法では無いかもしれません。
実は、もう1つ対応方法があり、それはTypeAliasType
オブジェクトの__value__
プロパティを呼ぶ方法です。(参考)
上記で説明したとおり、TypeAliasType
は通常遅延評価されますが、__value__
プロパティを呼ぶことで評価が即時実行され型オブジェクトが取得できる、という訳です。
type U = int | str
val = "hoge"
# これならエラーなく型の確認ができる!!
isinstance(val, U.__value__) # True
ただ、見た目からも分かる通り若干TypeAliasType
のハック的な使い方になる点と、
ジェネリクス型と組み合わせた際に__value__
を使った方法でもうまくいかないケースがあるようなので、注意が必要です。
さいごに
TypeAliasType
を使った型エイリアスでisinstance
関数を使う方法について説明しました。
最近の Python ではかなり型のサポートが充実してきましたが、この記事のような一見わかりにくい挙動もまだまだあるなと感じました。
自分自身、型など何も意識せずに 5 年くらいコードを書いてきた身ですが、
TypeScript を使い始め型の魅力に取り憑かれてからは、 Python でも型が無いと不安になる体になってしまいました。
(じゃあ Python じゃなくて最初から型がある言語を使えって??、、、、、うん、それはそう。)
世の中の Python 型絶対つけるマンの方々にとってこの記事が少しでも参考になれば幸いです。
Discussion