辞書に格納されている要素を関数にキーワード引数として渡したい。しかも、キーワードと辞書のキーが一緒! そんなときに「キーワード=辞書[キー]」とか書いていませんか? もっとよい方法ありますよ。
以下のコードではfunc関数を定義して、それを呼び出している。func関数の2つのパラメーターはどちらもキーワード専用であり、func関数を呼び出す際には以下のようにパラメーター名をキーワードとして付加する必要があるため、書くのが面倒くさい。もっとカンタンに呼び出す方法はないだろうか。ただし、func関数の定義に手を加えてはならないものとする。
def func(*, name, age):
print(f'{name} is {age} years old')
hpk = {'name': 'hp kawasaki', 'age': 99}
func(name=hpk['name'], age=hpk['age'])
どうもHPかわさきです。
今回は上で定義しているfunc関数について聞いてみました。デフォルト引数値の指定もないままに、キーワード専用のパラメーターを置くのがよいか悪いかについてです。選択肢は以下の4つ。
3つのLLM+筆者の解答は次の通りです。
筆者としてはもちろん「クイズの問題用に考えたんだから気にしないでくれよ」としかいえません。
Claudeさんは結構辛辣(しんらつ)で「デフォルト値なしのキーワード専用パラメーターは、関数の柔軟性を損なう可能性があり、設計として少し中途半端に感じられます」という意見のようです。
ChatGPTさんは「デフォルト値がないのは、必須引数として強制したい意図が感じられるため、サイテーではなく、合理的とも受け取れます」と(ただのクイズの問題なのに)よいことのように考えてくれました(テキトーなんだよ、テキトー)。
Geminiさんは「これがクイズの問題で使う関数の定義」ということを踏まえて、「よくできた例」としています。聞きたかったのはそういうことじゃないんだけどねぇ。
LLMと筆者による問題文にある関数の定義がいいものかどうかは四者四様の答えになるという意外な展開になりました。個人的には2と3がいい感じに拮抗してくれるとよかったのですが、Geminiさんの俯瞰(ふかん)的な答えが何ともいい味を出してくれていますね。
正解のコード例を以下に示します。
def func(*, name, age):
print(f'{name} is {age} years old')
hpk = {'name': 'hp kawasaki', 'age': 99}
func(**hpk)
「**hpk」としてhpkの要素(キーと値の組)をアンパックすることで、「func(name='hp kawasaki', age=99)」と呼び出すのと同じ結果が得られます。
問題文のコードを見てみましょう。この関数はnameとageという2つのパラメーターを持ち、それらは共にキーワード専用となっています(パラメーターリストでは単独の「*」よりも後ろにあるパラメーターは全てキーワード専用になります)。また、デフォルト引数値が指定されていないので、これら2つのパラメーターは呼び出し時に引数を渡すことが必須です。
def func(*, name, age):
print(f'{name} is {age} years old')
hpk = {'name': 'hp kawasaki', 'age': 99}
func(name=hpk['name'], age=hpk['age'])
また、変数hpkは{'name': 'hp kawasaki', 'age': 99}という辞書を参照しています。たまたま(なのか、意図してなのかはさておき)辞書のキーがパラメーター名と一致しています。このようなときに上のコードにあるように「name=hpk['name']」「age=hpk['age']」のようにパラメーター名に続けて、辞書[キー]と書くのはとても面倒くさいことですよね。
このようなときには、**演算子を辞書の前に前置することで、辞書の要素(キーと値の組)をアンパックして、関数に「キー=値」という形式で渡せます。それが正解例に示したやり方です。
def func(*, name, age):
print(f'{name} is {age} years old')
hpk = {'name': 'hp kawasaki', 'age': 99}
func(**hpk)
「**hpk」とすることで、実質的には「func(name='hp kawasaki', age=99)」と書いたのと同じことになります。ホント? と思うかもしれませんが、ちょっと下のコードを見てください。
list(**hpk) # TypeError: list() takes no keyword arguments
このコードを実行すると「list() takes no keyword arguments」というメッセージ付きでTypeError例外が発生します。このことからも「**hpk」により辞書の要素がアンパックされて、キーワード引数として関数に渡されていることが分かるでしょう。
ただし、これがうまく機能しているのは、キーワード引数として値を与えられるパラメーターと辞書が持つキーが完全に対応しているからであることには注意してください。以下は、余計なキー/値の組を格納している辞書をfunc関数に渡してみた例です。
foo = {'name': 'FOO', 'age': 100, 'info': 'INFO'}
func(**foo) # TypeError: func() got an unexpected keyword argument 'info'
このような場合には、TypeError例外が発生してしまいます(逆に「func(**{'age': 99})」のようにパラメーターとして指定が必要なキーが不足していても例外が発生します)。
その一方で、関数が「**kwargs」で任意のキーワード引数を受け取れるようにすることは可能です。
def func2(**kwargs):
name = kwargs.get('name', 'nanasi')
age = kwargs.get('age', 0)
print(f'{name} is {age} years old')
hpk = {'name': 'hp kawasaki', 'age': 99}
func2(**hpk)
こっちがいいじゃん! とカンタンにはいえないところもあります。元の関数ではnameやageはパラメーターとして直接使えましたが、こちらではkwargsにキー/値の組として格納されているので、kwargs[キー]やkwargs.get(キー)などの形でアクセスする必要があります(上のコードでもそうしていますね)。
また、キーが間違っている(あるいは、関数呼び出し時に「キーワード=値」として指定するキーワードが間違っている)場合でも、関数は問題なく、それらを受け取ってしまうことです。
以下がその例です。
func2(**{'nama': 'hp kawasaki', 'age': 99}) # キーが間違っている
上のコードではキーが'name'ではなく'nama'となってしまっています。func2関数の中では「name = kwargs.get('name', 'nanasi')」として'name'キーの値を取得しているので、'name'キーがないこの場合は'nanasi'がデフォルト値として返されます。コードは例外を発生させることなく実行されますが、果たしてこれが意図した通りの振る舞いといえるのでしょうか(まあ、上のコードならgetメソッドではなく、キーを介したアクセスにすることで必要なモノがなければ例外が発生するようにすればよいのでしょうが)。
こんなことも発生するので、取りあえず「**kwargs」を使って何でも受け取っておけばいいやみたいなやり方には難しい点もあることには留意しておくのがよいでしょう。
「関数にキーワード引数を渡したい、でも、なるべくならスッキリと呼び出したい」というときには「**」演算子による辞書の要素のアンパックが便利ということは頭に入れておくようにしましょう。
なお、関数の引数については「Python入門」の「関数の引数」を参照してください。「**」に限らず、いろいろな形式の引数について説明しています。
Copyright© Digital Advantage Corp. All Rights Reserved.