静かなる名辞

pythonとプログラミングのこと

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念


【python】dictの集合演算を辞書ビューオブジェクトで行う

はじめに

 pythonのdictは便利なデータ型ですが、複数のdictに対して重複を除去する、逆に共通部分のみを抜き出すといった集合のような演算を行いたいときがあります。

 dictそのものは集合演算をサポートしていませんが、辞書ビューオブジェクトというものを使うと集合演算が容易に行なえます。

辞書ビューオブジェクトとは?

 普段よく使うdict.keys(), dict.values(), dict.items()はすべて辞書ビューオブジェクトのことを指します。

4. 組み込み型 — Python 3.6.5 ドキュメント

 たいていはforでイテレータを取り出すためだけに使われるので、あまり詳細には知られてはいませんが、このように振る舞います。

>>> d = {x:str(x) for x in range(3)}
>>> d
{0: '0', 1: '1', 2: '2'}
>>> items = d.items()
>>> items
dict_items([(0, '0'), (1, '1'), (2, '2')])
>>> d["hoge"] = "fuga"
>>> items
dict_items([(0, '0'), (1, '1'), (2, '2'), ('hoge', 'fuga')])

 要するに、元の辞書をずっと参照していて、元の辞書を変更すれば変更が波及するという性質があるので、ビューという名前になっています。

辞書は集合演算できないが、辞書ビューオブジェクトはできる

 さて、辞書に対して集合演算をかけようとするとエラーになります。

>>> d1 = {x:str(x) for x in range(0, 3)}
>>> d2 = {x:str(x) for x in range(1, 4)}
>>> d1 & d2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for &: 'dict' and 'dict'

 これはもうどうにもなりませんね。サポートされていないんですから。

 しかし、辞書ビューオブジェクトは集合演算をサポートします。意外な仕様ですが、ドキュメントにもそう書いてあります。 

キーのビューは、項目が一意的でハッシュ可能であるという点で、集合に似ています。すべての値がハッシュ可能なら、 (key, value) 対も一意的でハッシュ可能なので、要素のビューも集合に似ています。(値のビューは、要素が一般に一意的でないことから、集合に似ているとは考えられません。) 集合に似ているビューに対して、抽象基底クラス collections.abc.Set で定義されている全ての演算 (例えば、 ==、<、^) が利用できます。
4. 組み込み型 — Python 3.6.5 ドキュメント

 早速やってみましょう。

>>> d1.keys() & d2.keys()
{1, 2}

 できました。ちなみに返る型はsetです。

keysはできるがitemsには制約がある。valuesはできない

 上のドキュメントの引用にも書いてある通り、集合演算が使えるためには

  • 項目が一意的でハッシュ可能である

 という条件があります。考えればわかることですが、それぞれ整理しておくと、

  • keysの場合、上の条件を常に満たすので集合演算可能
  • itemsの場合、値がすべてハッシュ可能なら集合演算可能(listなどのimmutableが値に入っていると駄目)
  • valuesは駄目({"hoge":"a", "fuga":"a"}みたいな辞書があり得るため)

 ということになります。

ちょっとした応用

 itemsで辞書の共通部分を抜き出した新しい辞書を作りたい場合、集合型で返りますから辞書に再変換する必要があります。

>>> dict(d1.items() & d2.items())
{1: '1', 2: '2'}

 値にimmutableが含まれるがキーに基づいて処理できれば良い、という場合は、keysで処理してから辞書内包表記で辞書を組み立てることが出来ます。ただし演算の種類によっては、ややこしくなります。

>>> d1 = {x:[x] for x in range(0, 3)}
>>> d2 = {x:[x] for x in range(1, 4)}
>>> {k:d1[k] if k in d1 else d2[k] for k in d1.keys() | d2.keys()}
{0: [0], 1: [1], 2: [2], 3: [3]}

 これは他の方法でやるべきですかね。

まとめ

 このように、辞書ビューオブジェクトを使うと複雑な辞書同士の操作がときに簡単に行なえます。もし必要になったら使ってみてください。