for文を使って辞書のキーを反復しながら特定のキーと値だけを抜き出したい。そんなときには内包表記の中でアレを使うのがよろしいんじゃないですかね。
以下に示すコードにおいて、dataはキーと値の組を格納する辞書である。また、target_keysはdataからキーと値の組を抽出する対象となるキーを格納するリストだ。for文ではtarget_keysの要素を反復し、反復した値がキーとして辞書に存在するかどうかをチェックしている。キーとして存在していれば、その値を取得して、キーと値の組をタプルとしてresultに追加している。これは正しく動作するが、よりPython風な書き方が可能である。どんなコードが考えられるだろう。
data = {'a': 10, 'foo': 20, 'b': 30, 'bar': 40}
target_keys = ['a', 'b', 'c']
result = []
for k in target_keys:
if k in data:
result.append((k, data[k]))
print(result) # [('a', 10), ('b', 30)]
筆者は問題文のコードをよりスマートにPython風に書く案として2つのやり方を考えました。しかし、それらには一長一短があるかなと思い、どちらが適切かをChatGPT(GPT 5)/Gemini 2.5 Flash/Claude(Claude Sonnet 4)という3つの大規模言語モデル(LLM)に聞いてみました。その結果が以下です。
なんと! LLMは全員が解答例1をより適切であると結論付けました。さて、そのコードとはどんなものなのでしょう。
どうもHPかわさきです。
いつの間にか秋の雰囲気がやってまいりましたね。などといいつつ、昼間はエアコン全開なのですが(笑)。今年の秋は短いそうなので、食欲の秋も短めで済みそうなのが幸いです。とはいえ、寒くなってきたらなってきたで、美味しいものはたくさんありますからねぇ。湯豆腐食べたい。
そんなこととは関係なく、今回はfor文を使って辞書の要素をフィルタリングする方法についての問題です。for文と要素の反復といえば内包表記なので、記事のタイトルには「内包表記」とかズバリ書いちゃっていますが、こいつと何を組み合わせるのがよいのかな? について考えてみてください。
こんなに長くなるはずじゃなかったんですが、最後までお付き合いいただけるとうれしいでーす。
3つのLLMの全てがより適切であると答えたコード例を以下に示します。
result = [(k, data[k]) for k in target_keys if k in data]
ご覧の通り、リスト内包表記を使って、target_keysの要素を反復し、「data[k]」のようにして辞書からキーに対応する値を取得しています。このときに、target_keysの要素が辞書に存在しないキーだと例外が発生するので、if句でin演算子を使い、キーが存在することを確認しています。
筆者が考えたもう1つのやり方は以下です。
result = [(k, v) for k in target_keys if (v := data.get(k))]
こちらもリスト内包表記を使っています。違うのはif句でin演算子ではなく「(v := data.get(k))」のように代入式を使って変数vに辞書のキーkの値を取得している点です。これにより、内包表記の先頭では「(k, data[k])」のように辞書にアクセスするのではなく「(k, v)」と記述できています。
では、3つのLLMはなぜ内包表記とin演算子を使うやり方をより適切だと考えたのでしょうか。
まずは正解例とした2つのコードを再掲しておきます。
# その1:リスト内包表記とin演算子を使用
result = [(k, data[k]) for k in target_keys if k in data]
# その2:リスト内包表記と代入式を使用
result = [(k, v) for k in target_keys if (v := data.get(k))]
幾つかの観点からこのコードを比較してみましょう。
まずはこのコードが何をするのか? が分かりやすいのはどっちかについて考えてみましょう。
その1のコードは、target_keysの要素がdataのキーに含まれているかどうかを内包表記のif句で判断していることが(内包表記に慣れた人には)すぐ分かるでしょう。
これに対して、その2のコードでは辞書のgetメソッドを使って、辞書にキーが存在するかどうかの判定とその値の取得を行っています。getメソッドは辞書にキーが存在しなかった場合、デフォルトではNoneを戻り値とします。つまり、Noneが戻ってきたら、キーが存在しないと考えられます(「ん?」と思った方、これについてはちょっと後で)。そこで、戻り値がNoneでないかどうかをif句で判定するようにしています。
ここまでの動作をコードにすると次のようになります。
# その2を代入式を使わないで書くと……
result = [(k, data[k]) for k in target_keys if data.get(k)]
ここで重要なのは、if句でもgetメソッドでdata[k]を取得しているのに、内包表記の先頭では「(k, data[k])」として同じ値を取得していることです。これは無駄な気がします。そこで代入式の出番というわけです。「v := data.get(k)」として取得した値を変数vに代入しておけば、内包表記の先頭で「(k, v)」と書くだけで同じ値を二度取得せずに済みます。その結果が上で示したその2のコードになっているわけです。
といっても、その1のコード(in演算子バージョン)でも「data[k]」「k in data」という形で辞書のルックアップを2回やっています。そういう意味では、似たような無駄はその1のコードにも存在しています。が、それを補う可読性の高さがあると考えてください。
その2のコードのメリットは、代入式で変数に保存した値を何度でも使い倒せるところにありますが、その辺はまた別の問題で採り上げることにしましょう。
getメソッドの動作と代入式についての知識があれば「あー、そうだね」と理解はできるでしょうが、パッと見ただけで、これを理解するのは大変かもしれませんね。
そういうわけで、読みやすさ(可読性)という面ではその1のコードがより適切といえると筆者は考えています(恐らく3つのLLMも)。
辞書の値に0や空文字列、Noneがあった場合には、実はその2のコードはうまくありません。実際に試してみましょう。
data = {'a': 0, 'foo': 20, 'b': '', 'bar': 40, 'c': None}
target_keys = ['a', 'b', 'c']
# その2
result = [(k, v) for k in target_keys if (v := data.get(k))]
print(result) # []
この例ではdataに含まれるキーと値の組のうち、target_keysで指定されるキーの値が0、''、Noneになっています。これらは真偽値(True/False)として評価される場合には偽として扱われます。そのため、その2のやり方だと全てがif句でフィルタリングされてしまい、resultは空のリストになってしまいます。
次のようにすればNone以外には対処できるでしょうが、辞書の値にNoneが含まれているときにはこのやり方ではうまくいきません(実はうまくやる方法もあります。記事の最後のカコミを参照)。
data = {'a': 0, 'foo': 20, 'b': '', 'bar': 40, 'c': None}
target_keys = ['a', 'b', 'c']
result = [(k, v) for k in target_keys if (v := data.get(k)) is not None]
print(result) # [('a', 0), ('b', '')]
これは、キーの存在確認を(その1のようにin演算子で)直接行うのではなく、getメソッドの戻り値を介して行っているからともいえます。つまり、辞書の値にNoneがないことが確実でない場合、このやり方はオススメできないということです。
ただし、キーに対応する値を内包表記の中で何度も使用したいのであれば、代入式を使って変数に値を保存しておくのはよい考えです。このときには上でも述べたように、辞書の値が偽と評価される値が含まれない場合に限るでしょう。
というわけで、リスト内包表記とin演算子を使うのが、問題文のコードをよりPython風に書き替える一番よいやり方だといえそうです。他にもっとよいやり方があるよ! って方は教えてくださいね(代入式バージョンのもっとよいやり方はこの後で!)。
実は今回の問題は「代入式をリスト内包表記の中で使うと便利!」みたいなものにする予定だったのですが、サンプルコードを書いているうちに「あれ? in演算子の方がカンタンじゃね?」となってしまったのでした。あれば便利な代入式ですが、なかなかその良い使いどころって思い付かないものですねぇ。
なお、リスト内包表記については「Python入門」の「リストの基本」をご覧いただければと思います。ただなー。今回のような突っ込んだお話ではなく、あくまでも入門記事ですので、その点はご承知置きくださいね。
あ、最後にこの原稿をClaudeにチェックしてもらったところ、よい方法を教えてくれました。
data = {'a': 0, 'foo': 20, 'b': '', 'bar': 40, 'c': None, 'd': object()}
target_keys = ['a', 'b', 'c', 'd', 'e']
sentinel = object()
result = [(k, v) for k in target_keys
if (v := data.get(k, sentinel)) is not sentinel]
print(result) # [('a', 0), ('b', ''), ('c', None), ('d', <object object at …>)]
object関数を呼び出して、新規にobjectインスタンスを生成して、それをsentinelに保存しておきます。このオブジェクトを参照しているのは、この時点ではsentinelだけです。これをgetメソッドの第2引数に渡すと、キーが存在しなかった場合にはそのオブジェクトが返されてvに代入されます。すると、「v is not sentinel」はFalseになるので、「(存在しないキー, sentinel)」というタプルがリストに追加されることはありません。逆にキーが存在していて、その値を得たときには、多くの場合、それはsentinelとは異なるオブジェクトです。そのため、安心して0だろうが、空文字列だろうが、Noneだろうがキーに対応する値として扱えます(そのため、sentinelを別の場所で使うことがないように、その扱いには気を付ける必要があります)。
上のサンプルコードでは、辞書に'd'というキーとその値(object()呼び出しにより作成されるオブジェクト)が含まれています。これはif句でフィルタリングされたくはありません。また、target_keysには辞書のキーにはない'e'が含まれています。こちらは結果を含むresultに('e', sentinel)のように追加されたくはありません。
この辺りを考慮しながら、興味のある方は上のコードを実行してみてください。
Copyright© Digital Advantage Corp. All Rights Reserved.