[Python]関数構造の取得について調べてみた（PEP362）。

都合によりPythonのCallable（関数とメソッド）の構造を取得する制御が必要になったので調べてみました（[PEP362 Function Signature Object](https://www.python.org/dev/peps/pep-0362/#references)）。

# 使うものとバージョンについて

Pythonビルトインのinspectパッケージを利用していきます。今回使うinspect.SignatureなどのものはPython3.3以降で追加されています。

また、本記事では型アノテーションを絡めた点にも触れていきます。型アノテーションがPython3.5以降からのサポートとなります。

# import

必要なものはinspectパッケージに諸々入っていますのでそちらのimportで大体対応ができます。

```py
import inspect
```

# Signatureインスタンス

`inspect.signature`関数の第一引数にCallableのオブジェクトを指定することで、その関数（メソッド）の引数などの情報を保持したSignatureインスタンスを取得することができます。

まずは引数も返却値も無い関数で試してみます。

```py
def sample_func_1():
    ...


signature = inspect.signature(sample_func_1)
```

返却値の型は`inspect.Signature`となります。

```py
>>> type(signature)
inspect.Signature

>>> isinstance(signature, inspect.Signature)
True
```

printなどを通してみると、引数の情報などが表示されます。今回は引数も返却値も持っていない関数なので空の括弧の`()`が出力されます。

```py
>>> print(signature)
()
```

試しに引数を受け付ける関数を用意してそちらを試してみます。

```py
def sample_func_2(a, b):
    ...


signature = inspect.signature(sample_func_2)
```

printしてみると、Signatureインスタンスがaとbという二つの引数の情報を保持していることを確認できます。

```py
>>> print(signature)
(a, b)
```

より詳細な引数の値を取りたい場合にはparameters属性で参照することができます。

```py
>>> signature.parameters
mappingproxy({'a': <Parameter "a">, 'b': <Parameter "b">})
```

`mappingproxy`というあまり聞きなれないものが設定されていますが、`mappingproxy`は値の更新が効かない辞書といったオブジェクトになります（`__setattr__`メソッドの付いていない辞書となります）。

キーには引数名（今回は`a`と`b`）、値にParameterインスタンスが設定されます（Parameterインスタンスは後々の節で説明します）。

基本的には辞書と同じなので、len関数などを使うと引数の件数が取得することができます。

```py
>>> len(signature.parameters)
2
```

引数の無い関数で確認してみれば空のmappingproxyになっていることが確認できます。

```py
>>> signature = inspect.signature(sample_func_1)
>>> signature.parameters
mappingproxy({})

>>> len(signature.parameters)
0
```

また、Python3.6や3.7辺りからPythonの辞書は順番を保持するようになっています。そのためそのままループを回せば引数順通りにParameterインスタンスにアクセスすることができます。

それ以前のPythonバージョン（3.4や3.5など）であればcollectionsパッケージのOrderDictが設定されるようになっています。型は厳密には異なりますが、古いバージョンでも引数順は保たれた形の辞書として設定されます。

返却値の情報に関してはreturn_annotation属性で取ることができます。ただし型アノテーションの有無に依存します。

例えば以下のようにreturnの記述はあるものの型アノテーションが無い状態の関数のsignatureインスタンスでは`_empty`という型の値になります。

```py
def sample_func_3(a, b):
    return 100


signature = inspect.signature(sample_func_3)
```

```py
>>> signature.return_annotation
inspect._empty
```

空かどうかといった判定をしたい場合にはStructureクラスのempty属性と同じ型の値となるようです。

```py
>>> signature.return_annotation == inspect.Signature.empty
True
```

return_annotation属性自体は型（クラス）なのでtype関数を通すとtypeと表示されます。

```py
>>> type(signature.return_annotation)
type
```

試しに返却値に型アノテーションをして試してみます。

```py
def sample_func_4(a, b) -> int:
    ...


signature = inspect.signature(sample_func_4)
```

正しく型などが取れていることが確認できます。

```py
>>> signature.return_annotation
int

>>> type(signature.return_annotation)
type
```

また、signatureインスタンス自体をprintした際の表示なども変わります。

```py
>>> print(signature)
(a, b) -> int
```

typingパッケージのもの（例えばtyping.Tupleなど）を使ってアノテーションをした場合には、そのクラス（たとえばtuple）ではなくそのままtypingパッケージのものが設定されるようです。

```py
from typing import Tuple


def sample_func_5(a, b) -> Tuple[int, str]:
    ...


signature = inspect.signature(sample_func_5)
```


```py
>>> type(signature.return_annotation)
typing.TupleMeta

>>> print(signature)
(a, b) -> Tuple[int, str]
```

引数に対する型アノテーションを確認してみます。

```py
def sample_func_6(a: str, b: int) -> Tuple[int, str]:
    ...


signature = inspect.signature(sample_func_6)
```

こちらも返却値と同様に、printなどを通してみると内容が反映されています。

```py
>>> print(signature)
(a: str, b: int) -> Tuple[int, str]
```

デフォルト値についても試してみます。こちらも同様にprintなどで結果に反映されます。

```py
def sample_func_7(a: str = 'cat', b: int = 10) -> Tuple[int, str]:
    ...


signature = inspect.signature(sample_func_7)
```

```py
>>> print(signature)

(a: str = 'cat', b: int = 10) -> Tuple[int, str]
```

# Parameterインスタンス

引数の詳細は前述の通りSignatureインスタンスのparameters属性でアクセスができます。

```py
>>> signature.parameters
mappingproxy({'a': <Parameter "a: str = 'cat'">,
              'b': <Parameter "b: int = 10">})
```

辞書（mappingproxy）に設定されている値の型はinspect.Parameterクラスのインスタンスとなります。

```py
>>> isinstance(signature.parameters['a'], inspect.Parameter)
True
```

mappingproxyという特殊な型ですが、値の更新以外は通常の辞書と同じインターフェイスでアクセスができます。

```py
>>> for arg_name, parameter in signature.parameters.items():
...     print('-' * 20)
...     print('Argument name:', arg_name)
...     print('Parameter:', parameter)

--------------------
Argument name: a
Parameter: a: str = 'cat'
--------------------
Argument name: b
Parameter: b: int = 10
```

Parameterインスタンスでは、name属性で引数名にアクセスすることができます。

```py
>>> parameter_a = signature.parameters['a']
>>> parameter_a.name
'a'
```

返却値のreturn_annotationのように、annotation属性で型のアノテーション内容を取得することができます。

```py
>>> parameter_a.annotation
str
```

default属性ではデフォルト値に設定されている値を取得することができます。

```py
>>> parameter_a.default
'cat'
```

kind属性では引数の種類がどんな種類なのかが設定されています。例えばビルトインの関数やパッケージなどでちらほら損雑する位置指定引数（positional argument）のみ受け付ける引数や、逆にキーワード引数のみ受け付ける設定になっている引数、両方とも受け付けてくれる（一番一般的な）引数などの種類が取れます。

```py
>>> parameter_a.kind
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>
```

設定される値はParameterクラスに設定されているEnumの値で、以下のような0～4の定義になっています。

```py
>>> inspect.Parameter.POSITIONAL_ONLY
<_ParameterKind.POSITIONAL_ONLY: 0>

>>> inspect.Parameter.POSITIONAL_OR_KEYWORD
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>

>>> inspect.Parameter.VAR_POSITIONAL
<_ParameterKind.VAR_POSITIONAL: 2>

>>> inspect.Parameter.KEYWORD_ONLY
<_ParameterKind.KEYWORD_ONLY: 3>

>>> inspect.Parameter.VAR_KEYWORD
<_ParameterKind.VAR_KEYWORD: 4>
```

POSITIONAL_ONLYは位置指定引数のみ受け付ける引数です。ビルトインの関数などで存在します。例えば文字列のreplaceメソッドなどが該当します。

以下のようにキーワード引数を指定しようとすると怒られることを確認できます。

```py
>>> cat_str = 'cat'
>>> cat_str.replace(old='c', new='b', count=1)

      1 cat_str = 'cat'
----> 2 cat_str.replace(old='c', new='b', count=1)

TypeError: replace() takes no keyword arguments
```

```py
>>> cat_str = 'cat'
>>> signature = inspect.signature(cat_str.replace)
>>> signature.parameters['old'].kind
<_ParameterKind.POSITIONAL_ONLY: 0>

>>> signature.parameters['old'].kind == inspect.Parameter.POSITIONAL_ONLY
True
```

POSITIONAL_OR_KEYWORD は自前でPythonコードを書くときには一番一般的な種別です。位置指定引数とキーワード引数両方受け付けるタイプです。

```py
def sample_func_8(a: str) -> Tuple[int, str]:
    ...
```

```py
>>> signature = inspect.signature(sample_func_8)
>>> signature.parameters['a'].kind
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>

>>> signature.parameters['a'].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
True
```

VAR_POSITIONALは任意の個数の位置指定引数を受け付けるタイプの引数が該当します。`*args`といったように、アスタリスクと共に表記されるタイプの引数ですね。

```py
def sample_func_9(*args) -> Tuple[int, str]:
    ...
```

```py
>>> signature = inspect.signature(sample_func_9)
>>> signature.parameters['args'].kind
<_ParameterKind.VAR_POSITIONAL: 2>

>>> signature.parameters['args'].kind == inspect.Parameter.VAR_POSITIONAL
True
```

KEYWORD_ONLYはキーワード引数のみ受け付けるタイプの引数です。Pythonで定義する際にはその引数の前に`, *,`といったように、アスタリスクを引数部分に挟みます。

例えば以下のような関数だと、a引数は位置指定引数が設定できますがアスタリスクの後のb引数には位置指定引数は指定できずエラーになります。

```py
def sample_func_10(a: int, *, b: str) -> Tuple[int, str]:
    ...
```

```py
>>> sample_func_10(10, 'cat')
----> 1 sample_func_10(10, 'cat')

TypeError: sample_func_10() takes 1 positional argument but 2 were given
```

```py
>>> signature = inspect.signature(sample_func_10)
>>> signature.parameters['b'].kind
<_ParameterKind.KEYWORD_ONLY: 3>

>>> signature.parameters['b'].kind == inspect.Parameter.KEYWORD_ONLY
True
```

最後のVAR_KEYWORDは任意のキーワード引数を受け付けるタイプの引数です。`**kwargs`といったようにアスタリスク2つで表現されるタイプのものになります。

```py
def sample_func_11(**kwargs) -> Tuple[int, str]:
    ...
```

```py
>>> signature = inspect.signature(sample_func_11)
>>> signature.parameters['kwargs'].kind
<_ParameterKind.VAR_KEYWORD: 4>

>>> signature.parameters['kwargs'].kind == inspect.Parameter.VAR_KEYWORD
True
```

# 参考文献

- [PEP362 Function Signature Object](https://www.python.org/dev/peps/pep-0362/#references)
- [Python: inspect.signature() で関数のシグネチャを調べる](https://blog.amedama.jp/entry/2016/10/31/225219)