BLOGサブスレッドの日常

2025.05.19

dict.get()

tama

こんにちは tama です。

サブスレッドは代表が20年来の Python ユーザーということもあり、創業以来ずっと Python の仕事をしています。
(Python だけでなく、Webフロントエンドはもちろん、C#、C++、Objective-C、Swift、Kotlin などさまざまな仕事をしています)
長く同じ言語のプログラムを書いていると、意味を深く考えず書いてしまう手癖のような表現が出てきます。コードレビューをしていてもよく見かけます。
例えば dict. get() です。

dict. get() とは

dic = {"a":123, "b":456}
dic["c"]  # ①
dic.get("c")  # ②
dic.get("c", 789)  # ③

Pythonの dict型(辞書型)は、[] でアクセスする際、指定されたキーを持っていないときは KeyError という例外を吐きます。
①の時点で KeyError となってしまい以後の処理を継続できません。

そこで dict. get() の登場です。
"c" をキーとする値が無いとき、②なら None が、③なら第2引数の 789 が結果として返されます。

    def get(self, key, default=None):
        return self[key] if key in self else default

内部的にこんなコードが動いているような感じです。
(実際は組み込み型なので Python で書かれているわけではありません。後でソースも参照します)

うかつに [] でアクセスして KeyError したくないときにとても重宝します。
例えば値があるときだけ処理したい(値がなくてもエラーしない)場合には、セイウチ演算子を使ってこんな風に書いたりします。

if value := dic.get(key):
    # value を使う処理

しかし、当たり前のように手癖で dict. get() を使うのは必ずしも適切とは言えない場合があります。

事例1

json_result.get("result").get("value")

{"result":{"value":"!!!"}} みたいなJSONオブジェクトから result -> value を取り出したいとき。
result が無くても KeyError しないように dict. get() を使っていると考えられますが、
json_result.get("result") が None を返すときは None.get("value") を呼ぶことになり、 AttributeError が発生してしまいます。
思いがけない AttributeError を例外処理するより、素直に json_result["result"]["value"] と書いて
キーが無いときは KeyError を吐いてもらったほうが問題を的確に表現していてわかりやすいと思います。

事例2

int(request.GET.get("count"))

辞書(に類するマッピング型の)オブジェクトの値を int() など他の関数に渡すとき。
渡される関数や値を受け取る側が None を想定していなければ結局エラーが出てしまいます。
(ちなみに int() の場合、引数が数値と見なせない場合は ValueError を吐きますが、None など数値にできない型の場合は TypeError になります)
であれば、これも、 dict. get() を使うのでなく、素直に [] でアクセス int(request.GET["count"]) して、
キーが無いときは KeyError を出したほうがデバッグしやすいのではないでしょうか。

CPythonの場合

せっかくなので CPython 3.13.3 のコードで []dict. get() を比べてみます。
(CPythonは C言語で書かれた Pythonの標準的な実装です。一般に「Python」と言えばこの CPython を使うことが多いはずです)
dict型は dictobject.c のここに定義があり、メソッドは Mapプロトコルのメソッド群 mapp に列挙されています。

Pythonの言語仕様で、[] は特殊メソッド __getitem__() が呼ばれることになっています。
dict. __getitem__() は dictobject.c の dict_subscript() で実現されます。

一方の dict. get() は、dictobject.c.h に定義される DICT_GET_METHODDEF というマクロで dict_get() が指定されています。
_PyCFunction_CAST は dict_get を PyCFunction にキャストしているだけなので無視してかまいません)
dict_get() は引数チェック(第2引数 default を省略したときの処理)をしているだけで、実体は dictobject.c の dict_get_impl() です。

[] を実現している dict_subscript()
dict. get() の実体 dict_get_impl() を見比べると、基本的な処理の流れは同じです。
_PyObject_HashFast() で key のハッシュを作り、
_Py_dict_lookup_threadsafe() で key に対応する値を参照します。
違うのは指定されたキーを持っていない(値が無い)とき。
[] は、__missing__() があればそれを呼び、なければ KeyError を raise します。
dict. get()default_value の参照カウンタを +1 するだけです。
仕様に則った理に適った実装で、どちらかが取り立てて非効率な処理をしているわけではないことがわかります。

結論

キーが無いときのデフォルト処理を考慮できないなら、素直に [] を使って KeyError したほうが対処しやすいのではないかと思う次第です。
もちろん何かしらの適切な初期値を渡せる場合はこの限りではありません。
int(request.GET.get("count", 10)) なら納得できます(この場合も ValueError 対策は必要ですが)

※ あくまで tama 個人の感想で、サブスレッドの見解を代表するものではありません。
※ そんな tama 自身も長年の Python 生活で考え方が変わることもあり、あくまで現時点での考えにすぎません。
※ これが唯一の正解、という話ではないので、ときおり自分で書いたりレビューしたりするコードを考えるきっかけにしてもらえれば幸いです。

この記事を書いた人

tama