アノテーションのベストプラクティス—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/3.10/howto/annotations
移動先:案内検索

アノテーションのベストプラクティス

著者
ラリーヘイスティングス

概要

このドキュメントは、注釈辞書を操作するためのベストプラクティスをカプセル化するように設計されています。 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'"}を印刷します。 これは実際には「癖」と見なされるべきではありません。 意外かもしれないという理由だけでここで言及されています。