銀月の符号

Python 使い見習いの日記・雑記

関数に与えられた * 引数、 ** 引数の名前も調べる

関数に与えられた引数の名前を調べる を読んで。 func_code の存在を思い出していました。そして、この方法だと * 引数、 ** 引数の名前が取り出せないので追加を。

inspect モジュールの出番

結論から。inspect.getargspec で得られます。行われているのは func_code らのより丁寧な調査です。 inspect.getargspec を使うとデフォルト値まで取り出せるおまけがついてきます。

>>> def sample2(self, arg0, arg1=None, *args, **kwargs):
...   local0 = None
...   local1 = None
...   abcdef = None
...
>>> import inspect
>>> inspect.getargspec(sample2)
(['self', 'arg0', 'arg1'], 'args', 'kwargs', (None,))

そして、 alisue さんの危惧していた func_code.co_varnames の内容の順序ですが、これは inspect モジュールの実装から推測するに、引数、* 引数、 ** 引数、ローカル変数の順で並んでいるようです。心配ありません。

inspect モジュール、その中身は?

inspect モジュールでは何が行われているのか、踏み込んでみます。 Python 2.5.4 の inspect.getargspec のコードです。

def getargspec(func):
    """Get the names and default values of a function's arguments.

    A tuple of four things is returned: (args, varargs, varkw, defaults).
    'args' is a list of the argument names (it may contain nested lists).
    'varargs' and 'varkw' are the names of the * and ** arguments or None.
    'defaults' is an n-tuple of the default values of the last n arguments.
    """

    if ismethod(func):
        func = func.im_func
    if not isfunction(func):
        raise TypeError('arg is not a Python function')
    args, varargs, varkw = getargs(func.func_code)
    return args, varargs, varkw, func.func_defaults

まずメソッドであるかどうか確認し、メソッドであった場合は解析対象を im_func に変更しています。次に関数であるか確認しています。そして func_code を対象に inspect.getargs 関数を実行しています。これで引数名、 *引数名、 ** 引数名が得られます。最後にデフォルト値を func_defaults から得ています。

続きまして、 Python 2.5.4 の inspect.getargs のコードです。

# These constants are from Python's compile.h.
CO_OPTIMIZED, CO_NEWLOCALS, CO_VARARGS, CO_VARKEYWORDS = 1, 2, 4, 8

def getargs(co):
    """Get information about the arguments accepted by a code object.

    Three things are returned: (args, varargs, varkw), where 'args' is
    a list of argument names (possibly containing nested lists), and
    'varargs' and 'varkw' are the names of the * and ** arguments or None."""

    if not iscode(co):
        raise TypeError('arg is not a code object')

    nargs = co.co_argcount
    names = co.co_varnames
    args = list(names[:nargs])
    step = 0

    # 中略

    varargs = None
    if co.co_flags & CO_VARARGS:
        varargs = co.co_varnames[nargs]
        nargs = nargs + 1
    varkw = None
    if co.co_flags & CO_VARKEYWORDS:
        varkw = co.co_varnames[nargs]
    return args, varargs, varkw

co は inspect.getargspec から来るので func_code であることが期待されます。まずは co がコードオブジェクトかどうか確認しています。

その後は引数名を得ています。引数名は co_varnames リストから先頭 co_argcount の数だけ要素を取り出すことで得られます。

次に * 引数名、 ** 引数名ですが、まず存在するかどうか調べないといけません。存在するならば、これらの名前は co_varnames の引数、ローカル変数の間に挟まっています。これは co_flags のあるビットがたっているかどうか調べることで確認できます。

中略部分は anonymous (tuple) arguments なるものに対応するための 33 行のコードです。これは co_code バイトデータを直接覗き込んでいるコードで、よくわかりませんでした。私の理解としては「まれに co_varnames に入っている名前が引数名でないことがあり、この通りにすると正しい引数名が取り出せる」らしいというところまでです。興味がわいてきた方はお手元の inspect.py ソースコードを読んでみてください。

ちなみに、この中略部分は Python 3.0.1 の inspect.py には存在しませんでした。バッドノウハウの類なのかもしれません。