本稿は Python 3.5 で導入された型ヒントの PEP 0484 を翻訳したものです。
時間がなくて精訳できていないため、誤訳やより適切な翻訳があれば編集リクエストなどでご指摘ください。
- 2015-09-26: @knzm による修正内容を反映しました。誤訳の修正、より読みやすい訳文になっています。
PEP 0484 - 型ヒント
PEP: | 484 |
---|---|
タイトル: | 型ヒント |
著者: | Guido van Rossum <guido at python.org>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, Łukasz Langa <lukasz at langa.pl> |
BDFL-委任: | Mark Shannon |
Discussions-To: | Python-Dev <[python-dev at python.org](mailto:[email protected]?subject=PEP 484)> |
ステータス: | 認可 |
種別: | 標準化過程 |
作成日: | 2014年9月29日 |
投稿履歴: | 2015年1月16日、2015年3月20日、2015年4月17日、2015年5月20日、2015年5月22日 |
決議: | https://mail.python.org/pipermail/python-dev/2015-May/140104.html |
概要
PEP 3107 は関数アノテーションの構文を導入しましたが、その意味論は意図的に未定義にされていました。今日では静的型解析を目的としたサードパーティの利用法がたくさんあり、コミュニティは標準化された語彙と標準ライブラリ内の基本的ツールの恩恵を受けられます。
この PEP は、標準化された定義やツールを提供するための暫定モジュールおよびアノテーションを利用できない状況に対する規約を導入します。
この PEP は依然としてアノテーションの他の利用方法を明示的に阻むものではありません。さらに、この仕様に準拠した場合でも、アノテーションに対する何らかの特別な処理を必要とする (または禁止する) ものでもないことに注意してください。それは PEP 333 が Web フレームワークに行ったことのように、単純により良い協調をもたらすものです。
例えば、次の簡単な関数はその引数と戻り値の型がアノテーションで宣言されています。
def greeting(name: str) -> str:
return 'Hello ' + name
これらのアノテーションは実行時に普通の __annotations__
属性として参照できる一方で、 実行時には型チェックを行いません 。その代わり、この提案は独立したオフライン型チェッカーの存在を仮定しています。ユーザーはそのような型チェッカーを使って自主的にソースコードを検査できます。基本的にこういった型チェッカーは非常に強力なリンター (linter) として機能します。(もちろん似たようなチェッカーを使って個々のユーザー向けに契約による設計 (Design By Contract) の強制や JIT 最適化を実行時に行うこともできるはずですが、そういったツールはまだ実用レベルにはなっていません。)
この提案は mypy [mypy] に強く触発されています。例えば、"整数型のシーケンス" の型は Sequence[int]
のように記述します。角括弧を使うことで言語に新しい構文を追加する必要はありません。ここで紹介する例は pure-Python な typing
モジュールからインポートされたカスタム型 Sequence
を使います。Sequence[int]
といった表記は、メタクラスで __getitem__()
を実装することで実行時に動作します (しかし、そのシグネチャは主にオフライン型チェッカー向けです) 。
型システムは直和 (union) 型、ジェネリック型、および Any
というすべての型と一貫性のある(つまり相互に代入可能な)特別な型をサポートします。この後者の機能は漸進的型付け (gradual typing) のアイディアから受け継いでいます。漸進的型付けや型システム全体のことは PEP 483 で説明しています。
私たちが借りてきたその他の手法、もしくは比較や対比した他の手法については PEP 482 にまとめられています。
根拠と目的
PEP 3107 は関数定義の一部として任意のアノテーションをサポートするために追加されました。その時点ではアノテーションには特別な意味付けはありませんでしたが、そこには型ヒントのために使うという潜在的な目的が常にありました [gvr-artima] 。PEP 3107 では最初のユースケースとして型チェックが挙げられています。
この PEP では、型アノテーションの標準化された構文の提供を目的とします。それは容易な静的解析やリファクタリング、潜在的な実行時型チェックや (おそらくは何らかのコンテキストで) 型情報を利用したコード生成といったことに対して Python コードの可能性を切り開きます。
これらの目的のうち、最も重要なのは静的解析です。これには mypy のようなオフライン型チェッカーのサポートと、コード補完やリファクタリグのために IDE が利用する標準的な表記法の提供が含まれます。
目的としないもの
提案された typing モジュールは実行時型チェックのための構成要素、特に get_type_hints()
関数を含んでいますが、特定の実行時型チェックの機能を実装するためには(例えばデコレーターやメタクラスを使う)サードパーティパッケージの開発が必要になるでしょう。パフォーマンスの最適化に型ヒントを利用することは、この PEP の読者への課題として残してあります。
さらに以下のことも強調しておきましょう。 Python は依然として動的型付け言語のままです。 Python の作者たちは(たとえ規約としてであっても)型ヒントを必須とすることを望んではいません。
アノテーションの意味論
アノテーションのない任意の関数は、すべての型チェッカーからなるべく汎用な型を持つように扱われるか、無視されるべきです。 @no_type_check
デコレーターまたは # type: ignore
というコメントが付いた関数は、アノテーションを持っていないものとして扱う必要があります。
チェック対象関数は、すべての引数と戻り型のアノテーションを持つように推奨はしますが、必須ではありません。チェック対象関数の引数と戻り型のデフォルトのアノテーションは Any
型です。その例外は、インスタンスメソッドとクラスメソッドの最初の引数はアノテートする必要がないというものです。インスタンスメソッドに対してはそのインスタンスメソッドの所属するクラスの型を、クラスメソッドに対してはそのクラスメソッドの所属するクラスオブジェクトに対応する type オブジェクト型を持つことが仮定されます。例えば、クラス A
のインスタンスメソッドの最初の引数は暗黙的に型 A
になります。クラスメソッドでは、利用可能な型表記では最初の引数の正確な型を表現できません。
(__init__
の戻り型は -> None
のようにアノテートすべきだということに注意してください。この理由は微妙です。もし __init__
が -> None
という戻り型のアノテーションを持つとみなされる場合、引数を持たないアノテートされていない __init__
メソッドは型チェックすべきでしょうか? この曖昧さを残したりこの例外に対する例外を導入したりするよりも、私たちは単純に __init__
は戻り型のアノテーションを持つべきだと言います。これによってそのデフォルトの振る舞いは他のメソッドと同様になります。)
型チェッカーは、チェック対象関数の中身が指定されたアノテーションと整合性を持つかどうかチェックすることが期待されます。アノテーションは他のチェック対象関数に現れる呼び出しの正当性をチェックすることにも使われます。
型チェッカーは、必要に応じてできるだけ多くの情報を推測しようとすることが期待されます。最小要件は、組み込みデコレータである @property, @staticmethod
と @classmethod
を処理することです。
型定義の構文
構文は以下の節で述べる多くの拡張と共に PEP 3107 スタイルのアノテーションを活用します。その基本的な形式として、関数アノテーションスロットをクラスで埋めることによって型ヒントが使われます。
def greeting(name: str) -> str:
return 'Hello ' + name
これは name
引数に期待される型が str
だということを宣言しています。同様に、期待される戻り型は str
です。
その型が特定の引数の型のサブタイプである式もまたその引数として許容されます。
許容される型ヒント
型ヒントは、組み込みクラス (標準ライブラリまたはサードパーティ製の拡張モジュールで定義されたものを含む) か、抽象基底クラス、types
モジュールで定義された型、ユーザー定義クラス (標準ライブラリまたはサードパーティモジュールで定義されたものを含む) のいずれかです。
アノテーションは一般的に型ヒントに最適なフォーマットではあるものの、特別なコメント、または別ファイルに分離したスタブファイルで型ヒントを表現する方がより適切な場合があります。(以下の例を参照してください。)
アノテーションは、関数が定義された時点で例外をあげることなく評価する有効な式でなければなりません (ただし、前方参照については以下を参照してください) 。
アノテーションはシンプルに保つべきで、そうしないと静的解析ツールがその値を解釈できないかもしれません。例えば、動的に計算される型は理解できそうにありません。(これは意図的にやや曖昧な要件になっています。議論の結果として特定の追加仕様や例外仕様がこの PEP の将来のバージョンに追加される可能性があります。)
上記に加え、以下に定義された特別なコンストラクタを利用できます: None
, Any
, Union
, Tuple
, Callable
, typing
で公開されているすべての抽象基底クラスと具象クラス向けの代替となるもの (例 Sequence
や Dict
)、型変数、そして型の別名です。
以下の節で説明する機能をサポートするために使われる新たに導入されたすべての名前 (Any
や Union
など) は、 typing
モジュールが提供します。
None の利用
型ヒントで用いるとき None
という式は type(None)
と等価であるとみなされます。
型の別名
型の別名は単純に変数の代入によって定義されます。
Url = str
def retry(url: Url, retry_count: int) -> None:
...
ここで注意することは別名の文字の先頭を大文字にするのを推奨している点です。なぜなら、別名はユーザー定義型を表わしていて、それは(ユーザー定義クラスのように)通常そのように表記するからです。
型の別名はアノテーションにおける型ヒントと同程度に複雑なものになるでしょう。型ヒントとして許容できるすべてのものは型の別名として許容されます。
from typing import TypeVar, Iterable, Tuple
T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]
def inproduct(v: Vector) -> T:
return sum(x*y for x, y in v)
これは次のコードと等価です。
from typing import TypeVar, Iterable, Tuple
T = TypeVar('T', int, float, complex)
def inproduct(v: Iterable[Tuple[T, T]]) -> T:
return sum(x*y for x, y in v)
呼び出し可能オブジェクト
特定のシグネチャを持つコールバック関数を受け取るフレームワークは Callable[[Arg1Type, Arg2Type], ReturnType]
を使って型ヒントを行うかもしれません。例です。
from typing import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
引数リストをリテラルの省略記号 (3つのドット) で置き換えることで、呼び出しシグネチャを指定せずに呼び出し可能オブジェクトの戻り型を宣言できます。
def partial(func: Callable[..., str], *args) -> Callable[..., str]:
# Body
省略記号の前後に角括弧がないことに注意してください。この場合コールバック関数の引数には何の制約もありません (そしてキーワード引数は許容されます) 。
コールバック関数にキーワード引数を使うことは一般的なユースケースとしては認識されていないため、 Callable
にキーワード引数を指定するための仕組みはいまのところありません。同様に、コールバックのシグネチャに特定の型の可変長引数を指定するための仕組みもありません。
typing.Callable
は collections.abc.Callable
を置き換えるものとして2つの役割を担っているため、 isinstance(x, typing.Callable)
は isinstance(x, collections.abc.Callable)
を遅延させることにより実装されています。しかし、 isinstance(x, typing.Callable[...])
はサポートされていません。
ジェネリクス
コンテナー内のオブジェクトに関する型情報は一般的な方法で静的に型推論できません。そのため、抽象基底クラスが拡張されてコンテナー要素に期待される型を示すために配列添字をサポートするようになっています。例です。
from typing import Mapping, Set
def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None:
...
ジェネリクスは typing
モジュールにある TypeVar
という新たなファクトリーを使うことでパラメーター化されます。例です。
from typing import Sequence, TypeVar
T = TypeVar('T') # Declare type variable
def first(l: Sequence[T]) -> T: # Generic function
return l[0]
この場合、戻り値がコレクションに保持されている要素と一貫性があるということが契約になります。
TypeVar()
という式は、常に直接変数に代入しなければなりません (より大きな式の一部に使うべきではありません)。 TypeVar()
に渡す引数は、それを代入する変数名と等価な文字列でなければなりません。型変数を再定義してはいけません。
TypeVar
は特定の起こり得る型の集合に制約するパラメーター化された型をサポートします。例えば、 str
と bytes
だけを扱う型変数を定義できます。デフォルトでは、型変数はすべての起こり得る型を扱います。型変数の制約の例です。
from typing import TypeVar
AnyStr = TypeVar('AnyStr', str, bytes)
def concat(x: AnyStr, y: AnyStr) -> AnyStr:
return x + y
関数 concat
は、2つの str
型の引数か、2つの bytes
型の引数のいずれかで呼び出すことができますが、 str
型と bytes
型の引数を混在させて呼び出すことはできません。
制約は、もし存在するなら少なくとも2つ必要です。制約を1つだけ指定することはできません。
型変数により制約される型のサブタイプは、型変数のコンテキストでは、それぞれの明示的に表された基本型として扱われるべきです。この例を考えてみます。
class MyStr(str):
...
x = concat(MyStr('apple'), MyStr('pie'))
この呼び出しは有効ですが、その型変数 AnyStr
は MyStr
ではなく str
に設定されるでしょう。x
に代入される戻り値として推論される型も実際には str
になります。
さらに Any
はすべての型変数にとって有効な値です。次のことを考えてみましょう。
def count_truthy(elements: List[Any]) -> int:
return sum(1 for elem in elements if element)
これはジェネリクス表記を取り除いて単に elements: List
とするのと等価です。
ユーザー定義のジェネリック型
ジェネリック型としてユーザー定義のクラスを定義するために Generic
基底クラスが使えます。例です。
from typing import TypeVar, Generic
T = TypeVar('T')
class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value
def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new
def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value
def log(self, message: str) -> None:
self.logger.info('{}: {}'.format(self.name message))
基底クラスとしての Generic[T]
は、1つの型パラメーター T
をとるクラス LoggedVar
を定義します。さらにこれはクラス内部で T
を型として有効にします。
Generic
基底クラスは LoggedVar[t]
が型として有効になるように __getitem__
を定義するメタクラスを使います。
from typing import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
ジェネリック型は、任意の数の型変数をとり、型変数を制約します。次のコードは有効です。
from typing import TypeVar, Generic
...
T = TypeVar('T')
S = TypeVar('S')
class Pair(Generic[T, S]):
...
Generic
に渡す個々の型変数は別個のものでなければなりません。これにより次のコードは無効です。
from typing import TypeVar, Generic
...
T = TypeVar('T')
class Pair(Generic[T, T]): # INVALID
...
Generic
には多重継承が使えます。
from typing import TypeVar, Generic, Sized
T = TypeVar('T')
class LinkedList(Sized, Generic[T]):
...
型パラメーターを指定せずにジェネリッククラスをサブクラス化すると、それぞれの位置引数には Any
を指定したものとみなされます。次の例では、MyIterable
はジェネリック型ではありませんが、暗黙的に Iterable[Any]
から継承します。
from typing import Iterable
class MyIterable(Iterable): # Same as Iterable[Any]
...
ジェネリック型のメタクラスはサポートされません。
ジェネリッククラスのインスタンス化と型消去
List
や Sequence
のようなジェネリック型はインスタンス化できません。しかし、それらのジェネリック型から派生したユーザー定義クラスはインスタンス化できます。 Generic[T]
を継承する Node
クラスを考えてみましょう。
from typing import TypeVar, Generic
T = TypeVar('T')
class Node(Generic[T]):
...
いまこのクラスをインスタンス化する方法が2つあります。型チェッカーが推論する型は、そのどちらの形式を使ったかによって異なります。最初の方法は、型パラメーターの値を明示的に渡すことです。これは型チェッカーが実行する任意の型推論を上書きします。
x = Node[T]() # The type inferred for x is Node[T].
y = Node[int]() # The type inferred for y is Node[int].
型が明示的に指定されていない場合、型チェッカーが自由に推論します。次のコードを考えてみましょう。
x = Node()
推論される型は Node[Any]
になります。より正確な型を推論するのに足るコンテキストがないからです。あるいは型チェッカーはこの行を受け入れず、次のように明示的なアノテーションを要求するかもしれません。
x = Node() # type: Node[int] # Inferred type is Node[int].
より強力な型推論をもつ型チェッカーは x
がそのファイル内でどう使われるかを調べて、明示的な型アノテーションが見つからなかったとしても Node[int]
のようにより正確に型推論しようとします。しかし、おそらくはそういった類の型推論をすべてのケースでうまく動くようにするのは不可能です。 Python のプログラムはあまりに動的になりえるからです。
この PEP は型推論がどう作用すべきかの詳細については定めません。私たちは様々なアプローチを試すために様々なツールを許容します。将来のバージョンでより明確なルールを定めるかもしれません。
実行時に型は保持されません。 x
のクラスはすべてのケースにおいて単に Node
です。この振る舞いは "型消去" と呼ばれています。ジェネリクスをもつ言語 (例えば Java や TypeScript) に共通のプラクティスです。
基底クラスとしての任意のジェネリック型
Generic[T]
は基底クラスとしてのみ有効です。これは厳密には型ではありません。しかし、上述したサンプルコードの LinkedList[T]
にあるユーザー定義ジェネリック型や、組み込みのジェネリック型や List[T]
や Iterable[T]
のような抽象基底クラスは、型としても基底クラスとしても有効です。例えば、型引数を特殊化した Dict
のサブクラスを定義できます。
from typing import Dict, List, Optional
class Node:
...
class SymbolTable(Dict[str, List[Node]]):
def push(self, name: str, node: Node) -> None:
self.setdefault(name, []).append(node)
def pop(self, name: str) -> Node:
return self[name].pop()
def lookup(self, name: str) -> Optional[Node]:
nodes = self.get(name)
if nodes:
return nodes[-1]
return None
SymbolTable
は dict
のサブクラスであり、Dict[str, List[Node]]
のサブタイプです。
ジェネリック基底クラスが型引数としての型変数を持つとしたら、これは定義したクラスをジェネリックにします。例えば、繰り返し処理可能なコンテナーオブジェクトであるジェネリック LinkedList
クラスを定義できます。
from typing import TypeVar, Iterable, Container
T = TypeVar('T')
class LinkedList(Iterable[T], Container[T]):
...
いま LinkedList[int]
は有効な型です。 T
を Generic[...]
の内部で複数回使わない分には T
を基底クラスのリストで複数回使える点に注意してください。
また次の例を考えてみます。
from typing import TypeVar, Mapping
T = TypeVar('T')
class MyDict(Mapping[str, T]):
...
この場合 MyDict は単一のパラメーター T
をとります。
抽象ジェネリック型
Generic
が使うメタクラスは abc.ABCMeta
のサブクラスです。ジェネリッククラスは抽象メソッドまたはプロパティを含めることにより抽象基底クラスになります。また、ジェネリッククラスは基底クラスとして複数の抽象基底クラスをメタクラスの競合なしに持つことができます。
上界をもつ型変数
型変数は bound=type
を使って上界 (upper bound) を指定できます。これが意味するのは、型変数を (明示的にしろ暗黙的にしろ) 置き換える実際の型は境界型 (boundary type) のサブクラスでなければならないということです。一般的な例は、ほとんどの通常のエラーを捕捉するのに十分うまく動く Comparable 型の定義です。
from typing import TypeVar
class Comparable(metaclass=ABCMeta):
@abstractmethod
def __lt__(self, other: Any) -> bool:
...
... # __gt__ etc. as well
CT = TypeVar('CT', bound=Comparable)
def min(x: CT, y: CT) -> CT:
if x < y:
return x
else:
return y
min(1, 2) # ok, return type int
min('x', 'y') # ok, return type str
(これは必ずしも理想的ではないことに注意してください。例えば、min('x', 1)
は実行時にエラーとなりますが、型チェッカーは単純に戻り型として Comparable
を推論するでしょう。残念ながら、これに対応するにはさらに強力で複雑な概念である F-bounded polymorphism を導入する必要があります。将来的に私たちはこの概念を再考するかもしれません。)
上界は (前の例に使った AnyStr
のように) 型制約と組み合わせることはできません。型制約は、推論された型が制約された型のどれかと _厳密に_ 一致するようにします。それに対して上界は、実際の型が境界型のサブクラスであることのみを要求します。
共変性と反変性
クラス Employee
とそのサブクラス Manager
を考えてみましょう。いま List[Employee]
でアノテートされた引数を持つ関数があると仮定します。 List[Manager]
型の変数を引数としてこの関数を呼び出すことが許されるべきでしょうか? 多くの人はその論理的帰結を考えもせずに "はい、もちろん" と答えるでしょう。しかし、その関数についての詳細を知らない限り、型チェッカーはそういった呼び出しを拒否すべきです。その関数は Empoyee
インスタンスをそのリストに追加するかもしれません。それは呼び出し側で変数の型に違反します。
そのような引数は _反変的_ (contravariantly) な振る舞いだということが分かります。それに対して直感的な回答 (この関数が引数を変化させない場合は正しい!) は _共変的_ (covariantly) に振る舞う引数を要求します。これらの概念の長めの入門は Wikipedia [wiki-variance] にあります。ここでは手短に型チェッカーの振る舞いをどう制御するかを説明しましょう。
デフォルトでは型変数は _不変的_ (invariant) であるとみなされます。それは List[Employee]
型のようにアノテートされた引数に対する引数は、型アノテーションに厳密に一致しなければならないということを意味します。型パラメーター (この例では Employee
) のサブクラスやスーパークラスは許されません。
共変な型チェックが受け入られるコンテナー型の宣言を行うためには covariant=True
を使って型変数を宣言します。反変な振る舞いが求められる (稀な) 場合には contravariant=True
を渡します。これらのうちの高々1つを渡せます。
典型的な例は、書き換え不能 (または読み取り専用) なコンテナークラスの定義です。
from typing import TypeVar, Generic, Iterable, Iterator
T = TypeVar('T', covariant=True)
class ImmutableList(Generic[T]):
def __init__(self, items: Iterable[T]) -> None:
...
def __iter__(self) -> Iterator[T]:
...
...
class Employee:
...
class Manager(Employee):
...
def dump_employees(emps: ImmutableList[Employee]) -> None:
for emp in emps:
...
mgrs = ImmutableList([Manager()]) # type: ImmutableList[Manager]
dump_employees(mgrs) # OK
typing
モジュールにある読み取り専用コレクションクラス (例: Mapping
と Sequence
)はすべて共変な型変数を使って宣言されています。可変コレクションクラス (例: MutableMapping
と MutableSequence
) は通常の不変な型変数を使って定義されています。反変な型変数の例の1つは Generator
型です。 send()
の引数の型は反変になります (以下を参照) 。
注意: 変位指定 (variance) はジェネリック型の型パラメーターに影響します。それは通常のパラメーターには影響しません。例えば、次の例は問題ありません。
from typing import TypeVar
class Employee:
...
class Manager(Employee):
...
E = TypeVar('E', bound=Employee) # Invariant
def dump_employee(e: E) -> None:
...
dump_employee(Manager()) # OK
数値型階層
PEP 3141 は Python の数値型階層を定義します。標準ライブラリの numbers
モジュールは対応する抽象基底クラス (Number
, Complex
, Real
, Rational
, Integral
) を実装します。これらの抽象基底クラスにはいくつかの課題がありますが、組み込みの具象数値クラス complex
, float
と int
は至るところで使われています (特に後者の2つ :-) 。
ユーザーが import numbers
と書いて numbers.Float
などを使うことを義務付ける代わりに、この PEP はほぼ同じ効果がある簡単なショートカットを提案します: ある引数が float
型を持つとアノテートされるとき int
型の引数も許容されます。同様に complex
型を持つとアノテートされた引数に対しては float
型か int
型の引数も許容されます。これは対応する抽象基底クラスを実装するクラスや fractions.Fraction
クラスを扱えませんが、そういったユースケースはとても稀だと考えています。
バイト型
byte の配列のために bytes
, bytearray
と memoryview
という3つの組み込みクラスがあります (array
モジュールが提供するクラスは数に入れていません)。当然 bytes
と bytearray
は多くの振る舞いが共通しています (すべてではありません。例えば bytearray
は変更可能です) 。
collections.abc
で ByteString
という共通基底クラスが定義されていて、それに対応する型が typing
にも存在しますが、 (いずれかの) バイト型を受け取る関数は普通にあるため、至るところで typing.ByteString
を書かなければならないのは面倒でしょう。そのため、組み込みの数値クラスのときと同様のショートカットとして、引数が bytes
型を持つとしてアノテートされているときは bytearray
型または memoryview
型も許容されます。(繰り返しますが、これがうまくいかない状況はあります。しかし、実際にはそういった状況は稀だと考えています。)
前方参照
型ヒントがまだ定義されていない名前を含むとき、その定義を文字列リテラルとして表すことができます。文字列リテラルで表された名前は、あとで名前解決されます。
このことが一般的に発生する状況はコンテナークラスの定義のときです。コンテナークラスでは、定義中のクラスがいくつかのメソッドのシグネチャに現れます。例えば、次のコード (単純なバイナリツリーの実装の最初の数行) は動きません。
class Tree:
def __init__(self, left: Tree, right: Tree):
self.left = left
self.right = right
これに対応するには次のように書きます。
class Tree:
def __init__(self, left: 'Tree', right: 'Tree'):
self.left = left
self.right = right
文字列リテラルは有効な Python の式 (つまり compile(lit, '', 'eval')
が有効なコードオブジェクトになる) を含んでいなければならず、モジュールが完全に読み込まれた時点でエラーなく評価できなければなりません。それを評価するときのローカルおよびグローバルの名前空間は、同じ関数に対するデフォルト引数が評価される名前空間と同じでなければなりません。
さらにその式は有効な型ヒントとして解析できなければなりません。例えば、許容される型ヒント の節で上述したルールによって制約されます。
型ヒントの 一部 として文字列リテラルを使うことが認められます。例えば、
class Tree:
...
def leaves(self) -> List['Tree']:
...
前方参照の一般的な使用例は、たとえば Django モデルがシグネチャに必要とされるような場合です。典型的にはそれぞれのモデルは別のファイルにあり、他のモデルに関係する型を持った引数を受け取るメソッドがあります。Python における循環インポートの解決方法のために、直接必要なモデルをすべてインポートできないときがしばしばあります。
# File models/a.py
from models.b import B
class A(Model):
def foo(self, b: B):
...
# File models/b.py
from models.a import A
class B(Model):
def bar(self, a: A):
...
# File main.py
from models.a import A
from models.b import B
最初に main がインポートされると想定すると、models/b.py の from models.a import A
の行で ImportError が発生して失敗します。b.py は class A が定義される前に models/a.py からインポートされているからです。解決策はモジュールのみインポートするように切り替えて _module_._class_ 形式でモデルを参照することです。
# File models/a.py
from models import b
class A(Model):
def foo(self, b: 'b.B'):
...
# File models/b.py
from models import a
class B(Model):
def bar(self, a: 'a.A'):
...
# File main.py
from models.a import A
from models.b import B
直和型 (Union types)
単一の引数に対して限定的な少数の期待される型の集合を受け取ることは一般的であるため、 Union
という特別なファクトリーが用意されています。例です。
from typing import Union
def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
if isinstance(e, Employee):
e = [e]
...
Union[T1, T2, ...]
で表される型は、 T1
とその任意のサブタイプや T2
とその任意のサブタイプなどに対して、 issubclass
チェックの結果が True
になります。
直和型の一般的な使用例は optional 型です。関数定義のときにデフォルト値の None
を指定しない限り、デフォルトでは None
は任意の型に対して無効な値です。例です。
def handle_employee(e: Union[Employee, None]) -> None:
...
Union[T1, None]
の簡略化した表現として Optional[T1]
と記述できます。例えば、先ほどのコードは次のコードと等価です。
from typing import Optional
def handle_employee(e: Optional[Employee]) -> None:
...
また、デフォルト値が None
のときは自動的に optional 型とみなします。例えば、
def handle_employee(e: Employee = None):
...
これは次のコードと等価です。
def handle_employee(e: Optional[Employee] = None) -> None:
...
Any
型
Any
は特別な型です。すべての型は Any
のサブタイプです。これは組み込み型の object
についても同様です。ただし、静的型チェッカーにとってはこれらは完全に違います。
ある値の型が object
であるとき、型チェッカーはその値に対するほとんどすべての操作を拒否するでしょう。そして、より特殊化された型の変数にその値を代入する (または戻り値としてその値を使う) ことは型エラーとなります。それに対して、ある値が Any
型であるとき、型チェッカーはその値のすべての操作を許容するようになり、より制約された型の変数に Any
型の値を代入する (または戻り値としてその値を使う) ことができます。
バージョンとプラットフォームのチェック
型チェッカーは単純なバージョンおよびプラットフォームのチェックが行えることが求められます。例です。
import sys
if sys.version_info[0] >= 3:
# Python 3 specific definitions
else:
# Python 2 specific definitions
if sys.platform == 'win32':
# Windows specific definitions
else:
# Posix specific definitions
チェッカーが "".join(reversed(sys.platform) == "xunil"
のような難読化されたコードを扱うことは期待しないでください。
デフォルト引数の値
スタブでは、デフォルトを持つ引数を実際のデフォルト値を指定せずに宣言できるのは便利かもしれません。例です。
def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr:
...
デフォルト値はどのような値になるべきでしょうか? ""
, b""
または None
のいずれも型制約を満たすのに失敗します (実際 None
はその型を Optional[AnyStr]
となるように 変更します) 。
こういったケースでは、デフォルト値をリテラルの省略記号として指定することができます。つまり、上述した例では正に書きたいものを表しています。
その他の関数アノテーションの用途と互換性
多くの既存または潜在的な関数アノテーションのユースケースがあり、それらは型ヒントと非互換です。そういったものは静的型チェッカーを混乱させる可能性があります。しかし、型ヒントのアノテーションは実行時に何もしません (アノテーション式の評価と関数オブジェクトの __annotations__
属性にアノテーションを保持する以外のことはしません) 。これはプログラムを不正なものにしません。ただ型チェッカーが誤った警告かエラーを出力するだけでしょう。
プログラムの一部に型ヒントを適用しないようにするには、次に述べる方法のうちのいくつかを使います。
-
# type: ignore
といったコメント - クラスや関数への
@no_type_check
デコレーター -
@no_type_check_decorator
でマークされたカスタムクラスや関数デコレーター
詳細については後の節を参照してください。
オフライン型チェックと最大限の互換性を保つために、アノテーションに依存したインターフェースを別の仕組み(例えば、デコレーター)に切り替えることが最終的に良いアイデアかもしれません。とはいえ、Python 3.5 ではそのような圧力はありません。以下の 却下された代替案 の長い議論も参照してください。
型コメント
変数を特定の型と明示的にマークするためのファーストクラスな構文はこの PEP では追加しません。複雑なケースにおける型推論を支援するため、次のフォーマットのコメントが使えます。
x = [] # type: List[Employee]
x, y, z = [], [], [] # type: List[int], List[int], List[str]
x, y, z = [], [], [] # type: (List[int], List[int], List[str])
x = [
1,
2,
] # type: List[int]
型コメントは変数宣言を含む文の最終行に置く必要があります。また型コメントは with
文とfor
文にも、そのコロンのすぐ後ろに置けます。
with
と for
文の型コメントの例です。
with frobnicate() as foo: # type: int
# Here foo is an int
...
for x, y in points: # type: float, float
# Here x and y are floats
...
スタブでは、初期値を指定せずに変数の存在を宣言することが便利でしょう。これはリテラルの省略記号を使ってできます。
from typing import IO
stream = ... # type: IO[str]
非スタブコードでは、同様の特別なケースがあります。
from typing import IO
stream = None # type: IO[str]
型チェッカーは (None
という値が指定された型に一致しないにも関わらず) このコードをエラーとみなすべきでなく、推論される型を Optional[...]
に変更すべきでもありません (アノテートされた引数とデフォルト値の None
のためにこれを行うルールがあるにも関わらず) 。ここでの仮定は、他のコードが責任を持ってその変数に適切な型の値を代入するだろうということです。そして、その変数を使用するすべての箇所では指定された型を持つことを想定できます。
# type: ignore
コメントはそのエラーを参照している行に置く必要があります。
import http.client
errors = {
'not_found': http.client.NOT_FOUND # type: ignore
}
# type: ignore
コメントが1行に単独で存在する場合、そのコメントがある行からファイルの残りの部分のすべての型チェックを無効にします。
もし型ヒントが一般的に役に立つと分かってきたら、型変数の構文が将来の Python バージョンで提供される可能性があります。
キャスト
場合によっては型チェッカーが異なる種類のヒントを必要とすることがあります: プログラマーは、ある式が型チェッカーが推論できる型よりも制約された型であることを知っているかもしれません。例えば、
from typing import List, cast
def find_first_str(a: List[object]) -> str:
index = next(i for i, x in enumerate(a) if isinstance(x, str))
# We only get here if there's at least one string in a
return cast(str, a[index])
型チェッカーによっては a[index]
の型が str
だと推論できずに object
か Any
型と推論するかもしれません。しかし、プログラマーは (コードがそこに到達したのであれば) 文字列に間違いないことを分かっています。cast(t, x)
は型チェッカーに x
の型が t
だと確信していることを伝えます。実行時にはキャストは常に式を変更せずにそのまま返します。その型をチェックしませんし、その値に対して型変換もしません。
キャストは型コメント(前の節を参照)とは違います。型コメントを使うとき、型チェッカーは依然として推論された型がその表明された型と一貫性があるかを検証する必要があります。キャストを使うとき、型チェッカーは無分別にプログラマを信頼すべきです。また、キャストは式において使われるのに対して、型コメントは代入において適用されます。
スタブファイル
スタブファイルは型ヒントを含むファイルであり、実行時ではなく型チェッカーによってのみ利用されます。スタブファイルには様々なユースケースがあります。
- 拡張モジュール
- 作者がまだ型ヒントを追加していないサードパーティモジュール
- 型ヒントがまだ書かれてない標準ライブラリモジュール
- Python 2と3で互換性を維持しなければならないモジュール
- 他の目的のためにアノテーションを使うモジュール
スタブファイルは通常の Python モジュールとして同じ構文を使います。スタブファイルでのみ使える typing
モジュールの機能が1つあります。それはこの後の節で説明する @overload
デコレータです。
型チェッカーはスタブファイルの関数シグネチャのみチェックすべきです。スタブファイルの関数本体はリテラルの省略記号 (...
) のみにするのを推奨します。
型チェッカーはスタブファイルの検索パスを設定可能にする必要があります。スタブファイルが見つかった場合、型チェッカーは対応する "本物" のモジュールを読み込むべきではありません。
スタブファイルは構文的に有効な Python モジュールであるものの、対応する本物のモジュールと同じディレクトリにおいてスタブファイルを保守できるように .pyi
という拡張子を使います。スタブファイルを別にすることにより、実行時に何もしないというスタブファイルに期待された振る舞いの印象を強めます。
スタブファイルに関するその他の注意事項です。
- スタブファイルにインポートされるモジュールや変数は、
import ... as ...
という形式でインポートしない限り、スタブファイルからエクスポートされたものとみなしません。
関数オーバーロード
@overload
デコレーターは引数の型の異なる組み合わせをサポートする関数を記述できるようにします。このパターンは組み込みモジュールと組み込み型で頻繁に使われています。例えば、bytes
型の __getitem__()
メソッドは次のように記述されます。
from typing import overload
class bytes:
...
@overload
def __getitem__(self, i: int) -> int:
...
@overload
def __getitem__(self, s: slice) -> bytes:
...
このコードは (引数と戻り型の関係を表現できない) 直和型を使って実現するものよりも厳密です。
from typing import Union
class bytes:
...
def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]:
...
@overload
が役に立つ他の例は、組み込みの map()
関数の型です。map()
関数は呼び出し可能オブジェクトの型次第で異なる引数を取ります。
from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload
T1 = TypeVar('T1')
T2 = TypeVar('T2')
S = TypeVar('S')
@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]:
...
@overload
def map(func: Callable[[T1, T2], S],
iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]:
...
# ... and we could add more items to support more than two iterables
map(None, ...)
をサポートするために簡単に要素を追加できることにも注意してください。
@overload
def map(func: None, iter1: Iterable[T1]) -> Iterable[T1]:
...
@overload
def map(func: None,
iter1: Iterable[T1],
iter2: Iterable[T2]) -> Iterable[Tuple[T1, T2]]:
...
@overload
デコレーターはスタブファイルでのみ使えます。この構文を使って多重ディスパッチの実装を提供することもできますが、その実装はみんなのひんしゅくを買う sys._getframe()
を必要とするでしょう。さらに効率的な多重ディスパッチの仕組みを設計・実装するのは困難です。それは過去にあった試みが放棄されて functools.singledispatch()
が支持された理由です。(PEP 443 を参照してください。特に "Alternative approaches" の節です。)将来的に要件を満たす多重ディスパッチの設計を思い付くかもしれませんが、そうした設計がスタブファイルの型ヒント向けに定義されたオーバーロード構文によって制約されることは望んでいません。当面の間、 @overload
デコレーターを使ったり overload()
を直接呼び出したりすると RuntimeError
が発生します。
@overload
デコレーターを使う代わりに制約を課した TypeVar
型が大抵は使われます。例えば、このスタブファイルの concat1
と concat2
の定義は等価です。
from typing import TypeVar
AnyStr = TypeVar('AnyStr', str, bytes)
def concat1(x: AnyStr, y: AnyStr) -> AnyStr:
...
@overload
def concat2(x: str, y: str) -> str:
...
@overload
def concat2(x: bytes, y: bytes) -> bytes:
...
上述した map
や bytes.__getitem__
のような関数は、型変数を使って正確に表現できません。しかし、@overload
とは異なり、型変数はスタブファイルの外部でも使えます。@overload
は、スタブのみに使えるという特殊な状態のため、型変数が十分ではない用途に限り使うことを推奨します。
AnyStr
のような型変数と @overload
を使うことの別の重要な違いは、前者はジェネリッククラスの型パラメーターの制約を定義するのにも使えるところです。例えば、ジェネリッククラスの型パラメーター typing.IO
の制約を定義しています (IO[str]
, IO[bytes]
と IO[Any]
のみ有効) 。
class IO(Generic[AnyStr]):
...
スタブファイルの保存場所と配布
スタブファイルの保存場所と配布の最も簡単な形態は、 Python モジュールと一緒に同じディレクトリに置くことです。こうすることでプログラマーとツールの両方から見つけやすくなります。しかし、パッケージメンテナーは自分たちのパッケージに型ヒントを追加しないのも自由です。そのため PyPI から pip
コマンドでインストール可能なサードパーティスタブもサポートされています。この場合、名前付け、バージョン、インストールパスの3つの課題を考慮する必要があります。
この PEP はサードパーティスタブファイルパッケージに使う名前付けスキームの勧告は提供しません。見つけやすさはうまくいけばパッケージの人気度に基づくでしょう。例えば Django パッケージのようにです。
サードパーティスタブは、互換性のあるソースパッケージの最低バージョンを使ってバージョン設定される必要があります。例: FooPackage はバージョン 1.0, 1.1, 1.2, 1.3, 2.0, 2.1, 2.2 があります。バージョン 1.1, 2.0 と 2.2 で API の変更があります。スタブファイルパッケージメンテナーは、すべてのバージョン向けにスタブをリリースするのは自由ですが、エンドユーザーがすべてのバージョンの型チェックを行うために少なくとも 1.0, 1.1, 2.0 と 2.2 が必要とされます。ユーザーは、パッケージバージョンと最も近い 低いまたは同じ スタブのバージョンは互換性があるということを知っているからです。先ほどの例では、FooPackage 1.3 向けにユーザーはスタブのバージョン 1.1 を選択するでしょう。
もしユーザーが利用可能なソースパッケージの "最新" を使うことに決めた場合、そのスタブが頻繁にアップデートされているなら、一般的には "最新" のスタブファイルを使うことでもうまくいくということに注意してください。
サードパーティスタブパッケージは、スタブの保存場所に任意の場所を使えます。型チェッカーは PYTHONPATH を使ってスタブファイルを検索する必要があります。必ずチェックされるデフォルトのフォールバックディレクトリは shared/typehints/python3.5/
(または 3.6 など) です。環境毎に特定の Python バージョンに対してインストールされるパッケージは一つだけなので、そのディレクトリ配下では他にバージョン設定が行われることはありません。スタブファイルパッケージの作成者は setup.py
で次のコードを使うことができます。
...
data_files=[
(
'shared/typehints/python{}.{}'.format(*sys.version_info[:2]),
pathlib.Path(SRC_PATH).glob('**/*.pyi'),
),
],
...
Typeshed リポジトリ
便利なスタブが集められた [typeshed] という共有リポジトリがあります。あるパッケージのスタブはパッケージオーナーの明示的な承諾なくここには含められないことに注意してください。ここに集められるスタブに関するさらなるポリシーは python-dev で議論した後で決定し、typeshed リポジトリの README で報告します。
例外
発生する例外を明示的に示す構文は提案しません。いまのところ、この機能のユースケースとして知られているのはドキュメント用途のみです。そして、その場合に推奨されるのは docstring にこの情報を記述することです。
typing
モジュール
静的型付けの使用を Python 3.5 だけでなく古いバージョンにも広げるためには統一的な名前空間が必要です。この目的のために、標準ライブラリに新たな typing
というモジュールを導入します。
このモジュールは、型を構築するための基本的な構成要素 (例: Any
)、組み込みコレクションのジェネリックバリアントを表現する型 (例: List
)、ジェネリックコレクションの抽象基底クラスを表現する型 (例: Sequence
)を定義し、その他の便利な定義を含みます。
基本的な構成要素:
- Any は
def get(key: str) -> Any: ...
のように使う - Union は
Union[Type1, Type2, Type3]
のように使う - Callable は
Callable[[Arg1Type, Arg2Type], ReturnType]
のように使う - Tuple は
Tuple[int, int, str]
のように要素の型を示すのに使う。任意の長さの同一型のタプル (homogeneous tuples) は単一の型と省略記号を使ってTuple[int, ...]
のように表します。(ここでの...
はリテラルの省略記号で構文の一部です。) - TypeVar は
X = TypeVar('X', Type1, Type2, Type3)
または簡潔にY = TypeVar('Y')
のように使う (詳細は上述した節を参照) - ジェネリックはユーザー定義のジェネリッククラスを作るのに使う
組み込みコレクションのジェネリック版:
- Dict は
Dict[key_type, value_type]
のように使う - List は
List[element_type]
のように使う - Set は
Set[element_type]
のように使う。以下のAbstractSet
の所見を参照してください。 - FrozenSet は
FrozenSet[element_type]
のように使う
注意: Dict
、List
、Set
と FrozenSet
は主に戻り値をアノテートするのに便利です。引数には以下で定義される抽象コレクション型の方が適切です。例えば Mapping
、Sequence
または AbstractSet
です。
コンテナー抽象基底クラスのジェネリック版 (とコンテナー以外のもの):
- ByteString
- Callable (上記を参照、ここでは網羅性のために記述)
- Container
- Generator は
Generator[yield_type, send_type, return_type]
のように使う。これはジェネレーター関数の戻り値を表現します。それはIterable
のサブタイプであり、send()
メソッドが受け付ける型 (それは反変です。Employee
インスタンスを送信するのを許容するジェネレーターはManager
インスタンスを送信するのを許容するジェネレータを必要とするコンテキストにおいて有効)と、ジェネレーターの戻り型に対応する追加の型変数を持ちます。 - Hashable (ジェネリックではないが、網羅性のために記述)
- ItemsView
- Iterable
- Iterator
- KeysView
- Mapping
- MappingView
- MutableMapping
- MutableSequence
- MutableSet
- Sequence
- Set は
AbstractSet
に名前変更される。この名前変更はtyping
モジュールのSet
はジェネリクスのset()
を意味するために必要になりました。 - Sized (ジェネリックではないが、網羅性のために記述)
- ValuesView
単一の特殊メソッドを検査するためにいくつかの1回限りの型が定義されています (Hashable
や Sized
と同様) :
- Reversible は
__reversed__
の検査に使う - SupportsAbs は
__abs__
の検査に使う - SupportsComplex は
__complex__
の検査に使う - SupportsFloat は
__float__
の検査に使う - SupportsInt は
__int__
の検査に使う - SupportsRound は
__round__
の検査に使う - SupportsBytes は
__bytes__
の検査に使う
便利な定義:
- Optional は
Optional[t] == Union[t, type(None)]
により定義される - AnyStr は
TypeVar('AnyStr', str, bytes)
として定義される - NamedTuple は
NamedTuple(type_name, [(field_name, field_type), ...])
のように使い、collections.namedtuple(type_name, [field_name, ...])
と等価です。名前付きタプルのフィールドの型を宣言するのに便利です。 - cast() は前の節で説明しました
- @no_type_check はクラスまたは関数の型チェックを無効にするデコレーター (以下を参照)
-
@no_type_check_decorator は
@no_type_check
と同じ意味を持つ独自デコレーターを作成するためのデコレーター (以下を参照) - @overload は前の節で説明しました
- get_type_hints() は関数またはメソッドから型ヒントを取得するためのユーティリティ関数です。関数またはメソッドオブジェクトを渡すと
__annotations__
と同じフォーマットのディクショナリを返します。しかし、元の関数またはメソッド定義のコンテキストの式として (文字列リテラルとして渡された) 前方参照を評価します。
typing.io
サブモジュールで利用可能な型:
- IO (
AnyStr
のジェネリック) - BinaryIO (
IO[bytes]
の単純なサブタイプ) - TextIO (
IO[str]
の単純なサブタイプ)
typing.re
サブモジュールで利用可能な型:
- Match と Pattern は
re.match()
の型とre.compile()
の結果 (AnyStr
のジェネリック)
却下された代替案
この PEP の初期の草稿に対する議論中に様々な反論がなされ、いくつもの代替案が提案されました。ここではそういったものについて議論し、なぜそれらを却下したのかについて説明します。
主要な反論をいくつか取り上げました。
ジェネリックの型パラメーターにどの括弧を使う?
ほとんどの人は C++, Java, C# や Swift といった言語でジェネリック型のパラメーターを表現する山括弧 (例: List<int>
) に馴染みがあります。この問題は山括弧をパースするのが本当に難しいということです。 Python のような素朴なパーサーでは特にそうです。ほとんどの言語では、特別な構文的な位置では山括弧のみを許容し任意の式が許容されないようにしてその曖昧さに対処するのが普通です。 (さらにコードの任意の部分をバックトラックできる強力なパース技術も使います。)
しかし、Pythonでは型の式とその他の式を (構文的に) 同じにしたいです。それにより、例えば型の別名を作成するために変数に代入できるようになります。この単純な型の式を考えてみましょう。
List<int>
Python のパーサーの観点からは、この式は連鎖した比較と同じ4つのトークン (NAME, LESS, NAME, GREATER) から始まります。
a < b > c # I.e., (a < b) and (b > c)
両方の方法でパースできる例も作れます。
a < b > [ c ]
言語に山括弧を持っていると仮定すると、このコードは次の2通りのいずれかに解釈できます。
(a<b>)[c] # I.e., (a<b>).__getitem__(c)
a < b > ([c]) # I.e., (a < b) and (b > [c])
確かにそういったケースの曖昧さをなくすためにルールを考えることはできます。しかし、多くのユーザーにとってそのルールは任意であり複雑なものに感じます。またこのために CPython のパーサーの劇的な変更も必要とします (そして Python 向けの他のすべてのパーサーも) 。Python の現在のパーサーは意図的に "バカ" であるのに注意するべきです。つまり簡潔な文法はユーザーにとって推測しやすいものだということです。
こういったすべての理由から、角括弧 (例: List[int]
) がジェネリクスの型パラメーターの構文に選ばれました (そして昔からずっと好まれていました) 。これはメタクラスで __getitem__()
メソッドを定義することで実装でき、新しい構文を全く必要としません。このオプションは最近の全 Python バージョン (Python 2.2 から始まる) で動作します。またこの構文を選択しているのは Python だけでもありません。Scala もジェネリッククラスに角括弧を使います。
アノテーションの既存用途についてはどうする?
議論の1つに PEP 3107 が関数アノテーションで任意の式の利用を明示的にサポートするという内容を指摘します。新たな提案は PEP 3107 とは非互換であると考えられます。
このことに対する返答として、まず第一に現在の提案が直接的な非互換を発生させるものではないということです。そのため、Python 3.4 でアノテーションを使っているプログラムは Python 3.5 でも不利益なくそのまま正常に動作します。
最終的には型ヒントがアノテーションの唯一の用途となることを期待しています。しかし、そうするには Python 3.5 と共に初期の typing モジュールを公開した後で追加の議論と廃止期間を必要とします。現在の PEP は Python 3.6 がリリースされるまでの暫定的なステータスとなるでしょう (PEP 411 を参照) 。最速で考えられる計画は、Python 3.6 で非型ヒントのアノテーションに初期の廃止期間を導入し、3.7 で完全な廃止期間とし、3.8 でアノテーションの用途として型ヒントの宣言のみを許可します。アノテーションを使っているパッケージの作者へ他のアプローチを考案するために十分な時間を与える必要があります。もし型ヒントが急速に成功を収めたとしてもそうです。
別の可能性のある結果は、最終的に型ヒントがアノテーションのデフォルトの意味となるが、型ヒントを無効にするためのオプションを必ず維持することです。この目的のために現在の提案は、クラスまたは関数で型ヒントとしてのアノテーションのデフォルトの解釈を無効にする @no_type_check
というデコレーターを定義しています。さらに @no_type_check_decorator
というメタデコレーターも定義していて、これはデコレーターをデコレートします (!) 。このメタデコレーターで作られたデコレーターでデコレートされた任意の関数またはクラスのアノテーションは型チェッカーによって無視されるようになります。
また # type: ignore
というコメントもあります。そして静的型チェッカーは選択したパッケージの型チェックを無効にする設定オプションをサポートすべきです。
これらのすべてのオプションにも関わらず、型ヒントとその他のアノテーションの形式を個別の引数に対して共存できるようにするという提案が後を絶ちません。ある提案は、もしある引数に対するアノテーションが辞書リテラルなら、それぞれのキーが異なるアノテーションの形式を表し、そのキーが 'type'
であれば型ヒントに使うといったものでした。このアイディアおよびその亜種の問題点は、その表記法がとてもノイズが多く読み難いということです。またアノテーションを使う既存のライブラリのほとんどのケースにおいて、型ヒントとそれらのライブラリを組み合わせる必要性は小さいものでしょう。そのため、選択的に型ヒントを無効にする簡潔な手法で十分にみえます。
前方参照の問題
型ヒントが前方参照を含む必要がある場合、現在の提案は明らかに次善の策です。 Python はすべての名前が使われるときにそれらが定義されている必要があります。循環インポートは別にして、これはめったに問題とはなりません: ここでの "使われる" は "実行時に見つかる" ことを意味します。ほとんどの "前方" 参照は、その名前を使う関数が呼び出される前に、その名前が定義されていることを保証すれば問題はありません。
型ヒントにおける問題は、 (PEP 3107 を通して、またデフォルト引数も同様) 関数が定義された時点でアノテーションが評価され、アノテーションで使われる任意の名前はその関数が定義された時点で予め定義されていなければならないということです。よくあるシナリオは、そのクラスのアノテーションにクラス自身の参照を必要とするメソッドを持つクラス定義です。(もっと一般的に言うと相互再帰なクラスでも発生します。)これはコンテナー型にとっては自然なものです。例えば、
class Node:
"""Binary tree node."""
def __init__(self, left: Node, right: Node):
self.left = left
self.right = right
以前書いたようにこれは動作しません。それは、クラス名はそのクラスの本体全体が実行された時に初めて定義されるという Python の特性によるものです。我々の解決策は、アノテーションで文字列リテラルを使えるようにすることです(これは特に洗練されたものではありませんが、しっかりと役割を果たします)。ただし、ほとんどの場合これを使う必要はありません。型ヒントの大半の 使用 は、組み込み型または他のモジュールで定義された型を参照することを想定しています。
ある反対提案は、型ヒントが実行時に一切評価されないようにその意味論を変更しようとするものでした (結局のところ型チェックはオフラインで行われるのだから、型ヒントが実行時に評価される必要はあるのだろうか) 。これはもちろん後方互換性の問題に抵触します。特定のアノテーションが型ヒントまたはその他になるかどうかを Python インタープリターは実際には知らないからです。
次のように __future__
インポートで所定モジュールの すべての アノテーションを文字列リテラルに変えられるようにするといった妥協案はあり得ます。
from __future__ import annotations
class ImSet:
def add(self, a: ImSet) -> List[ImSet]:
...
assert ImSet.add.__annotations__ == {'a': 'ImSet', 'return': 'List[ImSet]'}
そういった __future__
インポート文が別の PEP で提案されるかもしれません。
二重コロン
数人が創造性を発揮してこの問題の解決策を発明しようと挑戦しました。例えば、同時に2つの問題を解決するために二重コロン (::
) を型ヒントに使うことが提案されました: 他のアノテーションと型ヒントの間で曖昧さをなくすことと、実行時評価が起きないように意味論を変えることです。しかし、この考えには不適切なこともいくつかありました。
- これは醜いです。Python では単一コロンが多く使われます。そういった用法のすべては英語のテキストのコロンの用法と似ているため馴染みがあります。これは、区切り記号のほとんどの形式で Python が遵守している一般的なやり方です (例外は典型的には他のプログラミング言語から伝わってきたものです)。しかし、ここでの
::
の利用は英語では奇妙であり、他の言語 (例: C++) でも二重コロンは全く異なるスコープ演算子として使われます。これとは対照的に、型ヒントの単一コロンは自然に読めます。そして、単一コロンはこの目的のために慎重に設計されているから当然のことです (PEP 3107 [gvr-artima] よりかなり古くからのアイディア) 。その他の言語においても Pascal から Swift まで同じように使われています。 - 戻り型のアノテーションはどうする?
- 型ヒントが実行時に評価されることは実際には特徴の1つです。
- 型ヒントが実行時に利用できることによって、型ヒント上に実行時型チェッカーを構築できます。
- 型チェッカーが実行されないときでも誤りを捉えられます。型チェッカーは独立したプログラムであるため、ユーザーは型チェッカーを実行しない (またはインストールしない) かもしれませんが、簡潔なドキュメントとして型ヒントを使いたいときもあるでしょう。壊れた型ヒントはドキュメントにさえも使えません。
- これは新たな構文なので、二重コロンを型ヒントに使うことは Python 3.5 でしか動かないコードに制限してしまいます。既存の構文を使うことにより、現在の提案を Python 3 の古いバージョンでも簡単に動かせます。(実際に mypy は Python 3.2 以上をサポートします)
- もし型ヒントが成功を収めたら、将来的に変数の型を宣言するための新しい構文を追加するかもしれません。例えば、
var age: int = 42
のような構文です。もし型ヒントの引数に二重コロンを使ったら、一貫性のために将来の構文でも同じ規則を使う必要があるでしょう。そして醜さが永続します。
新たな構文の他の形態
その他にもいくつかの代替となる構文が提案されました。例えば、予約語 where
[roberge] や Cobra に触発された requires
句の導入です。しかし、Python 3 の初期バージョンで動作しないという二重コロンと同じ問題があります。同じことが、新たな __future__
インポートにも適用されます。
その他の後方互換性規則
提案中のアイディアを含みます。
- デコレーター、例えば、
@typehints(name=str, returns=str)
のようなものです。これは動作しますが、あまりに冗長 (余分な行と引数名を繰り返さなければなりません) であり、洗練された PEP 3107 の表記法とは似ても似つかないものです。 - スタブファイル。スタブファイルはほしいですが、それは主に型ヒントを追加しない既存コードに型ヒントを追加するのに役立つものです。例えば、サードパーティパッケージ、Python 2 と Python 3 の両方をサポートするコード、特に拡張モジュールです。ほとんどの状況では、関数定義の場所にアノテーションがあった方がはるかに便利です。
- ドキュメンテーション文字列。Sphinx 表記 (
:type arg1: description
) を基にした docstrings 向けの規約が既にあります。これはかなり冗長 (1パラメーターにつき1行いる) で洗練されていません。何か新しい規則を作るかもしれませんが、アノテーション構文を打ち破るのは難しいです (正にこの目的のために設計されたのだから) 。
あるいは単純に次のリリースを待つことが提案されています。しかし、それはどのような問題を解決するのでしょうか? 単に先送りに過ぎないでしょう。
PEP 開発プロセス
この PEP の生の草稿は [github] 上にあります。イシュートラッカー [issues] もあり、多くの技術的な議論がそこで行われます。
GitHub 上の草稿は定期的に少しずつ更新されます。公式 PEPS リポジトリ [peps] は (通常) python-dev に新しい草稿が投稿されたときのみ更新されます。
謝辞
このドキュメントは Jim Baker, Jeremy Siek, Michael Matson Vitousek, Andrey Vlasovskikh, Radomir Dopieralski, Peter Ludemann と BDFL 委任の Mark Shannon からの貴重な意見、励ましや助言なくして完成することはありませんでした。
PEP 482 で言及したライブラリやフレームワーク、既存の言語から影響を受けています。アルファベット順でその作者たちへ感謝を述べます: Stefan Behnel, William Edwards, Greg Ewing, Larry Hastings, Anders Hejlsberg, Alok Menghrajani, Travis E. Oliphant, Joe Pamer, Raoul-Gabriel Urma と Julien Verlaguet
参考文献
著作権
このドキュメントはパブリックドメインに置かれています。