アノテーションのベストプラクティス
- 著者
- ラリーヘイスティングス
概要
このドキュメントは、注釈辞書を操作するためのベストプラクティスをカプセル化するように設計されています。 Pythonオブジェクトで__annotations__
を調べるPythonコードを作成する場合は、以下に説明するガイドラインに従うことをお勧めします。
このドキュメントは、Pythonバージョン3.10以降のオブジェクトのアノテーションにアクセスするためのベストプラクティス、Pythonバージョン3.9以前のオブジェクトのアノテーションにアクセスするためのベストプラクティス、__annotations__
のその他のベストプラクティスの4つのセクションで構成されています。これはすべてのPythonバージョンに適用され、__annotations__
の癖があります。
このドキュメントは、__annotations__
の操作に関するものであり、 for アノテーションを使用していないことに注意してください。 コードで「タイプヒント」を使用する方法についての情報をお探しの場合は、タイピングモジュールを参照してください。
Python3.10以降でオブジェクトのアノテーションディクトにアクセスする
Python 3.10は、標準ライブラリに新しい関数 inspect.get_annotations()を追加します。 Pythonバージョン3.10以降では、この関数を呼び出すことが、アノテーションをサポートするオブジェクトのアノテーションdictにアクセスするためのベストプラクティスです。 この関数は、文字列化された注釈を「文字列化解除」することもできます。
何らかの理由で inspect.get_annotations()がユースケースで実行できない場合は、
__annotations__
データメンバーに手動でアクセスできます。 このためのベストプラクティスはPython3.10でも変更されました。Python3.10以降、o.__annotations__
は常に Python関数、クラス、およびモジュールで動作することが保証されています。 調べているオブジェクトがこれら3つの固有のオブジェクトのいずれかであることが確実な場合は、o.__annotations__
を使用してオブジェクトの注釈辞書を取得できます。ただし、他のタイプの呼び出し可能オブジェクト(たとえば、 functools.partial()によって作成された呼び出し可能オブジェクト)には、
__annotations__
属性が定義されていない場合があります。 未知の可能性のあるオブジェクトの__annotations__
にアクセスする場合、Pythonバージョン3.10以降でのベストプラクティスは、getattr(o, '__annotations__', None)
などの3つの引数を指定して getattr()を呼び出すことです。
Python3.9以前のオブジェクトのアノテーションディクトへのアクセス
Python 3.9以前では、オブジェクトのアノテーションdictへのアクセスは、新しいバージョンよりもはるかに複雑です。 問題は、これらの古いバージョンのPythonの設計上の欠陥であり、特にクラスの注釈に関係しています。
inspect.get_annotations()を呼び出さないと仮定すると、他のオブジェクト(関数、他の呼び出し可能オブジェクト、モジュール)の注釈辞書にアクセスするためのベストプラクティスは、3.10のベストプラクティスと同じです。3つを使用する必要があります。 -引数 getattr()を使用して、オブジェクトの
__annotations__
属性にアクセスします。残念ながら、これはクラスのベストプラクティスではありません。 問題は、
__annotations__
はクラスではオプションであり、クラスは基本クラスから属性を継承できるため、クラスの__annotations__
属性にアクセスすると、のアノテーション辞書が誤って返される可能性があることです。 ]基本クラス。例として:class Base: a: int = 3 b: str = 'abc' class Derived(Base): pass print(Derived.__annotations__)
これにより、
Derived
ではなく、Base
から注釈辞書が出力されます。調べているオブジェクトがクラス(
isinstance(o, type)
)の場合、コードには個別のコードパスが必要です。 その場合、ベストプラクティスは、Python 3.9以前の実装の詳細に依存します。クラスにアノテーションが定義されている場合、それらはクラスの__dict__
ディクショナリに格納されます。 クラスにはアノテーションが定義されている場合とされていない場合があるため、ベストプラクティスは、クラスdictでget
メソッドを呼び出すことです。すべてをまとめるために、Python3.9以前の任意のオブジェクトの
__annotations__
属性に安全にアクセスするサンプルコードを次に示します。if isinstance(o, type): ann = o.__dict__.get('__annotations__', None) else: ann = getattr(o, '__annotations__', None)
このコードを実行した後、
ann
は辞書またはNone
のいずれかになります。 さらに調べる前に、 isinstance()を使用してann
のタイプを再確認することをお勧めします。一部のエキゾチックまたは不正な型のオブジェクトには
__dict__
属性がない場合があるため、安全性を高めるために、 getattr()を使用して__dict__
にアクセスすることもできます。
文字列化された注釈の文字列化を手動で解除する
一部のアノテーションが「文字列化」されている可能性があり、それらの文字列を評価してそれらが表すPython値を生成する場合は、 inspect.get_annotations()を呼び出してこの作業を行うのが最適です。
Python 3.9以前を使用している場合、または何らかの理由で inspect.get_annotations()を使用できない場合は、そのロジックを複製する必要があります。 現在のPythonバージョンでの inspect.get_annotations()の実装を調べて、同様のアプローチに従うことをお勧めします。
一言で言えば、任意のオブジェクトの文字列化された注釈を評価したい場合
o
:
o
がモジュールの場合、 eval()を呼び出すときにglobals
としてo.__dict__
を使用します。o
がクラスの場合、 eval( )。o
が、 functools.update_wrapper()、 functools.wraps()、または functools.partial()を繰り返し使用して、ラップされた呼び出し可能である場合ルートアンラップ関数が見つかるまで、必要に応じてo.__wrapped__
またはo.func
にアクセスしてアンラップします。o
が呼び出し可能(クラスではない)の場合、 eval()を呼び出すときにグローバルとしてo.__globals__
を使用します。ただし、 eval()によって、アノテーションとして使用されるすべての文字列値をPython値に正常に変換できるわけではありません。 文字列値には理論的には任意の有効な文字列を含めることができ、実際には、特に評価できない文字列値で注釈を付ける必要があるタイプヒントの有効な使用例があります。 例えば:
- PEP 604 | を使用する共用体型。これは、Python3.10にサポートが追加される前のものです。
- 実行時に不要な定義。 typing.TYPE_CHECKING がtrueの場合にのみインポートされます。
eval()がそのような値を評価しようとすると、失敗して例外が発生します。 したがって、アノテーションを処理するライブラリAPIを設計するときは、呼び出し元から明示的に要求された場合にのみ文字列値の評価を試みることをお勧めします。
Pythonバージョンでの__annotations__のベストプラクティス
- オブジェクトの
__annotations__
メンバーに直接割り当てることは避けてください。 Pythonに設定__annotations__
を管理させます。- オブジェクトの
__annotations__
メンバーに直接割り当てる場合は、常にdict
オブジェクトに設定する必要があります。- オブジェクトの
__annotations__
メンバーに直接アクセスする場合は、その内容を調べる前に、それが辞書であることを確認する必要があります。__annotations__
辞書の変更は避けてください。- オブジェクトの
__annotations__
属性は削除しないでください。
__annotations__癖
Python 3のすべてのバージョンで、関数オブジェクトはレイジーです-そのオブジェクトにアノテーションが定義されていない場合は、アノテーションを作成します。
del fn.__annotations__
を使用して__annotations__
属性を削除できますが、fn.__annotations__
にアクセスすると、オブジェクトは新しい空のdictを作成し、それを保存して注釈として返します。 アノテーション辞書を遅延作成する前に関数のアノテーションを削除すると、AttributeError
がスローされます。del fn.__annotations__
を2回続けて使用すると、常にAttributeError
がスローされることが保証されます。上記の段落のすべては、Python3.10以降のクラスおよびモジュールオブジェクトにも適用されます。
Python 3のすべてのバージョンで、関数オブジェクトの
__annotations__
をNone
に設定できます。 ただし、その後fn.__annotations__
を使用してそのオブジェクトの注釈にアクセスすると、このセクションの最初の段落に従って空の辞書が遅延作成されます。 これは、どのPythonバージョンでも、モジュールとクラスには当てはまりません。 これらのオブジェクトは、__annotations__
を任意のPython値に設定することを許可し、設定された値を保持します。Pythonが(
from __future__ import annotations
を使用して)注釈を文字列化し、文字列を注釈として指定すると、文字列自体が引用符で囲まれます。 事実上、注釈は 2回引用されます。例:from __future__ import annotations def foo(a: "str"): pass print(foo.__annotations__)
{'a': "'str'"}
を印刷します。 これは実際には「癖」と見なされるべきではありません。 意外かもしれないという理由だけでここで言及されています。