Python ライブラリの型付け

Python の人気の多くは、開発者が利用できる豊富な Python ライブラリに起因しています。 これらのライブラリの作成者は、Python 開発者の体験を向上させる上で重要な役割を果たしています。 このドキュメントでは、Python ライブラリの作成者向けにいくつかの推奨事項とガイダンスを提供します。

なぜ型注釈を提供するのですか?

型注釈を提供することには次のような利点があります。

  1. 型注釈は、ユーザーが迅速かつ正確な補完候補、クラスおよび関数のドキュメント、シグネチャ ヘルプ、ホバー テキスト、自動インポートなどを利用できるようにすることで、ライブラリ ユーザーのコーディング エクスペリエンスを向上させるのに役立ちます。

  2. ライブラリのユーザーは、静的型チェッカーを使用してライブラリの使用に関する問題を検出できます。

  3. 型注釈を使用すると、ライブラリ作成者はツールによって強制されるインターフェイス コントラクトを指定できます。 これにより、ユーザーが実装の詳細に依存していることを恐れることなく、ライブラリの実装を進化させることができます。 ライブラリ インターフェイスに変更が加えられた場合、型チェッカーはコードに影響がある場合にユーザーに警告を発することができます。

  4. ライブラリ作成者は、静的型チェックを使用して、高品質でバグのない実装を作成するのに役立てることができます。

型注釈を提供する方法は?

PEP 561 では、ライブラリに型情報を提供するいくつかの方法について説明しています。

  • インライン型注釈 (推奨)

  • パッケージに含まれる型スタブ ファイル

  • 別のコンパニオン型スタブ パッケージ

  • typeshed リポジトリの型スタブ

インライン型注釈とは、単に .py ファイル内で注釈を使用することを指します。 対照的に、型スタブ ファイルでは、型情報は別の .pyi ファイルに存在します。 詳細については、スタブファイル および スタブファイルの作成と維持 を参照してください。

インライン型注釈アプローチを使用することをお勧めします。これは、次のような利点があるためです。

  • 追加と保守に必要な労力が最も少ない

  • ユーザーは追加のパッケージをダウンロードする必要はありません

  • 常に実装と一致する

  • ライブラリ作成者が自分のコードを型チェックできるようにする

  • 言語サーバーがユーザーにドキュメント文字列やデフォルトのパラメーター値など、実装に関連する詳細を表示できるようにします

ただし、インライン型注釈が不可能な場合もあります。特に、ライブラリの機能が Python 以外の言語で実装されている場合です。

ライブラリに型注釈を提供することに関心がない場合は、ユーザーに型スタブを typeshed プロジェクトに提供するよう提案することができます。

パッケージを型情報を提供するものとしてマークする

PEP 561 で指定されているように、特別な py.typed マーカーファイルが含まれていない限り、ツールはパッケージを型情報を提供するものとして扱いません。

注釈

パッケージを型情報を提供するものとしてマークする前に、ライブラリのインターフェイスが完全に注釈されていることを確認するのが最善です。 詳細については、ライブラリのどの部分に型が必要ですか? を参照してください。

インライン型注釈

典型的なディレクトリ構造は次のようになります。

setup.py
my_great_package/
   __init__.py
   stuff.py
   py.typed

py.typed マーカーファイルが配布パッケージに含まれていることを確認することが重要です。 setuptools を使用する場合、これは次のようにして実現できます。

from setuptools import setup

setup(
   name="my_great_distribution",
   version="0.1",
   package_data={"my_great_package": ["py.typed"]},
   packages=["my_great_package"],
)

パッケージに含まれる型スタブ ファイル

型スタブ ファイル (.pyi) とインライン型注釈 (.py) の混在を含めることができます。 パッケージに型スタブ ファイルを含めるユース ケースの 1 つは、ライブラリ内の拡張モジュールの型を提供することです。 典型的なディレクトリ構造は次のようになります。

setup.py
my_great_package/
   __init__.py
   stuff.py
   stuff.pyi
   py.typed

setuptools を使用する場合、次のようにして .pyi および py.typed ファイルが含まれていることを確認できます。

from setuptools import setup

setup(
   name="my_great_distribution",
   version="0.1",
   package_data={"my_great_package": ["py.typed", "stuff.pyi"]},
   packages=["my_great_package"],
)

.pyi ファイルの存在は、実行時に Python インタープリターに影響を与えることはありません。 ただし、静的型チェッカーは対応する .py ファイルを無視して .pyi ファイルのみを参照します。

コンパニオン型スタブ パッケージ

これらは「スタブのみ」パッケージと呼ばれることがよくあります。 スタブ パッケージの名前は、-stubs で終わるランタイム パッケージの名前である必要があります。 スタブのみのパッケージには py.typed マーカーファイルは必要ありません。 このアプローチは、ライブラリとは独立して型スタブを開発するのに役立ちます。

例えば:

setup.py
my_great_package-stubs/
   __init__.pyi
   stuff.pyi
from setuptools import setup

setup(
   name="my_great_package-stubs",
   version="0.1",
   package_data={"my_great_package-stubs": ["__init__.pyi", "stuff.pyi"]},
   packages=["my_great_package-stubs"]
)

その後、ユーザーはスタブのみのパッケージを別途インストールして、元のライブラリの型を提供できるようになります。

sdist への含める

.pyi および py.typed ファイルを sdist (.tar.gz アーカイブ) に含めることを確認するには、MANIFEST.in の含めるルールを変更する必要がある場合もあります (MANIFEST.in の詳細については、 packaging guide を参照してください)。 例えば:

global-include *.pyi
global-include py.typed

ライブラリのどの部分に型が必要ですか?

「py.typed」ライブラリは、型チェックと検査が最大限に機能するように、型が完全であることを目指す必要があります。 ここでは、ライブラリの「インターフェイス <library-interface>」を構成するすべてのシンボルに、完全に既知の型を参照する型注釈がある場合、そのライブラリは「型が完全である」と言います。 プライベート シンボルは免除されます。

型の完全性

次に、「型が完全である」と定義するためのベスト プラクティスの推奨事項を示します。

クラス:

  • 「表示可能」(オーバーライドされていない) すべてのクラス変数、インスタンス変数、およびメソッドに注釈が付けられ、既知の型を参照する

  • クラスがジェネリック クラスのサブクラスである場合、各ジェネリック型パラメーターに対して型引数が指定され、これらの型引数は既知の型である

関数とメソッド:

  • すべての入力パラメーターには、既知の型を参照する型注釈が付けられている

  • 戻り値のパラメーターには注釈が付けられ、既知の型を参照する

  • 1 つ以上のデコレータを適用した結果、既知の型が得られる

型エイリアス:

  • 型エイリアスによって参照されるすべての型は既知のものである

変数:

  • すべての変数には、既知の型を参照する型注釈が付けられている

型注釈は、型がコンテキストから明らかな場合に省略できます。

  • 単純なリテラル値が割り当てられた定数 (例: RED = '#F00' または MAX_TIMEOUT = 50 または room_temperature: Final = 20)。 定数は、1 回だけ割り当てられ、Final で注釈されているか、すべて大文字で名前が付けられているシンボルです。 単純なリテラル値が割り当てられていない定数には、明示的な注釈が必要です。できれば Final 注釈を付けてください (例: WOODWINDS: Final[List[str]] = ['Oboe', 'Bassoon'])。

  • Enum クラス内の Enum 値には注釈は必要ありません。これらは Enum クラスの型を取ります。

  • 型エイリアスには注釈は必要ありません。 型エイリアスは、モジュール レベルで単一の割り当てを持つシンボルであり、割り当てられた値がクラス インスタンスではなくインスタンス化可能な型であるものです (例: Foo = Callable[[Literal["a", "b"]], Union[int, str]] または Bar = Optional[MyGenericClass[int]])。

  • インスタンス メソッドの「self」パラメーターおよびクラス メソッドの「cls」パラメーターには、明示的な注釈は必要ありません。

  • __init__ メソッドの戻り値の型を指定する必要はありません。常に None であるためです。

  • 次のモジュール レベルのシンボルには型注釈は必要ありません。 __all__, __author__, __copyright__, __email__, __license__, __title__, __uri__, __version__

  • 次のクラス レベルのシンボルには型注釈は必要ありません。 __class__, __dict__, __doc__, __module__, __slots__

既知の型と未知の型の例

# 型が不明な変数
a = [3, 4, 5]

# 既知の型を持つ変数
a: List[int] = [3, 4, 5]

# 部分的に未知の型を持つ型エイリアス (型引数が list および dict に対して欠落しているため)
DictOrList = Union[list, dict]

# 既知の型を持つ型エイリアス
DictOrList = Union[List[Any], Dict[str, Any]]

# 既知の型を持つジェネリック型エイリアス
_T = TypeVar("_T")
DictOrList = Union[List[_T], Dict[str, _T]]

# 既知の型を持つ関数
def func(a: Optional[int], b: Dict[str, float] = {}) -> None:
    pass

# 部分的に未知の型を持つ関数 (型注釈が入力パラメーターおよび戻り値の型に対して欠落しているため)
def func(a, b):
    pass

# 部分的に未知の型を持つ関数 (Dict に型引数が欠落しているため)
def func(a: int, b: Dict) -> None:
    pass

# 部分的に未知の型を持つ関数 (戻り値の型注釈が欠落しているため)
def func(a: int, b: Dict[str, float]):
    pass

# 部分的に未知の型を持つデコレータ (型注釈が入力パラメーターおよび戻り値の型に対して欠落しているため)
def my_decorator(func):
    return func

# 部分的に未知の型を持つ関数 (型が型なしデコレータによって隠されているため)
@my_decorator
def func(a: int) -> str:
    pass


# 既知の型を持つクラス
class MyClass:
    height: float = 2.0

    def __init__(self, name: str, age: int):
        self.age: int = age

    @property
    def name(self) -> str:
        ...

# 部分的に未知の型を持つクラス
class MyClass:
    # クラス変数の型注釈が欠落している
    height = 2.0

    # 入力パラメーターの型注釈が欠落している
    def __init__(self, name, age):
        # インスタンス変数の型注釈が欠落している
        self.age = age

    # 戻り値の型注釈が欠落している
    @property
    def name(self):
        ...

# 部分的に未知の型を持つクラス
class BaseClass:
    # 型注釈が欠落している
    height = 2.0

    # 型注釈が欠落している
    def get_stuff(self):
        ...

# 既知の型を持つクラス (BaseClass によって公開されたすべてのシンボルをオーバーライドしているため)
class DerivedClass(BaseClass):
    height: float

    def get_stuff(self) -> str:
        ...

# 部分的に未知の型を持つクラス (dict がジェネリックであり、型引数が指定されていないため)
class DictSubclass(dict):
    pass

インライン型のベスト プラクティス

広い型と狭い型

型理論では、互いに関連する 2 つの型を比較する場合、「広い」型はより一般的な型であり、「狭い」型はより具体的な型です。 たとえば、Sequence[str]List[str] よりも広い型です。すべての List オブジェクトは Sequence オブジェクトでもありますが、その逆は成り立ちません。 サブクラスは、それが派生するクラスよりも狭いです。 型の共用体は、それを構成する個々の型よりも広いです。

一般に、関数の入力パラメーターには、実装でサポートされている最も広い型を注釈として付ける必要があります。 たとえば、実装が呼び出し元に文字列の反復可能なコレクションを提供する必要がある場合、パラメーターには List[str] ではなく Iterable[str] と注釈を付ける必要があります。 後者の型は必要以上に狭いため、ユーザーが文字列のタプルを渡そうとすると (実装でサポートされている)、型チェッカーは型の非互換性について警告します。

「可能な限り広い型を使用する」というルールの具体的な適用例として、ライブラリは (関数がコンテナーを変更する必要がない限り) 通常、変更可能な形式のコンテナー型ではなく、変更不可能な形式のコンテナー型を使用する必要があります。 SequenceList の代わりに使用し、MappingDict の代わりに使用します。 変更不可能なコンテナーは、型パラメーターが不変ではなく共変であるため、より柔軟性があります。 Sequence[Union[str, int]] として型指定されたパラメーターは、List[int]Sequence[str]、および Sequence[int] を受け入れることができます。 ただし、List[Union[str, int]] として型指定されたパラメーターははるかに制限が厳しく、List[Union[str, int]] のみを受け入れます。

オーバーロード

関数またはメソッドが複数の異なる型を返すことができ、それらの型が特定のパラメーターの存在または型に基づいて決定できる場合は、PEP 484 で定義されている @overload メカニズムを使用します。 オーバーロードは「.py」ファイル内で使用される場合、関数実装の前に表示される必要があり、@overload デコレータを持つべきではありません。

キーワード専用パラメーター

関数またはメソッドが名前でのみ指定されるパラメーターを取ることを意図している場合は、キーワード専用セパレータ (*) を使用します。

def create_user(age: int, *, dob: Optional[date] = None):
    ...

デコレータの注釈

デコレータはクラスまたは関数の動作を変更します。 デコレータに注釈を付けることは、デコレータが装飾された関数の元のシグネチャを保持する場合は簡単です。

_F = TypeVar("_F", bound=Callable[..., Any])

def simple_decorator(_func: _F) -> _F:
    """
     シンプルなデコレータは次のようにかっこなしで呼び出されます。
       @simple_decorator
       def my_function(): ...
     """
   ...

def complex_decorator(*, mode: str) -> Callable[[_F], _F]:
    """
     複雑なデコレータは次のように引数を指定して呼び出されます。
       @complex_decorator(mode="easy")
       def my_function(): ...
     """
   ...

装飾された関数のシグネチャを変更するデコレータは、型注釈に課題をもたらします。 PEP 612 で説明されている ParamSpec および Concatenate メカニズムはここで役立ちますが、これらは Python 3.10 以降でのみ使用できます。 より複雑なシグネチャの変更には、元のシグネチャを消去する型注釈が必要になる場合があり、これにより型チェッカーやその他のツールがシグネチャ アシスタンスを提供できなくなります。 そのため、ライブラリ作成者は、この方法で関数シグネチャを変更するデコレータの作成を控えることをお勧めします。

ジェネリック クラスと関数

さまざまな型に対してジェネリックな方法で操作できるクラスと関数は、PEP 484 で説明されているメカニズムを使用してジェネリックとして宣言する必要があります。 これには、TypeVar シンボルの使用が含まれます。 通常、TypeVar はそれを宣言するファイルに対してプライベートである必要があり、したがってアンダースコアで始まる必要があります。

型エイリアス

型エイリアスは、他の型を参照するシンボルです。 ジェネリック型エイリアス (特殊化されていないジェネリック クラスを参照するもの) は、ほとんどの型チェッカーでサポートされています。

PEP 613 は、新しい TypeAlias 注釈を使用してシンボルを型エイリアスとして明示的に指定する方法を提供します。

# シンプルな型エイリアス
FamilyPet = Union[Cat, Dog, GoldFish]

# ジェネリック型エイリアス
ListOrTuple = Union[List[_T], Tuple[_T, ...]]

# 再帰型エイリアス
TreeNode = Union[LeafNode, List["TreeNode"]]

# PEP 613 構文を使用した明示的な型エイリアス
StrOrInt: TypeAlias = Union[str, int]

抽象クラスとメソッド

サブクラス化する必要があるクラスは ABC から派生し、オーバーライドする必要があるメソッドまたはプロパティには @abstractmethod デコレータを付ける必要があります。 これにより、型チェッカーは必要なメソッドがオーバーライドされていることを検証し、そうでない場合に役立つエラーメッセージを開発者に提供できます。 抽象メソッドを実装するには、NotImplementedError 例外を発生させるのが一般的です。

from abc import ABC, abstractmethod

class Hashable(ABC):
   @property
   @abstractmethod
   def hash_value(self) -> int:
      """サブクラスはオーバーライドする必要があります"""
      raise NotImplementedError()

   @abstractmethod
   def print(self) -> str:
      """サブクラスはオーバーライドする必要があります"""
      raise NotImplementedError()

最終クラスとメソッド

サブクラス化することを意図していないクラスには、PEP 591 で説明されているように @final としてデコレートする必要があります。 同じデコレータを使用して、サブクラスによってオーバーライドできないメソッドを指定することもできます。

リテラル

型注釈は、PEP 586 で説明されているように、適切な場合にリテラル型を使用する必要があります。 リテラルは、非リテラルの対応する部分よりも型の特異性を高めることができます。

定数

定数値 (読み取り専用の値) は、PEP 591 で説明されているように Final 注釈を使用して指定できます。

型チェッカーは通常、すべて大文字の文字を使用して名前が付けられた変数を定数として扱います。

どちらの場合も、リテラル str、int、float、bool、または None 値が割り当てられている場合、定数の宣言型を省略しても問題ありません。 このような場合、型推論ルールは明確かつ一意であり、リテラル型注釈を追加することは冗長になります。

# 推論された型を持つすべて大文字の定数
COLOR_FORMAT_RGB = "rgb"

# 明示的な型を持つすべて大文字の定数
COLOR_FORMAT_RGB: Literal["rgb"] = "rgb"
LATEST_VERSION: Tuple[int, int] = (4, 5)

# 推論された型を持つ最終変数
ColorFormatRgb: Final = "rgb"

# 明示的な型を持つ最終変数
ColorFormatRgb: Final[Literal["rgb"]] = "rgb"
LATEST_VERSION: Final[Tuple[int, int]] = (4, 5)

型付き辞書、データ クラス、および名前付きタプル

ライブラリが新しいバージョンの Python でのみ実行される場合は、新しい型に対応したクラスの使用をお勧めします。

NamedTuple (PEP 484 で説明) は namedtuple よりも優先されます。

データ クラス (PEP 557 で説明) は、型なし辞書よりも優先されます。

TypedDict (PEP 589 で説明) は、型なし辞書よりも優先されます。

古い Python バージョンとの互換性

3.5 以降の各新しいバージョンの Python には、新しい型付け構造が導入されています。 これは、古いバージョンの Python との実行時の互換性を維持したいライブラリ作成者にとって課題となります。 このセクションでは、型を追加しながら後方互換性を維持するために使用できるいくつかの手法について説明します。

引用された注釈

変数、パラメーター、および戻り値の型注釈は引用符で囲むことができます。 その後、Python インタープリターはそれらを無視しますが、型チェッカーはそれらを型注釈として解釈します。

# OrderedDict 型は古いバージョンの Python ではサブスクリプトをサポートしていないため、注釈を引用符で囲む必要があります。
def get_config(self) -> "OrderedDict[str, str]":
   return self._config

型コメント注釈

Python 3.0 では、PEP 484 で指定されているように、パラメーターおよび戻り値の型注釈の構文が導入されました。 Python 3.6 では、PEP 526 で指定されているように、変数の型注釈のサポートが導入されました。

古いバージョンの Python をサポートする必要がある場合、型注釈は「型コメント」として提供できます。 これらのコメントは # type: の形式を取ります。

class Foo:
   # 変数の型コメントは、変数が割り当てられた行の末尾に記述します。
   timeout = None # type: Optional[int]

   # 関数の型コメントは、関数シグネチャの行の後に指定できます。
   def send_message(self, name, length):
      # type: (str, int) -> None
      ...

   # 関数の型コメントは、各パラメーターの型を個別の行に指定することもできます。
   def receive_message(
      self,
      name, # type: str
      length # type: int
   ):
      # type: () -> Message
      ...

typing_extensions

ランタイム サポートを必要とする新しい型機能は通常、stdlib typing モジュールに含まれます。 可能な場合、これらの新機能は typing_extensions というランタイム ライブラリにバックポートされ、古い Python ランタイムで動作します。

TYPE_CHECKING

typing モジュールは TYPE_CHECKING という変数を公開しており、これは Python ランタイム内では False の値を持ちますが、型チェッカーが分析を実行しているときは True の値を持ちます。 これにより、型チェック ステートメントを条件付きにすることができます。

TYPE_CHECKING を使用する場合は注意が必要です。型チェックと実行時の間で動作が変更されると、型チェッカーが検出するはずの問題が隠される可能性があるためです。

非標準の型の動作

型注釈は、典型的な型の動作に注釈を付ける方法を提供しますが、一部のクラスは標準以外の特殊な動作を実装しており、標準の型注釈を使用して記述することはできません。 現時点では、そのような型は Any として注釈を付ける必要があります。これは残念なことです。静的型付けの利点が失われるためです。

ドキュメント文字列

ドキュメント文字列は、インターフェイス内のすべてのクラス、関数、およびメソッドに提供する必要があります。 それらは PEP 257 に従ってフォーマットする必要があります。

関数およびメソッドのドキュメント文字列に関しては、現在、単一の合意された標準はありませんが、いくつかの一般的なバリアントが登場しています。 これらのバリアントのいずれかを使用することをお勧めします。