辞書から無効値を持つ項目を取り除く@python

関数とかクラスのキーワード引数に使うのに、None とか空文字とかが含まれてるとやだよね、的な。

たとえば

1 formatter_params = dict(
2     linenos=form.get("linenos", ""))
3 formatter = HtmlFormatter(**formatter_params)

みたいな。form は何が入ってるかアヤフヤな辞書で、また、HtmlFormatter の __init__ はキーワードパラメータを受け取る、として。

このときに、「妥当な値が入っていない」のにそのまま垂れ流すのがイヤだ。実際「空文字はイヤん」てなエラーを喰らうこともある。

こんな具合:

1 >>> d = dict(a=1, b=None, c="")
2 >>> d
3 {'a': 1, 'c': '', 'b': None}
4 >>> filter(lambda k: d[k], d)
5 ['a']
6 >>> ((k, d[k]) for k in filter(lambda k: d[k], d))
7 <generator object <genexpr> at 0x0000000002A55120>
8 >>> dict(((k, d[k]) for k in filter(lambda k: d[k], d)))
9 {'a': 1}

Python に不慣れだと意味わからんと思うので、少しだけ分解して説明しとく。

まず辞書の生成だけどね、

1 >>> {"a": 1, "b": None, "c": ""}
2 >>> dict(a=1, b=None, c="")

上2つは結果的には同じになります。ちなみに、仮に dict が pure Python で書かれてたとしたら

1 class dict(object):
2     def __init__(self, **kwarg):
3         for k in kwarg:
4             self.__setitem__(k, kwarg[k])

こんななはずです。実際は dict は C で書かれてるので観察は出来ないですが。

filter はいいかな? iterable、つまり列挙可能プロトコルに対して第一引数の callback が「真」の要素を取り出す…ので、辞書相手の場合は key のリストになりますね:

1 >>> filter(lambda k: d[k], d)
2 ['a']

ここまで準備出来たので、filter で得た「欲しいキー」と「どうにかして欲しいキーに対する値」のペアから辞書を再生成する、って流れになるんだけれど、その前に「リスト内包」ね。例えばこんな:

1 >>> ["%03d" % i for i in range(10)]  # リスト版リスト内包
2 ['000', '001', '002', '003', '004', '005', '006', '007', '008', '009']
3 >>> ("%03d" % i for i in range(10))  # タプル版リスト内包 (2016-02-11 追記:「ジェネレータ式」が正解。タプル版リスト内包なんて言葉はない。)
4 <generator object <genexpr> at 0x0000000002A55318>
5 >>> tuple(("%03d" % i for i in range(10)))
6 ('000', '001', '002', '003', '004', '005', '006', '007', '008', '009')

もうだいたいわかったよね。あとはこの事実:

1 >>> dict((("a", 1), ("b", None), ("c", "")))
2 {'a': 1, 'c': '', 'b': None}

つまり「キー, 値」のタプル(かリスト)のタプル(かリスト)から辞書を作れる。しかるに

1 >>> d = dict(a=1, b=None, c="")
2 >>> dict(((k, d[k]) for k in filter(lambda k: d[k], d)))
3 {'a': 1}

よろしい?

2016-02-11 追記:
なんか Popular で順位あがって久々にこれみて、今読むと無駄だなぁと思ったもんで、追記しとく。

これ、もちっと楽に書けます。

1 >>> dict(((k, d[k]) for k in d if d[k]))
2 {'a': 1}

要は filter と lambda はいらない。わからない人は最初の投稿のときと同じように分解してみてください。

2016-09-16 追記:
今日はPopular Post 復活記念でなんか昔の投稿読み直し祭り。

いやいや、もっと楽に書けるでしょーよ:

1 >>> {k: d[k] for k in d if d[k]}
2 {'a': 1}

2.7からだったかなこれは。(3.0~3.1 からのバックポートなので、2.6 からかも。)