nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonのジェネレータ関数の返り値の型ヒントを考える

はじめに

ニュージェネレーションズ! nikkieです。

Python使いのみなさん、ジェネレータ使ってますか?
私は多用しています、もう手放せません!
そんなジェネレータ(を返す関数)の型ヒントについてアウトプットします。

目次

ジェネレータ関数って、何よ?

端的に言えば、yieldを持った関数です。

def generator_function():
    yield "👩<200d>🎨"
    yield "🐯"
    yield "🐟"

用語集を参照しましょう。
https://docs.python.org/ja/3/glossary.html#index-19

generator iterator を返す関数です。 通常の関数に似ていますが、 yield 式を持つ点で異なります。

ジェネレータ関数の返り値が ジェネレータイテレータ
https://docs.python.org/ja/3/glossary.html#term-generator-iterator

generator 関数で生成されるオブジェクトです。

>>> generator_iterator = generator_function()
>>> next(generator_iterator)
'👩\u200d🎨'
>>> next(generator_iterator)
'🐯'
>>> next(generator_iterator)
'🐟'
>>> next(generator_iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

ジェネレータには日々Pythonを書く中で助けてもらっていますが、ここでは詳細には立ち入りません1
今回はジェネレータ関数の返り値(=ジェネレータイテレータ)をどう型ヒントするかを考えます。

現時点の私の結論

  • ジェネレータ関数の返り値はcollections.abc.Generatorで型ヒントするのがいいのではないか
    • collections.abcの型が型ヒントに使えるようになるのはPython 3.9からなので、古いPythonではfrom __future__ import annotationsも合わせて使う
    • ジェネレータ関数と明示できるので、個人的にはこちらを使いたい
  • collections.abc.Iteratorで型ヒントしてもよいかも
    • mypyのドキュメントに例示されている
    • []の指定の仕方がうるさくない

詳しく述べていきます。

動作環境

typing.Generator

型ヒントをサポートするモジュールtypingに、ジェネレータ関数の型ヒントに使う型Generatorがあります。
https://docs.python.org/ja/3/library/typing.html#typing.Generator

ジェネレータはジェネリックGenerator[YieldType, SendType, ReturnType] によってアノテーションを付けられます。

先のジェネレータ関数の型ヒントは以下のようになります。

from typing import Generator


def generator_function() -> Generator[str, None, None]:
    yield "👩<200d>🎨"
    yield "🐯"
    yield "🐟"

もしジェネレータが値を返すだけの場合は、 SendTypeReturnType に None を設定してください:

collections.abc.Generator

typing.Generatorのドキュメントには、実は非推奨という記載があります。

バージョン 3.9 で非推奨: collections.abc.Generator now supports subscripting ([]).

Python 3.9からcollections.abc.Generator[]をサポートするようになった2ので、typing.Generatorは非推奨とのことです。

私は作ったプログラムについて、継続してPythonのバージョンを上げていきたいと考えています。
今使っているPythonのバージョンが3.7や3.8だとしても、それは3.9, 3.10と上げたいです。
将来Python 3.9に上げたときにtyping.Generatorという型ヒントをすべて書き換えるのが面倒なので、今の私はcollections.abc.Generatorを選択しています。

https://docs.python.org/ja/3/library/collections.abc.html#collections.abc.Generator

ただしcollections.abc.GeneratorPython 3.8以前は[]をサポートしていないので、合わせてfrom __future__ import annotationsも使います3

from __future__ import annotations

from collections.abc import Generator


def generator_function() -> Generator[str, None, None]:
    yield "👩<200d>🎨"
    yield "🐯"
    yield "🐟"

別のやり方:IteratorIterable

mypyのドキュメントには、ジェネレータをtyping.Iteratorで型ヒントする例もあります4
https://mypy-lang.org/

それにならうと、先の例は

from __future__ import annotations

from collections.abc import Iterator


def generator_function() -> Iterator[str]:
    yield "👩<200d>🎨"
    yield "🐯"
    yield "🐟"

ともできます。

typing.Generatorのドキュメントを再度確認すると
https://docs.python.org/ja/3/library/typing.html#typing.Generator

代わりに、ジェネレータを Iterable[YieldType]Iterator[YieldType] という返り値の型でアノテーションをつけることもできます:

とあります。
typing.Iterable5typing.Iterator6Python 3.9で非推奨になっているので、collections.abc側を使っていきたい考えです。

なぜ3通りも書ける? 継承関係をもとに考える

ジェネレータ関数の返り値(ジェネレータイテレータ)に3通りも型ヒントが書ける理由を少し考えてみます。
https://docs.python.org/ja/3/library/collections.abc.html#collections-abstract-base-classes の継承関係を参照します7

「AがBを継承している」を「AはB」と表すと

よって、ジェネレータ関数の返り値はGeneratorでもIteratorでもIterableでも型ヒントできると理解しました。

終わりに

ジェネレータ関数の返り値について、collections.abc.Generator[YieldType, SendType, ReturnType]という意見を述べました。
ほかのやり方としてIteratorIterableを使う方法も紹介しています。

今回紹介したようなジェネリック型を使った型ヒントはtypingのドキュメントが一番詳しい印象です。
初手でcollections.abcのドキュメントを見に行っても、型ヒントを具体的にどう書けばいいかわからなくなる自信があります。
なので、オススメはtypingのドキュメントを参照し、「非推奨」の記載があったらcollections.abcを使うことです。
typingのドキュメントの説明はcollections.abcジェネリック型についても成り立ちます!

P.S. その1 どうしてtypingの各種クラスが非推奨なんですか!

Python 3.9 リリース時点でtypingの諸々が非推奨になり、私は納得いかなかったのですが、それを題材にするLT駆動学習したところ納得しています。

型ヒントが受け入れられたから、CPython自体を変更できるようになった (スライド33)

コミュニティが型ヒントを受け入れたことを表す、非常に喜ばしい出来事なんですよ!

P.S. その2 types.GeneratorType

型ヒントとは別に、types.GeneratorTypeというものがあります。
https://docs.python.org/ja/3/library/types.html#types.GeneratorType

これはジェネレータイテレータであることの検証に使う用途で知っていました。
ref: https://stackoverflow.com/a/12062004

import types
assert isinstance(la, types.GeneratorType)  # pytestだと思っています