CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: inspect.signature() で関数のシグネチャを調べる

Python は inspect モジュールを使うことでオブジェクトから多くの情報を取得できる。 そして、関数のシグネチャを調べる方法としては、これまで getargspec() 関数が使われることが多かった。 ただ、この関数は Python 3 系では非推奨 (Deprecated) となっている。 そのため、今後は使わない方が良い。

getargspec() 関数が非推奨となった代わりに、今は signature() 関数を使うことが推奨されているようだ。 今回は、この関数の使い方について扱う。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G1004
$ python --version
Python 3.5.2

インストール

Python 3.3 以降であれば signature() 関数は標準ライブラリの inspect モジュールに用意されている。 そのため、特に何も意識することなく使うことができる。

しかし、もし使っているのが Python 3.2 未満であれば PyPI からバックポート版をダウンロードして使うことができる。

$ pip install funcsigs

使ってみる

今回は Python の REPL を使って動作を確認していこう。

$ python

早速 inspect モジュールの signature() 関数をインポートしよう。

>>> from inspect import signature

もし、Python 2 系でバックポート版を使っているのであれば、次のようにしてインポートできる。

>>> from funcsigs import signature

まず手始めに、次のような関数 f() を定義しておく。 この関数は foo と bar というふたつのパラメータを受け取る。 bar に関してはデフォルト値が設定されている。

>>> def f(foo, bar='Hello'):
...     pass
...

それでは、signature() 関数に先ほど定義した関数 f() を渡してみよう。 すると Signature というオブジェクトが得られることが分かる。

>>> signature(f)
<Signature (foo, bar='Hello')>

このオブジェクトには parameters という名前で受け取るパラメータの情報が格納されている。 このメンバは次のように順序関係を持った辞書型のオブジェクトとして得られる。

>>> sig = signature(f)
>>> sig.parameters
mappingproxy(OrderedDict([('foo', <Parameter "foo">), ('bar', <Parameter "bar='Hello'">)]))

例えばパラメータの foo について知りたいときは、次のように名前をキーとしてアクセスすれば良い。 得られるのは Parameter というオブジェクトになっている。

>>> sig.parameters['foo']
<Parameter "foo">

このオブジェクトには名前はもちろん、デフォルト値などの情報も入っている。 もしデフォルト値が設定されていないときは inspect.Signature.empty で得られるオブジェクトになる。

>>> sig.parameters['foo'].name
'foo'
>>> sig.parameters['foo'].default
<class 'inspect._empty'>
>>> sig.parameters['bar'].default
'Hello'

その他、パラメータがどのような使われ方をしているかも得られる。 例えば、通常であれば次のように POSITIONAL_OR_KEYWORD になる。

>>> sig.parameters['foo'].kind
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>

それに対し *args, **kwargs といった可変長引数のときは VAR_POSITIONAL や VAR_KEYWORD になる。

>>> def f(*args, **kwargs):
...     pass
...
>>> signature(f)
<Signature (*args, **kwargs)>
>>> sig = signature(f)
>>> sig.parameters['args'].kind
<_ParameterKind.VAR_POSITIONAL: 2>
>>> sig.parameters['kwargs'].kind
<_ParameterKind.VAR_KEYWORD: 4>

また、Python 3 の機能ということもあってアノテーションにも対応している。 次のように引数や返り値にアノテーションをつけた関数を用意して signature() にかけてみる。

>>> def f(foo: str) -> int:
...     return -1
...
>>> signature(f)
<Signature (foo:str) -> int>
>>> sig = signature(f)

すると Parameter からは annotation というメンバでアノテーションに指定した型が得られる。

>>> sig.parameters['foo'].annotation
<class 'str'>

また Signature からも return_annotation というメンバで返り値のアノテーションが取得できる。

>>> sig.return_annotation
<class 'int'>

それ以外にも便利な機能がある。 例えば、その関数を特定の引数で実行したときに、パラメータが引数に対してどのようにバインドされるかを確かめることもできる。 これには Signature#bind() メソッドを使う。

>>> def f(foo, bar='Hello', baz=None, *args, **kwargs):
...     pass
...
>>> sig = signature(f)
>>> sig.bind('foo', baz=1, bar='Bye', hoge=True)
<BoundArguments (foo='foo', bar='Bye', baz=1, kwargs={'hoge': True})>

上記で得られるのは BoundArguments というオブジェクトになっている。 このオブジェクトからは arguments というメンバでバインドした結果が得られる。

>>> bound_args = sig.bind('foo', baz=1, bar='Bye', hoge=True)
>>> bound_args.arguments
OrderedDict([('foo', 'foo'), ('bar', 'Bye'), ('baz', 1), ('kwargs', {'hoge': True})])

上記を応用すると、関数の呼び出しをトレースするようなデコレータを書きやすいなと思った。 次のサンプルコードでは @recording というデコレータを定義している。 このデコレータは修飾した関数をラップして、呼び出し内容と結果をログに自動で残す機能を持っている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

try:
    from inspect import signature
except ImportError:
    from funcsigs import signature

import logging
import functools


LOG = logging.getLogger(__name__)


def recording(f):
    """関数 (メソッド) の呼び出しを記録するデコレータです。

    受け取ったパラメータと返り値をログに出力します。"""
    @functools.wraps(f)
    def _recording(*args, **kwargs):
        # 表面上は元々の関数 (メソッド) がそのまま実行されたように振る舞う
        result = f(*args, **kwargs)
        # デコレーションする関数のシグネチャを取得する
        sig = signature(f)
        # 受け取ったパラメータをシグネチャにバインドする
        bound_args = sig.bind(*args, **kwargs)
        # 関数名やバインドしたパラメータの対応関係を取得する
        func_name = f.__name__
        func_args = ','.join('{k}={v}'.format(k=k, v=v)
                             for k, v in bound_args.arguments.items())
        # ログに残す
        fmt = '{func_name}({func_args}) -> {result}'
        msg = fmt.format(func_name=func_name,
                         func_args=func_args,
                         result=result)
        LOG.debug(msg)
        # 結果を返す
        return result
    return _recording


# デコレーションすると、その関数の呼び出しをログに記録する
@recording
def add(a, b):
    return a + b


def main():
    # デバッグレベルのログも出力する
    logging.basicConfig(level=logging.DEBUG)
    # デコレーションされた関数を呼び出すと自動でログが出力される
    print(add(1, 2))


if __name__ == '__main__':
    main()

上記を実行すると、次のような出力が得られる。

$ python recording.py
DEBUG:__main__:add(a=1,b=2) -> 3
3

見事に関数の呼び出しと、その返り値までが自動でログに記録された。

めでたしめでたし。