更新関数で「引数を省略したら変更しない」「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固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。
