Python
 Computer >> コンピューター >  >> プログラミング >> Python

Pythonの関数アノテーション徹底解説:基本構文から実践的な活用術まで

Python 3.0で導入された関数アノテーションは、関数の引数(パラメータ)や戻り値に任意のメタデータを付加できる機能です。PEP-3107としてPython 3に正式に取り入れられ、その主な目的は、関数のパラメータや戻り値にメタデータを関連付けるための標準的な記法を提供することにありました。

関数アノテーションの基本

まず、押さえておきたい基本的なポイントを整理しましょう。

  • 関数アノテーションは、引数・戻り値のどちらに対しても完全に任意(省略可能)です。

  • コンパイル時に、関数の各部分と任意のPython式を関連付ける手段を提供します。

  • PEP-3107は、組み込み型を含めて標準的な意味づけ(セマンティクス)を定義していません。実際の解釈や活用はサードパーティライブラリに委ねられています。

アノテーションの構文

通常の引数へのアノテーション

引数へのアノテーションは、コロン「:」を使って次のように記述します。

def foo(x: expression, y: expression = 20):
    ....

可変長引数へのアノテーション

*args や **kwargs のような可変長引数にも、同様にアノテーションを付けられます。

def foo(*args: expression, **kwargs: expression):
    .....

ネストした引数の場合

ネストした引数の場合、アノテーションは常にパラメータ名の直後に置かれます。また、ネストした引数のすべてにアノテーションを付ける必要はありません。

def foo(x1, y1: expression, x2: expression = None):
    ......

ここで重要なのは、Python自体はアノテーションに対していかなる意味づけも行わないという点です。Pythonが提供するのは、メタデータを関連付けるための美しい構文サポートと、それへアクセスする簡単な方法だけです。さらに、アノテーションは必須ではありません。

>>> def func(x:'annotating x', y: 'annotating y', z: int) -> float: print(x + y + z)

上の例では、関数func()はx、y、zという3つの引数を受け取り、その合計を出力します。第1引数xには文字列'annotating x'、第2引数yには文字列'annotating y'、第3引数zにはint型がアノテーションとして付けられています。戻り値にはfloat型がアノテーションされており、「->」という構文で表現しています。

実行結果

>>> func(2,3,-4)
1
>>> func('Function','-','Annotation')
Function-Annotation

上記では、func()をint型の引数で1回、文字列型の引数でもう1回呼び出しています。いずれの場合もfunc()は正しく動作し、アノテーションは単純に無視されます。つまり、アノテーションは関数func()の実行には何の影響も与えないことが分かります。

関数アノテーションへのアクセス

すべてのアノテーションは__annotations__という辞書に格納されます。これは関数自身の属性になっています。

>>> def func(x:'annotating x', y: 'annotating y', z: int) -> float: print(x + y + z)
>>> func.__annotations__
{'x': 'annotating x', 'y': 'annotating y', 'z': <class 'int'>, 'return': <class 'float'>}

上のコード例から分かるように、アノテーションは型宣言ではありません。ただし、その目的で使うことも当然可能で、他の言語における型指定の構文にもよく似ています。次の例を見てください。

>>> def func(a: 'python', b: {'category': 'language'}) -> 'yep':
    pass
>>> func.__annotations__
{'a': 'python', 'b': {'category': 'language'}, 'return': 'yep'}
>>>

アノテーションは任意の式であり、つまり__annotations__辞書にはどんな値でも格納できます。Python本体にとっては「値を格納する」という以上の意味はありませんが、引数と戻り値の型を定義することは、関数アノテーションの最も一般的な用途となっています。

@no_type_checkデコレータ

もし利用しているツールがアノテーションを型宣言だとみなすのに、自分は別の目的でアノテーションを使いたい場合は、標準の@no_type_checkデコレータを使って、その関数を型チェックの対象から除外できます。

>>> from typing import no_type_check
>>> @no_type_check
def func(a: 'python', b: {'category': 'language'}) -> 'yep':
    pass
>>>

通常、これは必要ありません。アノテーションを利用するほとんどのツールには、自分向けのアノテーションを認識する仕組みが備わっているからです。このデコレータは、曖昧さが生じるエッジケースを保護するためのものです。

関数デコレータへの入力としてのアノテーション

アノテーションはデコレータとも相性が抜群です。アノテーションの値はデコレータに入力を渡す優れた手段になり、デコレータが生成するラッパーは、アノテーションに意味を持たせるコードを配置するのに適した場所だからです。

from functools import wraps

def adapted(func):
    @wraps(func)
    def wrapper(**kwargs):
        final_args = {}
        for name, value in kwargs.items():
            adapt = func.__annotations__.get(name)
            if adapt is not None:
                final_args[name] = adapt(value)
            else:
                final_args[name] = value
        result = func(**final_args)
        adapt = func.__annotations__.get('result')
        if adapt is not None:
            return adapt(result)
        return result
    return wrapper

@adapted
def func(a: int, b: repr) -> str:
    return a

このadaptedデコレータは、関数をラッパーで包み込みます。ラッパーはキーワード引数のみを受け付けるため、元の関数が位置引数を受け取れる場合でも、引数は必ず名前で指定しなければなりません。

関数がラップされると、ラッパーは関数の引数アノテーションの中から変換処理(アダプタ)を探し、実際の関数に引数を渡す前にそれを適用します。

関数が値を返した後、ラッパーは戻り値用のアダプタがないかを確認し、見つかった場合は最終的に返却する前にそれを適用します。

ここで起きていることの意味を考えてみると、かなり印象的です。私たちは実際に、「関数に引数を渡す」「値を返す」という動作の意味そのものを変更しているのです。

キーワード引数の自動保存

ときには、メソッドの引数のうち1つ以上が、selfの属性に代入する以外の処理を一切必要としないことがあります。デコレータとアノテーションを使って、これを自動化できないでしょうか?もちろん、できます。

from functools import wraps

def store_args(func):
    @wraps(func)
    def wrapper(self, **kwargs):
        for name, value in kwargs.items():
            attrib = func.__annotations__.get(name)
            if attrib is True:
                attrib = name
            if isinstance(attrib, str):
                setattr(self, attrib, value)
        return func(self, **kwargs)
    return wrapper

class A:
    @store_args
    def __init__(self, first: True, second: 'example'):
        pass

a = A(first=5, second=6)
assert a.first == 5
assert a.example == 6

store_argsデコレータを使えば、__init__メソッドの引数firstは自動的にself.firstへ、secondはアノテーションで指定した属性名self.exampleへと保存されます。このように、アノテーションをデコレータと組み合わせることで、定型コードを大幅に削減でき、より宣言的で読みやすいコードを実現できます。

  1. Pythonのissubset()関数とは?部分集合の判定方法を実例付きで解説

    本記事では、Python標準ライブラリに用意されているissubset()関数の仕組みと使い方について詳しく解説します。 issubset()メソッドは、あるセット(集合)のすべての要素が、引数として渡した別のセットにも含まれている場合にブール値のTrueを返し、1つでも含まれていない要素があればFalseを返します。 下の図では、BはAの部分集合です。もしAとBが同一のセットであれば、両者は互いに部分集合の関係にあるといえます。つまり、両方のセットがまったく同じ要素を持っているということを意味します。 構文 <set 1>.issubset(<set 2>) 戻り値

  2. Pythonのintersection()関数とは?集合の共通要素(積集合)を求める方法

    この記事では、Pythonのセット(集合)に対して実行できるintersection()関数について詳しく解説します。数学における「積集合(インターセクション)」とは、2つの集合から共通する要素を見つけ出すことを指します。構文<セット名>.intersection(<セットa1>, <セットa2>, …)戻り値引数として渡されたすべての集合に共通する要素(積集合)が、新しいセットとして返されます。使用例set_1 = {t,u,t,o,r,i,a,l} set_2 = {p,o,i,n,t} set_3 = {t,u,t} # 2つの集合の積集合 print