静かなる名辞

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

【python】dictのkeyとvalueを入れ替える話色々

 dictのkeyとvalueを入れ替えたい、誰でも一度はそう思うのではないだろうか。

 方法は何種類かある。とりあえず誰でも思いつくfor文を回す方法は説明から外すことにするけど。

 あと、この話は簡単なようで実は罠の宝庫。その辺もちょっと説明しておく。

リスト内包表記で書く

 dictのitems()メソッドを呼ぶとこんな形のものが返る。型はdict_itemsというイテレータで、深く考える必要はない。

>>> d = {"a":"あ","i":"い"}
>>> d.items()
dict_items([('a', 'あ'), ('i', 'い')]) 
# こんな形 -> [('a', 'あ'), ('i', 'い')]

 実は、この形のiterableをdictのインスタンス生成時に渡してやると、ちゃんと元のリストになる。

>>> dict(list(d.items())) # 1回リストにしてみても大丈夫!
{'a': 'あ', 'i': 'い'}

 これを応用して、次のように書くことができる。

>>> dict([(v,k) for k,v in d.items()])
{'い': 'i', 'あ': 'a'}

 まあ、単にkeyとvalueを入れ替えたいだけなら次の辞書内包表記の方が気軽だけど。この方法は2つのリストがあって、合体させて辞書にしたいみたいなとき、zipと組み合わせて使うと威力を発揮したりする。

辞書内包表記

 なんとこんな簡単に書ける。

>>> {v:k for k,v in d.items()}
{'い': 'i', 'あ': 'a'}

 辞書内包表記という。なんだか不思議だけど、こうやって書くことができる。

 さ、次は罠パートだ。気合い入れて読んでくれ。

罠1:valueがmutableだとできない(TypeError: unhashable type: ~)

 mutableとは値(内部状態)を変更できるオブジェクトのこと。普段はあまり意識しない概念だが、たとえばpython世界では数字の1はmutableではなくimmutableで、数字の1のオブジェクトを上書きしようとしても2になったり3になったりは絶対にしない。1+1という演算を行うと、新しく数字の2のオブジェクトが生成されて返される*1。一方、たとえばlistでは要素をappendしても同じオブジェクトのまま内部状態が上書きされて、結果的に要素が1つ増える。こういうのがmutable*2

 immutableなオブジェクトの例を挙げると、

  • int
  • float
  • str
  • tuple

 こんな連中がそう。

 mutableなオブジェクトの例は、

  • list
  • set
  • dict

 大体イメージが掴めただろうか?

 さて、辞書のkeyとvalueを入れ替える話に戻る。
 結論から言うと、mutableなオブジェクトは辞書のkeyにはできない。たとえば、listをkeyに使おうとするとこんなエラーが返ってくる。

>>> {[1,2,3]:1}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

 そりゃ、辞書のキーがほいほい書き換わったら困るもんな。だけど、valueにはmutableは普通に使えるので、

>>> d = {123:[1,2,3]}
>>> {v:k for k,v in d.items()}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'

 こんな悲しい目に遭う。対策は適当なimmutableに変換してやること。リストならtupleにするのが良いだろう。

>>> d = {123:[1,2,3]}
>>> {tuple(v):k for k,v in d.items()}
{(1, 2, 3): 123}

 じゃあvalueもdictだったらどうしよう? リストとstrが入り混じってたりしたら? そんなややこしいdictのkeyとvalueを入れ替えようとするくらいなら、処理フローを抜本的に見直せと言いたい。絶対もっとマシな書き方がある。

罠2:複数のkeyが同じvalueに刺さってるとき(エラーは出ないが悲しい結果になる)

 さて、ここまで説明してきたような方法は、実はkeyとvalueが1対1で対応しているときしか使えない。数学用語でいう単射

 複数のvalueが同じkeyに刺さってる場合・・・は、まあ、dictの性質上あり得ないが、複数のkeyが同じ値に刺さる場合はままある。これのkeyとvalueを入れ替えてひっくり返したからといって、dictの性質上、複数のvalueが同じkeyに刺さることはあり得ない。どうなるのか?

>>> d = {"あ":"a", "い":"i", "ア":"a", "イ":"i"}
>>> {v:k for k,v in d.items()}
{'a': 'あ', 'i': 'イ'}

 いずれかの値1つに決まる。こういうのはロジックエラーの温床になって怖いので、せめて警告くらい吐けよという気がしないでもない。ちなみにどれに決まるかは、ドキュメントでも定義されておらずランダムだと思う*3

 これの解決方法は、どんな形で結果を得たいかによって変わってくる。普通は何らかのcollectionにvalues(入れ替える前のkeys)を格納してあげたいことが多いと思う。その場合は、こんな風に書ける。簡潔に書くためにdefaultdictを使い、default_factory(要するにデフォルト値を生成するもん)にはsetを使ってみた。一応defaultdictのドキュメントを貼っておく。

8.3. collections — コンテナデータ型 — Python 3.3.6 ドキュメント

>>> from collections import defaultdict
>>> d = {"あ":"a", "い":"i", "ア":"a", "イ":"i"}
>>> d_inversed = defaultdict(set)
>>> for k,v in d.items():
...     d_inversed[v].add(k)
... 
>>> d_inversed
defaultdict(<class 'set'>, {'a': {'ア', 'あ'}, 'i': {'い', 'イ'}})

 こればっかりは内包表記で書こうとしても上手く行かない。for文を回して愚直に付け加えていく必要がある(と思う)。

まとめ

 意外と考えることが多くて面倒くさいから、安直にkeyとvalue入れ替えてどうこうするの、やめた方が良いときも多いと思った。

 特に、自分で構造をよく理解していないような辞書に対してやってはいけない。大丈夫だって確信できたらやってもいい。

*1:実際には高速化のためにオブジェクトをキャッシュしたり、色々忙しいことをしているらしいが

*2:厳密な説明じゃないと思うが、ご了承いただきたい

*3:でも何回回しても同じ結果になるので、内部的には「順番」はあるんだと思う