【Python】引数の省略とNoneを区別する:センチネルで更新処理を作る

PythonのTopに戻る


更新関数で「引数を省略したら変更しない」「Noneを渡したら空の値にする」を分けたい場合は、専用のセンチネルオブジェクトを既定値に使う。Noneを未指定の印にも使ってしまうと、この二つを区別できない。object()で一つだけ作った目印を、isで比較すれば三つの状態を明確に扱える。

未指定・空値・通常値を分ける

試料の備考を更新する処理を考える。引数を渡さなければ元の備考を残し、Noneなら備考を空の状態へ変更し、文字列ならその文字列へ置き換えたい。この三つを扱うには、None以外に「渡されていない」を示す値が必要になる。

空文字列を目印にする方法もあるが、空文字列を本当に保存したい場合と衝突する。0、False、空リストなども、データとして有効な場面がある。利用者が普通のデータとして渡さない専用オブジェクトを使う方が、値の意味を保ちやすい。

object()を一度だけ作って使う

example.py

_MISSING = object()


def update_record(record, *, note=_MISSING):
    result = record.copy()
    if note is not _MISSING:
        result["note"] = note
    return result


original = {"id": "A", "note": "keep this"}
unchanged = update_record(original)
cleared = update_record(original, note=None)
updated = update_record(original, note="new")
empty = update_record(original, note="")
for result in (unchanged, cleared, updated, empty):
    print(result)
assert unchanged["note"] == "keep this"
assert cleared["note"] is None
assert updated["note"] == "new" and empty["note"] == ""
assert original == {"id": "A", "note": "keep this"}
assert unchanged is not original

実行結果

{'id': 'A', 'note': 'keep this'}
{'id': 'A', 'note': None}
{'id': 'A', 'note': 'new'}
{'id': 'A', 'note': ''}

noteを省略した場合だけ、既定の_MISSINGがそのまま渡る。明示的なNoneや空文字列は目印と異なるため、更新の対象になる。この例のNoneはキーの削除ではなく、noteというキーにNoneを格納して「値なし」を表す約束である。

等値ではなく同一性で確認する

判定にはnote is _MISSINGを使う。値が似ているかどうかではなく、用意した目印そのものかを確認したいからである。==は型によって比較の処理を定義できるため、意図しない一致や例外が起こる可能性がある。

object()は呼ぶたびに別のオブジェクトを作る。判定側で新しくobject()を呼んでも、既定値として作った目印とは一致しない。モジュール内で_MISSING = object()と一度だけ作り、関数定義と判定で同じものを参照することが重要である。

入力を更新するか、結果を返すかを決める

例ではrecord.copy()で新しい辞書を作り、元の入力を変えずに結果を返す。更新対象が備考という最上位の値なので、浅いコピーで目的を満たせる。入れ子の辞書を内部で書き換えるなら、その部分の共有にも注意する必要がある。

実際のAPIでNoneをキーの削除に割り当てたいなら、その分岐でpopなどを使う設計もできる。ただし、値としてNoneを保存する用途とは両立しない。未指定、削除、空値をすべて別々に扱う場合は、削除用の別の操作や明示的なフラグを設けると分かりやすい。

境界を越える目印として使わない

この単純なセンチネルは、同じプロセス内の関数呼び出しに向いている。JSONへ書き出したり、別プロセスへ送ったり、何でもdeepcopyしたりしても、同じidentityが保たれるとは考えない方がよい。外部との受け渡しには、フィールドの有無や明示的な状態名などの形式を決める。

引数が増えた場合も、すべてをtruthyかどうかで判断しないことが大切である。例えば数値の0やFalseを正常な更新値として受け入れるなら、if valueの分岐では更新が飛ばされてしまう。値の有無と値の内容を別々に判定する。

呼び出し方を読みやすくする

例のnoteはキーワード専用引数なので、update_record(record, note=None)のように意味を示して渡せる。省略時に何もしないという設計とも相性がよい。更新項目が複数になっても、位置だけでどの値を消すかを判断しなくて済む。

動作確認では、引数省略、None、通常の文字列、空文字列をそれぞれ呼ぶ。元データが変わらないことも一緒に確かめれば、更新の意味とコピーの方針を確認できる。センチネルは仕組み自体は小さいが、更新APIの曖昧さを減らすのに役立つ。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る