検索
連載

Python 3.10の新機能:型ヒントに関連する新機能Python最新情報キャッチアップ(2/2 ページ)

Pythonで静的な型付けを行いながらプログラミングを行うために使える型ヒントに関連する機能がPython 3.10でどのように強化されたかを紹介する。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
前のページへ |       

明示的な型エイリアス

 Python 3.9までの型エイリアスは非常にシンプルなもので、モジュールのトップレベルで変数に型を代入するだけでよかった(Python 3.10でも従来の型エイリアスはこれまでと全く同じ形で使える)。以下に例を示す。

Url = str  # 型エイリアス
atmarkit: Url = 'https://atmarkit.itmedia.co.jp'

型エイリアスその1(Python 3.9)

 この例では、Urlはstr(文字列型)のエイリアスとなっている。型ヒントの大本を定義しているPEP 484では、ユーザー定義型と同様に、型名を構成する単語の1文字目を大文字にすることが推奨されているので、ここでは型エイリアスを「Url」としている。

 型エイリアスはシンプルだが、分かりにくい面や不便な面もある。

IntList = list[int]

型エイリアスその2(Python 3.9)

 IntListはint型の値を要素とするリスト(list[int])を表す型エイリアスだが、これは代入の右辺が型ヒントとして適切なものだからでしかない。そうでなければ、型エイリアスの宣言としたつもりでも、実際には単なる変数への値の代入となってしまう。

 型ヒントは文字列の形で、まだ定義されていない型を参照(前方参照)することも可能だ。

def foo() -> 'MyClass':
    return get_instance()

class MyClass:
    pass

def get_instance() -> MyClass:
    return MyClass()

未定義の型の前方参照(Python 3.9)

 このコードでは、foo関数の戻り値の型として、その定義の時点では未定義のクラス「MyClass」を文字列の形で指定している。これはPython 3.9でも問題なく解釈される。だが、これと同じことを型エイリアスで行うことは許されていない。

MyType = 'MyClass'  # 型エイリアス? 文字列?

def foo() -> MyType:
    return get_instance()

class MyClass:
    pass

def get_instance() -> MyClass:
    return MyClass()

「MyType = 'MyClass'」は型エイリアスの定義ではなく、変数への文字列の代入(Python 3.9)

 先頭にある「MyType = 'MyClass'」は型エイリアスを宣言しているようだが、Python 3.9ではこれは単なる変数への文字列の代入としてしか扱われない。そのため、このコードの型チェックはエラーを発生させるはずだ。

型エイリアスを使って前方参照できない
型エイリアスを使って前方参照できない

 このように分かりにくかったり、あいまいだったり、不便であったりする点があったことから、Python 3.10では型エイリアスを明示的に宣言するためのTypeAliasが導入された。TypeAliasを使った型エイリアスの宣言は「エイリアス: TypeAlias = 型」「エイリアス: TypeAlias = 型として適切な内容の文字列」の形で記述する。最初の例なら次のようになる。

from typing import TypeAlias

Url: TypeAlias = str

TypeAliasを使った型エイリアスの宣言その1(Python 3.10)

 また、先ほどの前方参照の例であれば次のようになる。こちらでは文字列を使って型エイリアスを宣言している点に注意してほしい。

from typing import TypeAlias

MyType: TypeAlias = 'MyClass'

def foo() -> MyType:
    return get_instance()

class MyClass:
    pass

def get_instance() -> MyClass:
    return MyClass()

TypeAliasを使った型エイリアスの宣言その2(Python 3.10)

 ただし、こちらの方法だと、型チェック時にはMyTypeは型エイリアスとして確かに機能するが、実際に格納されているのは文字列だ。そのため、「MyType()」のようなコードは動作しない点には注意しよう。

ユーザー定義の型ガード

 最後にユーザー定義の型ガードについても見ておこう。まずは以下のコードを見てほしい。

from typing import Any

class Foo:
    pass

def print_foo(x: Foo) -> None:
    print(x.__class__.__name__)

def is_foo(x: Any) -> bool:
    if isinstance(x, Foo):
        return True
    return False

mylist: list[Foo | str] = [Foo(), 'string']

for item in mylist:
    if is_foo(item):
        print_foo(item)
    else:
        print('not Foo')

ユーザー定義の型ガードを使っていないコード(Python 3.10)

 このコードはリストの要素がFooクラスのインスタンスか、文字列かで処理を変更するものだ。print_foo関数はFoo型のインスタンスだけを受け取り、そのクラス名を画面に出力する。is_foo関数は任意の型の値を受け取って、その型がFooであればTrueを、そうでなければFalseを返す。変数mylistはFooクラスのインスタンスか文字列を要素とする。

 最後のfor文の内部では、is_foo関数の戻り値を基に処理を分岐しているが、実はこのコードは型チェックに引っかかる。というのは、リストの要素の型がFooかstrなのかの絞り込みができていないからだ。

型チェックに失敗する
型チェックに失敗する

 実際には「is_foo(item)」を「isinstance(item, Foo)」に変更することで、その結果がTrueであれば、itemの型がFooであると絞り込める。


 何らかのオブジェクトの型を推測する際に、isinstance関数のように型の絞り込みに使える機構のことを「型ガード」という。では、is_foo関数の内部でもisinstance関数を使ってTrue/Falseを返すようにしているのに、なぜ型ガードとしてこれが機能していないかというと、これが型ガードとして機能することを型チェッカーに知らせていないからだ。

 そうできるように、Python 3.10ではtypingモジュールにTypeGuardが追加された。先ほども見たように、対象の型を調べる関数は多くの場合、「is」で始まる名前を持ち、ブーリアン値(TrueかFalse)を返す。型ガードとするには、この戻り値の型をTypeGuardにして、そこに判定する型を指定する。関数がTrueを返せば、対象の型は指定した型であり、Falseを返せばそうではないことを意味する。

 このことを利用すると、上記のis_foo関数は次のようになる。

from typing import Any, TypeGuard

# 省略

def is_foo(x: Any) -> TypeGuard[Foo]:
    if isinstance(x, Foo):
        return True
    return False

is_foo関数を型ガードとして使えるように修正したコード(Python 3.10)

 is_foo関数の戻り値の型をTypeGuard[Foo]としたので、この関数がTrueを返せば、引数として渡した値の型はFooであり、Falseを返せばそうでないということを型チェッカーが判断できるようになった。そのため、以下のように型エラーが発生しなくなる。

is_foo関数は引数の型がFooかどうかを調べる型ガードなので、型エラーがなくなった
is_foo関数は引数の型がFooかどうかを調べる型ガードなので、型エラーがなくなった

「Python最新情報キャッチアップ」のインデックス

Python最新情報キャッチアップ

Copyright© Digital Advantage Corp. All Rights Reserved.

前のページへ |       
[an error occurred while processing this directive]
ページトップに戻る