通知メールの文面で、宛名は今わかるが発送日は後で決まる、ということがある。str.format()は足りないキーが1つでもあるとKeyErrorになるので、埋められるところだけ先に埋めることができない。
tmpl = "Hello {name}, your order {order_id} ships on {date}"
tmpl.format(name="Alice")
# KeyError: 'order_id'dictのサブクラスに__missing__を定義すると、存在しないキーをd[key]で引いたときの動きを変えられる。str.format_map()はキーをd[key]で引くので、この2つを組み合わせると足りないところを{key}のまま残せる。
使い方
class Keep(dict):
def __missing__(self, key):
return "{" + key + "}"
step1 = tmpl.format_map(Keep(name="Alice"))
# 'Hello Alice, your order {order_id} ships on {date}'
step1.format_map(Keep(order_id=42, date="10/9"))
# 'Hello Alice, your order 42 ships on 10/9'format(**d)ではなくformat_map(d)を使うのがポイントで、**で展開すると普通のdictにコピーされて__missing__が効かない。
__missing__が呼ばれる条件
__missing__はdict.__getitem__がキーを見つけられなかったときにだけ呼ばれる。get()やinでは呼ばれない。
k = Keep(name="Alice")
k["x"] # '{x}'
k.get("x") # None
"x" in k # Falsecollections.defaultdictも同じ仕組みで、__missing__の中で値を作って登録している。
ハマりどころ
残したプレースホルダーに書式指定や属性アクセスが付いていると、そこで失敗する。__missing__が返すのはただの文字列なので、その文字列に対して書式指定や属性アクセスが適用されるからだ。
'{name} {date}' -> 'Alice {date}'
'{name} {price:,}' -> ValueError Cannot specify ',' with 's'.
'{name} {user.email}' -> AttributeError 'str' object has no attribute 'email'
'{name} {items[0]}' -> 'Alice {'
'{name!r} {date!r}' -> "'Alice' '{date}'"{items[0]}は例外にならず、'{items}'の0文字目の{が入る。エラーにならないぶん気づきにくい。!rの場合は文字列{date}のreprが入るので、引用符で囲まれた'{date}'が残る。2回目に埋めると'10/9'になり、結果的に引用符付きの値になる。書式指定や!rを使うテンプレートは、この方法では分割して埋められないと考えた方がよい。
単純な置き換えだけでよければ、標準ライブラリのstring.Template.safe_substitute()でも同じことができる。こちらは$nameの形式で、足りないキーをそのまま残す。
string.Template("Hello $name, ships on $date").safe_substitute(name="Alice")
# 'Hello Alice, ships on $date'関連
- __format__でログにAPIキーを出さないSecret型を作る: 書式指定が
__format__に渡る仕組み