カテゴリー

Python基礎

【inspect&引数名取得】defのパラメータ名を取り出す

2021年8月14日

こんな人にオススメ

curve_fitで得られたパラメータがどのパラメータ名だったのかが分かりにくい!

なんとかしてパラメータ値とパラメータ名の対応づけがしたい!

ということで、今回はdefで定義した関数のパラメータの値とその名前を対応づける方法について解説する。

どういうことかというと、例えば以下のようにパラメータの値が決まった場合を考える。

# 実はパラメータa=1, b=2, c=3
par = [1, 2, 3]

この時、それぞれの値が初めに決めていたパラメータのどれに該当するかを対応づけするということ。今でいえばa=1, b=2, c=3ということ。

標準ライブラリのinspectsignature関数を使用することで、パラメータ名を取得可能。あとは値とこの名称をうまく結びつければいい。

python環境は以下。

  • Python 3.9.6
  • numpy 1.21.1
  • scipy 1.7.0
  • plotly 5.1.0
  • plotly-orca 3.4.2
スポンサーリンク
スポンサーリンク

運営者のメガネと申します。TwitterInstagramも運営中。

自己紹介はこちらから、お問い合わせはこちらからお願いいたします。

運営者メガネ

下準備

import inspect
import sys
import numpy as np
import scipy.optimize
import plotly.graph_objects as go
import plotly.io as pio

sys.path.append('../../')
import plotly_layout_template as template

まずは下準備としてのimport関連。plotlyは初めの簡単な例でしか使わないので、グラフを描く必要がないならimportしなくてもいい。

また、plotly_layout_templateはplotlyの自作テンプレートを入れている。パスが2つ上なので'../../'としている。

【随時更新 備忘録】plotlyのグラフ即席作成コード

続きを見る

パラメータの値と名前を対応づけたい

まず初めに、対応づけとはいったいどういうことかということについてお話しする。実際にコードを書いた方がわかりやすいだろう。

ガウシアンっぽいデータを作成

def get_gaussian(x):
    # ガウシアンっぽいデータの作成
    arr = []
    for num, i in enumerate(x):
        np.random.seed(seed=num)
        coefficient1 = np.random.rand() / 100

        # 各xで乱数を変えながらガウシアン関数を作成
        eq1 = 1 / (np.sqrt(2 * np.pi))
        eq2 = np.exp(-((i - 1) ** 2) / 2)
        val = eq1 * eq2 + coefficient1
        arr.append(val)
    arr = np.array(arr)

    return arr

# 横軸の値を変更(-5から4.995まで0.01刻み)
x = np.arange(-5, 5, 0.005)
arr = get_gaussian(x=x)
print(f"x: {x}")
# x: [-5.    -4.995 -4.99  ...  4.985  4.99   4.995]
print(f"arr: {arr}")
# arr: [0.00548814 0.00417023 0.00435996 ... 0.00785357 0.00573374 0.00838173]

説明するためにはデータが必要なのでサクッと作成する。今回使用するデータはガウシアンっぽいデータ。

以下の記事で実際に使用した関数をそのまま使用している。

【python&フィッティング】polyfitとcurve_fitでfitting

続きを見る

データをfitさせてパラメータ値を取得

def gaussian_func(x, sigma, mu, b):
    eq1 = 1 / (np.sqrt(2 * np.pi) * sigma)
    eq2 = np.exp(-((x - mu) ** 2 / (2 * sigma ** 2)))
    y = eq1 * eq2 + b
    return y

# データをガウシアンでfitting
par, cov = scipy.optimize.curve_fit(f=gaussian_func, xdata=x, ydata=arr)

データのfitはscipycurve_fitを使用した。fit対象の関数はgaussian_func関数でガウシアンの形状。

curv_fitでfitさせるとパラメータと共分散行列が手に入るが、ここではパラメータparを使用する。

# パラメータの出力はできるけど、対応関係がわからない
print(f"par: {par}")
# par: [0.99997103 1.00021249 0.0048593 ]

今回のfitで得られたパラメータは上の3種類で、それぞれgaussian_func関数のsigma, mu, bに対応している。

んだが、どのパラメータがsigmaか、muか、bかがわかりにくい。3つだけだから簡単だが、それでも自分で対応させるのは面倒。

なら勝手に対応させるようにすればいい、というのが今回の記事。

ガウシアンのグラフとそのfit


蛇足ではあるが、元のデータとそのfitでできるグラフが上のグラフ。グラフ作成用のコードは以下。

inspect.signatureで対応づけ

結果からいうとinspect.signatureを使えばいい。ほぼこれで大丈夫。だが、これだけではうまくいかないので対策を解説。

inspect.signatureを使う

args_names = inspect.signature(gaussian_func)
print(args_names)
# (x, sigma, mu, b)

print(type(args_names))
# <class 'inspect.Signature'>

既に書いたが、inspect.signatureを使うと、その関数の引数名を得ることができる。今で言うとこの引数名がパラメータ名。

typeinspect.Signature。個人的には聞き慣れない型。

そのままで使えない

# そのまま使おうとしたらエラー
print(args_names[1:])
# TypeError: 'Signature' object is not subscriptable

これで解決かといえばそうではない。関数の引数には横軸の値として使用するxが入っている。だから、パラメータ名を取得するにはxを省く必要がある。

じゃあシンプルにスライスすればいいじゃんとなるが、実はスライスできない。事はそう単純ではない。

listとかにも変換できない

# tupleに変換しようとしてもエラー
args_names_tpl = tuple(args_names)
# TypeError: 'Signature' object is not iterable

なら一旦tupleとかに変換して仕舞えばいいなじゃないのか、となるがこれも許されない。イテラブルじゃないとな。ということはlistもダメだ。

# listに変換しようとしてもエラー
args_names_lst = list(args_names)
# TypeError: 'Signature' object is not iterable

parameter部分を取り出し

args_pair = args_names.parameters
print(args_pair)
# OrderedDict([('x', <Parameter "x">), ('sigma', <Parameter "sigma">), ('mu', <Parameter "mu">), ('b', <Parameter "b">)])
print(type(args_pair))
# <class 'mappingproxy'>

じゃあどうするかというと、実は.parametersでパラメータ部分の取り出しが可能。早く言ってよ。

この場合、パラメータ名とその状態(と勝手に呼んでる)が同時に出力される。状態というのは今回で言えば名称と同じだが、後々異なる。

で、この出力はdictに似た関係になっているので、.keysを使用する事で、パラメータ名を取り出すことができる。

par_names = args_pair.keys()
print(par_names)
# odict_keys(['x', 'sigma', 'mu', 'b'])
print(type(par_names))
# <class 'odict_keys'>

.keysで取り出せたらもうこっちのもの。tuplelistに変換可能なのでスライスもできるようになる。

par_names_tpl = tuple(par_names)
print(par_names_tpl)
# ('x', 'sigma', 'mu', 'b')
par_names_lst = list(par_names)
print(par_names_lst)
# ['x', 'sigma', 'mu', 'b']

ちなみに、.valuesとするとパラメータの状態を出力することも可能。

par_values = args_pair.values()
print(par_values)
# odict_values([<Parameter "x">, <Parameter "sigma">, <Parameter "mu">, <Parameter "b">])
print(type(par_values))
# <class 'odict_values'>

パラメータ値と名前を対応づけ

gaussian_par = par_names_tpl[1:]

par_dct = {}
for name, val in zip(gaussian_par, par):
    par_dct[name] = val
print(f"par_dct: {par_dct}")
# par_dct: {'sigma': 0.9999710320855185, 'mu': 1.0002124931675611, 'b': 0.004859304255264688}

ということで、パラメータ名が手に入ったのでこれをパラメータの値と対応づければいい。今回はdictを使った。

初めの[1:]はパラメータ名にあるxを省くためだ。これで当初の目的は達成。あっさり。

初期値とかargsとかある関数で試す

inspect.signatureを使うことでパラメータ値とパラメータ名を結びつけることに成功した。

ここからは、色んな種類の関数にinspect.signatureを適用してどんな挙動を見せるのかを確認する。

パラメータ名と組み合わせを出力する関数

# 関数のパラメータ名を出力する関数
def func_parmeters(func):
    args_names = inspect.signature(func)
    args_pair = args_names.parameters

    print(args_names)
    print(args_pair)

の前にサクッと出力を確認できる関数を作成。この関数に、調べたい関数名を入れることでパラメータ名を出力可能。

printだけ・返り値のある関数

# シンプルにprintするだけの関数
def print_func(a, b):
    print(a, b)

func_parmeters(print_func)
# (a, b)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">)])

まずはシンプルにprintだけの関数。この場合はガウシアンの時と同じようにパラメータ名が2種類取り出せる。

これは返り値だけの関数でも同様。

# 返り値のある関数
def return_func(a, b):
    return a, b

func_parmeters(return_func)
# (a, b)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">)])

途中に変数が入る関数

def middle(a, b):
    c = 10
    print(a, b, c)

func_parmeters(middle)
# (a, b)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">)])

途中に変数が入ったとしても、ちゃんとパラメータ名が出力される。これは安心。

初期値がある関数

def print_func_ini(a, b, c=10):
    print(a, b, c)

# 初期値もしっかり示してくれる
func_parmeters(print_func_ini)
# (a, b, c=10)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('c', <Parameter "c=10">)])

初期値がある関数でもパラメータ名を出力してくれるが、この場合はパラメータの状態に初期値情報が記載される。

ということで、型指定や色んな初期値を試してみる。全部うまくいく。いいね。

# 引数の種類を増やしてみた
def print_func_ini2(a: list, b=[1, 2, 3], c=10, d=True, e='e', f={10, 20}):
    print(a, b, c, d, e, f)

# 型指定だけでも反映される
func_parmeters(print_func_ini2)
# (a: list, b=[1, 2, 3], c=10, d=True, e='e', f={10, 20})
# OrderedDict([('a', <Parameter "a: list">), ('b', <Parameter "b=[1, 2, 3]">), ('c', <Parameter "c=10">), ('d', <Parameter "d=True">), ('e', <Parameter "e='e'">), ('f', <Parameter "f={10, 20}">)])

*argsのある関数

# argsのある関数
def print_func_args(*args, a, b):
    print(args, a, b)

func_parmeters(print_func_args)
# (*args, a, b)
# OrderedDict([('args', <Parameter "*args">), ('a', <Parameter "a">), ('b', <Parameter "b">)])

可変長引数*argsがある場合、パラメータ名にはargsだけが、その状態には*argsがつく。可変長引数については以下。

【python&関数化】defとかargsとかを使って関数を作成する

続きを見る

*argsの位置を変更

# argsを最後に持ってきた
def print_func_args2(a, b, *args):
    print(args, a, b)

func_parmeters(print_func_args2)
# (a, b, *args)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('args', <Parameter "*args">)])

可変長引数を最初ではなく最後に持ってきても大丈夫。単に出力される位置が変わるだけ。これは*argsを真ん中に持ってきた時も同様。

# argsを真ん中に持ってきた
def print_func_args3(a, *args, b):
    print(args, a, b)

func_parmeters(print_func_args3)
# (a, *args, b)
# OrderedDict([('a', <Parameter "a">), ('args', <Parameter "*args">), ('b', <Parameter "b">)])

**kwargsのある関数

# kwargsのある関数
def print_func_kwargs(a, b, **kwargs):
    print(a, b, kwargs)

func_parmeters(print_func_kwargs)
# (a, b, **kwargs)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('kwargs', <Parameter "**kwargs">)])

可変長引数**kwargsも同じようにパラメータ名には*kwargs、状態には**kwargsと出力される。

**kwargsの場合は引数の一番最後にしか書けないためこれで終わり。

argsのある関数で値と名前の対応づけ

argskwargsがない関数でのパラメータ値とパラメータ名の対応づけは既に書いた通り。ではある場合はどうなるのか。

さっきと同じコードで対応づけをすると、argsではうまくいかない。kwargsではうまくいく。

そもそも、argsとかを使って対応づけという状況が起きるかは分からんがとりあえずやってみる。

単純に対応づけするとズレる

# print_func_args関数のパラメータ名
args_pair = inspect.signature(print_func_args).parameters
print(args_pair)
# OrderedDict([('args', <Parameter "*args">), ('a', <Parameter "a">), ('b', <Parameter "b">)])

まずはパラメータ名を出力。これは今まで通り、というよりさっきやった。で、これにパラメータ値を対応づける。

今回は簡単に4種類のパラメータ値を用意した。前2つがargsに、あとはa, bにそれぞれ対応するようにした。

# print_func_args関数にパラメータ値
# 10, 20がargs、3がa、4がbに対応
par = (10, 20, 3, 4)

で、ガウシアンの時と同じコードを使用して書いてみると、argsには1つしか要素が入らずにa, bがズレてしまう。

# 普通にdictに入れるとa=3とb=4がズレる
par_dct = {}
for name, val in zip(args_pair, par):
    par_dct[name] = val
print(f"par_dct: {par_dct}")
# par_dct: {'args': 10, 'a': 20, 'b': 3}

argsの部分をtupleにする

# パラメータ名とパラメータ値の数
print(len(args_pair))
# 3
print(len(par))
# 4

# パラメータ名とパラメータ値の数の差分
diff = (len(par) - len(args_pair))

何が問題だったのかというと、argsに対応するパラメータが独立していたという点。ならtupleとかで囲めばいい。

ということで、まずはargsに対応するパラメータ数がいくつあるかを探るべく、パラメータ名と値の数を勘定して差分を取る。

次にパラメータの中でargsがどのインデックスにあるかを検索。今回で言えば0番目。

# argsが何番目にあるのか
index = list(args_pair).index('args')
print(index)
# 0

続いて、partupleだがtupleのままだとargsに対応する要素をtupleで囲めない。したがって、一旦listに変換。

listに変換後はargsに対応する要素がどれかを抽出。今回で言えば1020

# tupleだと変更不可能なので一旦listに変換
par_lst = list(par)

# argsの対象となる要素
print(par_lst[index: index + 1 + diff])
# [10, 20]

argsの対象となる要素をtupleで囲めばいい。tupleは「,」がないと成立しないのでこの点には注意。

# 要素の変更
par_lst[index: index + 1 + diff] = (par[index: index + 1 + diff],)
# tupleに戻す
par = tuple(par_lst)
print(par)
# ((10, 20), 3, 4)

これでargs部分を囲めたので、先程のコードを使用して対応づけるとうまくいく。

# argsをtupleにしたのでズレがなくなる
par_dct = {}
for name, val in zip(args_pair, par):
    par_dct[name] = val
print(f"par_dct: {par_dct}")
# par_dct: {'args': (10, 20), 'a': 3, 'b': 4}

kwargsのある関数で値と名前の対応づけ

# print_func_args関数のパラメータ名
args_pair = inspect.signature(print_func_kwargs).parameters
print(args_pair)
# OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('kwargs', <Parameter "**kwargs">)])

# print_func_args関数にパラメータ値
# kwargsの場合はdictにしないといけないので、元からdictに
par = (1, 2, dict(A=3, B=4, C=5))

これもこんな状況が生まれるのか分からないけどやってみる。kwargsの場合は一部がdictになるので元のコードでそのままいける。

# この方法なら初めからkwargs部分がdictで独立しているから大丈夫
par_dct = {}
for name, val in zip(args_pair, par):
    par_dct[name] = val
print(f"par_dct: {par_dct}")
# par_dct: {'a': 1, 'b': 2, 'kwargs': {'A': 3, 'B': 4, 'C': 5}}

__code__でも可能だけど面倒

def print_func(a, b, c=3):
    d = 1
    print(a, b, c, d)

今回はinspectライブラリを使用したが、実は__code__という書き方でも同じように関数の変数名を取得することができる。

しかし、この場合はローカル変数も反映されたりそもそもコードが長くなりやすいという点から個人的にはオススメしない。

# __code__はpythonドキュメント曰く
# > 関数をコンパイルした バイトコード を格納するコードオブジェクト
# とのこと(<https://docs.python.org/ja/3/library/inspect.html)>
print_func_info = print_func.__code__
print(print_func_info)
# <code object print_func at 0x10271d7c0, file "(ファイル名)", line (関数を定義した行番号)>

# ローカル変数も入ってしまう
print_variable_names = print_func_info.co_varnames
print(print_variable_names)
# ('a', 'b', 'c', 'd')

# 引数の数は合ってる
print_arg_nums = print_func_info.co_argcount
print(print_arg_nums)
# 3

__code__については以下に詳しく書いてある。

一応、__code__には色々派生系があるから、それを使う際には使えるかもしれないが、基本はinspectでいい気がする。

fittingの時に使えそう

今回はinspect.signatureを使用して、関数の引数名を取得する方法を解説した。さらに、これを応用してパラメータ値と対応づける方法も解説した。

実際に対応づけるタイミングがあるかといえばあまりないかもしれない。しかし、curve_fitでのfittingではパラメータが出力されるので、この時には活躍しそう。

ニッチな分野かもしれないが、知ってるだけで一歩くらいは先に進めるだろう。

関連記事

【python&表出力】tabulateモジュールで出力を表形式にする

続きを見る

【Python&Excel】openpyxlでフォルダ内の複数csv→1つのExcelへ

続きを見る

【python3&四捨五入】四捨五入すると1.5も2.5も2.0になる問題

続きを見る

【collections.Counter】配列の要素を要素名と個数で勝手に集計

続きを見る

【python&~】数値にチルダ(~)をつけると値が+1されて負の数になる(ビット反転)

続きを見る

【辞書の結合】dictのマージ

続きを見る

多項式関数のグラフ
【辞書&pandas】dict{name: , val: {a: [~], b:[~]}}のpandas化

続きを見る

【PEP8&flake8】pythonにおけるPEP8とflake8

続きを見る

関連コンテンツ

スポンサーリンク

Amazonのお買い物で損したない人へ

1回のチャージ金額通常会員プライム会員
¥90,000〜2.0%2.5%
¥40,000〜1.5%2.0%
¥20,000〜1.0%1.5%
¥5,000〜0.5%1.0%

Amazonギフト券にチャージすることでお得にお買い物できる。通常のAmazon会員なら最大2.0%、プライム会員なら2.5%還元なのでバカにならない。

ゲットしたポイントは通常のAmazonでのお買い物に使えるからお得だ。一度チャージしてしまえば、好きなタイミングでお買いものできる。

なお、有効期限は10年だから安心だ。いつでも気軽にAmazonでお買い物できる。

Amazonチャージはここから出来るで

もっとお得なAmazon Prime会員はこちらから

30日間無料登録

執筆者も便利に使わせてもらってる

スポンサーリンク

  • この記事を書いた人

メガネ

独学でpythonを学び天文学系の大学院を修了。 ガジェット好きでMac×Android使い。色んなスマホやイヤホンを購入したいけどお金がなさすぎて困窮中。 元々、人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂ったために今も人生めちゃくちゃ楽しい。 pythonとガジェットをメインにブログを書いていますので、興味を持たれましたらちょこちょこ訪問してくだされば幸いです🥰。 自己紹介→変わって楽しいの繰り返し

-Python基礎
-,