😸

Pythonの辞書操作チートシート

2020/12/18に公開

概要

※本記事は、随時執筆中です。

本記事では、一般的な辞書操作とそれらの細かい挙動に応じた使い分けと、辞書に関連するモジュールや関数を横断的に紹介しています。

本記事チートシートのような要求操作に対する実装例をコンパクトに俯瞰できるドキュメントが欲しかったのですが、画面幅の都合で詳細が伝わりきらないため、ひととおり執筆するハメになりました(汗)。

本記事をひととおりお読みいただいた後は、ガイドラインとしてチートシートを活用いただければ幸いです。

検証環境

  • Python3.9.0

対象読者

読者のメインターゲットは、Python3.5以上のユーザーです。
検証環境は3.9ですが、Python3.5系以上の互換性は調査・考慮しています。

辞書にフォーカスした解説のため、基礎的な知識・単語については、その他の記事等で補完ください。

Python中級者以上の方は、目次とチートシートを見て気になった章だけお読みください。

公式ドキュメント

最新の仕様や、より正確な仕様の理解には、公式ドキュメントも参照ください。

https://docs.python.org/ja/3/library/stdtypes.html#dict

基礎編

辞書を作成する

{}で辞書を作成する

辞書リテラル{}で辞書(以後、dictとする)を作成できます。

性能面でも優れているもっともポピュラーな方法ですので、基本的にこの方法を使用しましょう。

dic = {"key": "test"}

キーワード引数からdictを作成する

dict関数でキーワード引数に値を渡すと、そのままキーと値として定義できます。

ただし、dict関数を使用する場合、次の言語仕様上の制約があります。

  • キーワード引数に予約語は使用できません
  • キーワード引数に数値は使用できません
dic = dict(key="test")

# 例外
dic = dict(from=0)
dic = dict(1=0)
# => SyntaxError: invalid syntax

キーと値のタプルリストからdictを作成する

dict関数に、キーと値のタプルを要素とするリストを渡すと、dictを作成できます。

dic = dict([("key1", 1), ("key2", 2)])
# => {'key1': '1', 'key2': '2'}

辞書内包表記でdictを作成する

辞書内包表記を用いると、イテラブル[1]の処理結果からdictを作成できます。
{キー:値 for 変数名 in イテラブル}のように記述します。

dic = {str(x):x for x in range(5)}
# => {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}

辞書内包表記を、リスト内包表記で書き直すと次のようになります。

arr = [(str(x), x) for x in range(5)]
dic = dict(arr)
# => {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}

キーのリストからdictを作成する

fromkeys

fromkeysは、第1引数に与えられたイテラブルをキーとするdictインスタンスを生成します。

  • 第1引数: キーとするイテラブル
  • 第2引数: 初期値とする値。省略時はNone
dic = dict.fromkeys(["a", "b"])
# => {'a': None, 'b': None}

dic = dict.fromkeys(["a", "b"], 0)
# => {'a': 0, 'b': 0}

辞書内包表記で代替可能ですので、fromkeysは忘れてしまいましょう。

dic = {key:0 for key in ["a", "b"]}
# => {'a': 0, 'b': 0}

要素(キーと値)を登録する

キーを指定し、値を渡すことで要素を登録できます。
すでに対象キーが存在する場合は、値が上書きされます。

dic = {}
dic["key"] = "val"

__setitem__

dic[key] = valueは、dic.__setitem__(key, value)と等価です。

dic = {}
dic.__setitem__("key", "val")

キーに対応する値を取得する

キーに対応する値を取得には、次のようにします。
キーが存在しない場合は、KeyErrorが発生します。

dic = {"name": "test"}
val = dic["name"]

__getitem__

dic[key]は、dic.__getitem__(key)と等価です。

dic = {"name": "test"}
val = dic.__getitem__("name")

get

KeyErrorを無視したい場合は、getメソッドを使用できます。

getは、指定したキーの値を返します。
指定したキーが存在しない場合は、第2引数の値を返します。

  • 第1引数: キー
  • 第2引数: キーが登録されていない場合に返す値。省略時はNoneを返す
# キーが存在しない場合は、Noneが返る
val = dic.get("name")

# キーが存在しない場合は、第2引数の値が返る
val = dic.get("name", None)

setdefault

setdefaultは、キーが登録されていない場合はキーとデフォルト値を登録した上で、キーに対応する値を返します。

  • 第1引数: キー
  • 第2引数: キーが登録されていない場合に登録する値。省略時はNoneを登録
# キーが存在していないので、新たなキーと値を登録した上で、値を返す
val = {}.setdefault('new', 0)
# => 0

# すでにキーが存在しているので第2引数は無視される
val = {'new': 0}.setdefault('new', 1)
# => 0

# 第2引数を省略するとNoneが登録される
val = {}.setdefault('new')
# => None

要素を削除する

del

delは、指定したキーを削除します。

  • 指定したキーが存在しない場合は、KeyErrorが発生する
  • 削除以外の余計な処理を行わないので性能がもっとも優れる
del dic["name"]

pop

要素を削除するとともに、削除された値を受け取りたい時やKeyErrorを無視したい場合は、popメソッドを使用します。

popは、指定したキーを削除し、値を返します。

  • 第1引数: キー
  • 第2引数: キーが登録されていない場合に返す値。省略時はKeyErrorが発生
# キーが存在しない場合は、KeyErrorが発生する
val = dic.pop("name")

# キーが存在しない場合は、第2引数の値を返す
val = dic.pop("a", None)

popitem

popitemメソッドは、要素を1つ削除し、キーと値のタプルを返します。

  • dictの要素が空の場合、KeyErrorを送出する
  • Python3.7から、最後の要素から削除されることが保証された[2]
  • Python3.7未満は、無作為な順序で要素を削除する
dic = {"a": 0}
key_value = dic.popitem()
# => ('a', 0)

複数の要素を削除する

複数の要素を削除したい場合は、次のようにします。
なお、delはリスト内包表記とともに利用できません。

dic = {"key1": 1, "key2": 2, "key3": 3}
some_keys = ["key1", "key2"]

# 方法1
for key in some_keys:
  del dic[key]

# 方法2(パフォーマンス的には方法1を推奨)
[dic.pop(key, None) for key in some_keys]

すべての要素を削除する

clear

すべての要素を削除します。

dic.clear()
# => {}

キーを変更する

popを用いることで簡潔に実現できます。

# キーが存在しない場合は、KeyErrorが発生する
dic["new"] = dic.pop("old")

要素(キーや値)を列挙する

要素(キーや値)を列挙するには、次のようにします。

# キーを列挙する
for key in dic:
  print(key)

# キーを列挙する
for key in dic.keys():
  print(key)

# 値を列挙する
for value in dic.values():
  print(value)

# 要素(キーと値のタプル)を列挙する
for key, value in dic.items():
  print(key, value)

# 要素(キーと値のタプル)を列挙する
# Python2系のみ
# Python3系ではiteritemsはitemsに統合されました。(2系ではiteritemsの性能はitemsより優れていました。)
for key, value in dic.iteritems():
  print(key, value)

ビューオブジェクトについて

dictが持っている列挙用メソッドkeys values itemsは、ビューオブジェクトと呼ばれるインスタンスを返します。
ビューオブジェクトは、主に列挙に関する機能だけ提供します。

dic = {"a": 0}
keys = dic.keys()
# => dict_keys(['a'])

values = dic.values()
# => dict_values([0])

items = dic.items()
# => dict_items([('a', 0)])

# このように列挙処理が可能
for key, value in items:
  print(key, value)

ビューオブジェクトは、ソースに要素を追加した場合、同期的に動作します。

dic = {"a": 0}
items = dic.items()

# ソースに要素を追加・更新した場合は同期的に動作
dic["b"] = 0
items
# => dict_items([('a', 0), ('b', 0)])

マッピング型について

dictを利用していると、マッピング型と呼ばれる型に遭遇するかもしれません。

Pythonにおいてdictとは、マッピング型(厳密にはMutableMapping)のインタフェース(値を登録・参照したり、列挙したりする機能)を実装したひとつの型といえます。

インタフェースを知っていると、よりPythonの理解が深まるでしょう。

https://docs.python.org/ja/3/library/collections.abc.html

コピー編

シャローコピーする

シャローコピーの方法を紹介します。

いくつか実現方法がありますが、copyメソッドだけ覚えればよいでしょう。

src = {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 30}}

# 方法1
dest1 = dict(**src)

# 方法2
dest2 = dict(src)

# 方法3
dest3 = src.copy()

シャローは浅いを意味し、ネストしたdict等の値はコピーしません。
そのため、次のような副作用が生じます。

src = {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 30}}

dest1 = dict(**src)
dest2 = dict(src)
dest3 = src.copy()

# コピーしたdictを変更したつもりが、様々な箇所に影響してしまう
dest1["nest"]["age"] = 0
print(src)   # => {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 0}}
print(dest1) # => {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 0}}
print(dest2) # => {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 0}}
print(dest3) # => {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 0}}

シャローコピーする際は、影響範囲を考えて利用しましょう。

ディープコピーする

ディープコピーの方法を紹介します。

copyモジュールのdeepcopyで実現できます。
ディープコピーは、ネストされたdictも再帰的にコピーします。

import copy
src = {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 30}}
dest = copy.deepcopy(src)

# 結果確認
dest["nest"]["age"] = 5
print(src)   # => {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 30}}
print(dest) # => {"name": "bob", "age": 20, "nest": {"name": "mary", "age": 5}}

コピーの挙動を変更する

コピーの独自実装をする場合は、__copy__(シャローコピー用)メソッドと__deepcopy__(ディープコピー用)メソッドを実装します。

実装例については、割愛します。

class MyDict(dict):
  def __copy__(self):
    ...
  def __deepcopy__(self, memo):
    ...

マージ編

2つ以上のdictをマージして、1つのdictにしたい場合の実現方法を紹介します。
方法により挙動が異なるので、状況に応じて使い分けてください。

マージする(衝突するキーの値は上書き)

キーが衝突した場合、値を上書きするマージ方法を紹介します。

バージョン毎にプラクティスが異なるため、バージョンごとに紹介します。

Python3.5未満

dict関数の第1位置引数にソースdictを渡し、アンパック記法を併用することでマージされたdictを作成できます。
ソースdictを直接更新する場合は、updateメソッドが使用できます。

dic1 = {"name": "bob", "age": 20}
dic2 = {"name": "mary"}

# 新たに作成
dic = dict(dic1, **dic2)
# => {"name": "mary", "age": 20}

# ソースを更新
dic1.update(dic2)
# => {"name": "mary", "age": 20}

Python3.5以上

辞書リテラル内に、アンパック記法**を使用できるようになりました[3]
モダンなアプリケーションでは、よく用いられます。

dic1 = {"name": "bob", "age": 20}
dic2 = {"name": "mary"}

# 新たに作成
dic = {**dic1, **dic2}
# => {"name": "mary", "age": 20}

# ソースを更新
dic1.update(dic2)
# => {"name": "mary", "age": 20}

Python3.9以上

Python3.9からは、和集合演算子|と累算代入演算子|=が導入され、マージする意思をより明示的に表現可能になりました[4]

dic1 = {"name": "bob", "age": 20}
dic2 = {"name": "mary"}

# 新たに作成
dic = dic1 | dic2
# => {"name": "mary", "age": 20}

# ソースを更新
dic1 |= dic2
# => {"name": "mary", "age": 20}

マージする(衝突するキーは許容しない)

Pythonの言語仕様上、関数に渡したキーワード引数が衝突するとTypeErrorが発生します。

この仕様を利用して、衝突するキーは許容しないマージを実現します。

dic1 = {}
dic2 = {"name": "test"}

dic = dict(**dic1, **dic2)
# => {"name": "test"}

# キーワード引数と組み合わせることも可能
dic = dict(key1="val1", key2="val2", **dic2}
# => {"key1": "val1", "key2": "val2", "name": "test"}

キーが衝突した場合は、TypeErrorが発生します。

dic1 = {"name": "bob", "age": 20}
dic2 = {"name": "mary"}

# キーが衝突する場合はマージ不可
dic = dict(**dic1, **dic2)
# => TypeError: func() got multiple values for keyword argument 'name'

この方法は、dict関数に限った話でなく、関数全般に応用が可能です。

dic1 = {}
dic2 = {"name": "test"}

def func(**kwargs):
  pass

func(**dic1, **dic2)

関数が可変長キーワードを受け入れ可能な場合も、重複したキーが渡ってくるようなことはありません。

# 可変長キーワード引数に、重複してキーが渡ってくることもありません
dic1 = {"name": "bob", "age": 20}
dic2 = {"name": "mary"}

def func(name, age, **kwargs):
  pass

func(**dic1, **dic2)
# => TypeError: func() got multiple values for keyword argument 'name'

キーに空文字・記号・予約語が含まれていても問題ありません。

dic = {"": 0, "@": 0, "from": 0}
dict(**dic)
# => {'from': 0, '@': 0, '': 0}

ただし、この方法で使用できるキーは文字列に限ります。

dic = {0: 0}
dict(**dic)
# => TypeError: func() keywords must be strings

{}dictの混同に注意

{}によるマージとdict関数によるマージの挙動は異なるため、混同しないように注意しましょう。

dic1 = {"name": "bob", "age": 20}
dic2 = {"name": "mary"}

dic = {**dic1, **dic2}
# => {"name": "mary", "age": 20}

dic = dict(**dic1, **dic2)
# => TypeError: func() got multiple values for keyword argument 'name'

ディープマージ(ネストした辞書をマージ)する

前章で紹介したマージは、ネストされたdictどうしのマージは行ってくれません。
例を見てみましょう。

dic1 = {"nest": {"name": "bob", "age": 20}}
dic2 = {"nest": {"name": "mary"}}

# 方法1
{**dic1, **dic2}
# => {'nest': {'name': 'mary'}}

# 方法2
dict(dic1, **dic2)
# => {'nest': {'name': 'mary'}}

# 方法3
dic1 | dic2
# => {'nest': {'name': 'mary'}}

# 方法4
dic1.update(dic2)
# => {'nest': {'name': 'mary'}}

# 方法5
dic1 |= dic2
# => {'nest': {'name': 'mary'}}

ネストしたdictは、dictごと置き換えられています。
次のように、元のソースをベースにしたままマージするにはどうすればよいでしょうか。

a = {"nest": {"name": "bob", "age": 20}}
b = {"nest": {"name": "mary"}}

dic = deep_merge(a, b)
# => {'nest': {"name": "mary", "age": 20}}

上記のような挙動でディープマージを行いたい場合、関数を自作したり、ライブラリを使用する必要があります。
参考までに、ディープマージするためのコード例を載せておきます。

def merge_objects(obj1, obj2, *, deep: bool = True):
    if deep:
        import copy

        obj1 = copy.deepcopy(obj1)
        obj2 = copy.deepcopy(obj2)

    if isinstance(obj1, list):
        if not isinstance(obj2, list):
            raise TypeError(
                f"cant merge for not same type. {type(obj1)} : {type(obj2)}"
            )
        obj1 = obj1 + obj2
    elif isinstance(obj1, set):
        if not isinstance(obj2, set):
            raise TypeError(
                f"cant merge for not same type. {type(obj1)} : {type(obj2)}"
            )
        obj1 = obj1.union(obj2)
    elif isinstance(obj1, dict):
        if not isinstance(obj2, dict):
            raise TypeError(
                f"cant merge for not same type. {type(obj1)} : {type(obj2)}"
            )

        for key in obj2.keys():
            obj2_val = obj2[key]
            if isinstance(obj2_val, (list, set, dict)) and key in obj1:
                obj1_val = obj1[key]
                merged = merge_objects(obj1_val, obj2_val, deep=False)
                obj1[key] = merged
            else:
                obj1[key] = obj2_val

    else:
        raise TypeError(f"cant merge for unmergeable type: obj1 - {type(obj1)}")

    return obj1

a = {"nest": {"name": "bob", "age": 20, "list": [1, 2]}}
b = {"nest": {"name": "mary", "list": [3, 4]}}

dic = merge_objects(a, b)
# => {'nest': {'name': 'mary', 'age': 20, 'list': [1, 2, 3, 4]}}

問い合わせ編

キーの存在を調べる

inを使うことで、キーが存在するか調べることができます。
notを組み合わせることで、キーが存在していないことも調べることができます。

dic = {"a": 0}
"a" in dic
# => True

"b" not in dic
# => True

not "b" in dic
# => True

値の存在を調べる

値の存在を調べる場合は、invaluesメソッドを併用します。

dic = {"a": 0}
0 in dic.values()
# => True

キーに対するinはハッシュ探索のためコストが少ないですが、値に対するinは線形探索となるためコストが大きいです。
何度も走査する場合は、set型などのハッシュ探索を実装したオブジェクトを使用しましょう。

dic = {"a": 0, "b": 1, "c": 0}
values = set(dic.values())
# => {0, 1}

0 in values

複数のキーの存在を調べる

複数のキーを調べるには、all(すべての要素がTrueか判定)やany(いずれかの要素がTrueか判定)と、for文やリスト内包表記などと組み合わせて実現します。

dic = {"a": 0, "b": 0}

all(key in dic for key in {"a", "b"})
# => True

all(key in dic for key in {"a", "c"})
# => False

any(key in dic for key in {"a", "c"})
# => True

any(key in dic for key in {"c", "d"})
# => False

紹介したコードは、if文などと組み合わせて使用ください。

if "a" in dic:
  ...

if all(key in dic for key in {"a", "b"}):
  ...

要素をフィルタする

dictから任意の要素を抽出するには、次のようにします。

dic = {"a": 0, "b": 0}
condition = {"a"}

result = {}
for key, value in dic.items():
  if key in condition:
    result[key] = dic[key]
# => {"a": 0}

なお、condition = {"a"}setインスタンスを生成しています。
setインスタンスは、ユニークなリスト(値を持たないdictのようなもの)で、ハッシュ探索による高速な検索を行えます。

辞書内包表記

上記のコードは、やや冗長です。辞書内包表記を用いることでより簡潔にできます。
書き方が独特ですが、性能面でも優遇されているので、積極的に使いましょう。

dic = {"a": 0, "b": 0}
condition = {"a"}

result = {key:value for key, value in dic.items() if key in condition}
# => {"a": 0}

filter

filter関数は、第1引数に評価関数、第2引数にイテラブルを取ります。
各要素を評価関数で評価し、Trueと判定される要素のみを抽出するイテレータ[5]を生成します。

評価関数は1つの引数を取り、キーと値のタプルを受け取ります。

dic = {"a": 0, "b": 0}
condition = {"a"}

# キーが"a"のみ抽出する
result = dict(filter(lambda key_value: key_value[0] in condition, dic.items()))
# => {"a": 0}

# 値が"a"のみ抽出する
result = dict(filter(lambda key_value: key_value[1] in condition, dic.items()))
# => {}

要素数を調べる

要素数を調べる場合は、lenを使用します。

dic = {}
len(dic)
# => 0

集計する

要素を集計したい場合は、Counterを利用できます。
詳しい使い方については、要素を集計した辞書を参照ください。

from collections import Counter

dic = {
    "Google": "America",
    "Amazon": "America",
    "Facebook": "America",
    "Apple": "America",
    "Toyota": "Japan"
}

countries = dic.values()
Counter(countries)
# => Counter({'America': 4, 'Japan': 1})

フラット化する

TODO: 執筆中

ソート編

dictをソートする方法を紹介します。

デフォルトの順序について

Python3.7からは、dictが返す要素の順序は次のようになりました。

  • 挿入順で要素を返す[2:1]
  • 要素の更新は順序に影響を与えない

Python3.7未満で挿入順序を管理する場合はOrderedDictを利用ください。

数値をソートする

sorted関数は、任意のキーでソートされたリストを返します。

keyにソート用の関数を渡すと、その関数の戻り値の大小が昇順でソートされます。

# 数値でをソートする
dic = {"a": 1, "b": 0, "c": -1}

# 値を昇順にソートし、値をリスト化する
sorted(dic.values(), key=lambda x: x)
# => [-1, ,0 ,2]

# 値を昇順にソートし、値をリスト化する(keyを省略した場合は、暗黙的に列挙された値の大小が評価されます)
sorted(dic.values())
# => [-1, ,0 ,2]

# itemsでタプルを受け取る場合は、0 - キー, 1 - 値 でインデックスアクセスできる
# 値を昇順にソートし、要素をリスト化する
sorted(dic.items(), key=lambda x: x[1])
# => [('c', -1), ('b', 0), ('a', 1)]

# 値を降順にソートし、要素をリスト化する
sorted(dic.items(), key=lambda x: -1 * x[1])
# => [('a', 1), ('b', 0), ('c', -1)]

# 値を絶対値を昇順にソートし、要素をリスト化する
sorted(dic.items(), key=lambda x: abs(x[1]))
# => [('b', 0), ('a', 1), ('c', -1)]

# このようにソートされたキーと値を処理できる
for key, value in sorted(dic.items()):
    print(key, value)

文字列をソートする

文字列もsorted関数を利用し、同様にソートできます。

文字列にはそれぞれ文字コードが割り当てられており、文字コードに基づいた辞書順となります。
辞書順は、先頭の文字順で並び、2文字目以降も同様に繰り返し並び替えられるイメージになります。

なお、文字コードは、ord関数で確認できます。

dic = {"a": "a", "b": "12", "c": "A", "d": "AA", "e": "2", "f": "1"}

# 単純に値でソート
sorted(dic.items(), key=lambda x: x[1])
# => [('f', '1'), ('b', '12'), ('e', '2'), ('c', 'A'), ('d', 'AA'), ('a', 'a')]

# 大小文字区別なくソート(全て小文字にして比較)
sorted(dic.items(), key=lambda x: str.lower(x[1]))
# => [('f', '1'), ('b', '12'), ('e', '2'), ('a', 'a'), ('c', 'A'), ('d', 'AA')]

# 文字コード確認
ord("0")
# => 48

ord("A")
# => 65

ord("a")
# => 97

逆順にする

sorted

sorted関数にreverse=Trueを指定し、イテラブルを渡すと、列挙順序を逆にしたlistを返します。

dic = {"a": 0, "b": 1}

sorted(dic.keys(), reverse=True)
# => ['b', 'a']

reversed

reversed関数に、リバーシブル[6]を渡すと、列挙順序を逆にしたイテレータを返します。

ただし、Python3.8未満ではdictおよびにビューオブジェクトに対してreversedを使用できません[7]
その場合は、sorted関数かOrderedDictを利用してください。

dic = {"a": 0, "b": 1}

list(reversed(dic.keys()))
# => ['b', 'a']

reversedはイテレータを返すため、実体化するにはlist等にイテレータを渡す必要があります。

特殊な辞書編

初期値を保持した辞書を作成する

初期値保持したdictを作成する方法を紹介します。

たとえば、履歴書を模したdictに家族構成と学歴を登録するとしましょう。
家族構成と学歴は、リストにいくつか値を登録することを想定しています。

person = {}
person["family"].append("father")  # => KeyError: 'family'
person["background"].append("2000/4/1 Python高校入学")
person["background"].append("2003/4/1 Python大学入学")

上記のコードは、2行目でエラーとなりました。
dict初期化時に、リストを要素として登録していないので当然の結果ですね。

このような場合、defaultdictを用いることが可能です。

defaultdict

defaultdictは、コンストラクタにインスタンスを生成する引数なしで実行できる関数を渡すことで、その関数が返したインスタンスを初期値とします。

次の例は、list()で生成されるインスタンスを初期値としています。

from collections import defaultdict

# インスタンスを生成する関数をコンストラクタに渡します
person = defaultdict(list)

person["family"].append("father")
person["background"].append("2000/4/1 Python高校入学")
person["background"].append("2003/4/1 Python大学入学")
# => defaultdict(<class 'list'>, {'family': ['father'], 'background': ['2000/4/1 Python高校入学', '2003/4/1 Python大学入学']})

intやstrを渡すこともできます。

dic1 = defaultdict(int)
dic1["new"]
# => 0

dic2 = defaultdict(str)
dic2["new"]
# => ""

もちろん、関数を渡すこともできます。

def create_list():
    return ["hello"]

dic1 = defaultdict(create_list)
dic1["new"]
# => ["hello"]

挿入順を保持した辞書を作成する

OrderedDict

要素の挿入順を保持したdictを作成するには、OrderedDictを使用します。

ただし、Python3.7以降は標準のdictも挿入順を保持するようになったので、OrderedDictは使わないでいいでしょう[2:2]

from collections import OrderedDict
dic = OrderedDict()

dic["key1"] = 0
dic["key2"] = 0

for item in dic.items():
  print(item)
# => ("key1", 0)
# => ("key2", 0)

要素を集計した辞書を作成する

Counter

要素を集計するにはCounterというクラスが利用できます。

コンストラクタに、イテラブルを渡すと同じ要素の個数を集計できます。

from collections import Counter

data = ["tokyo", "osaka", "osaka"]
dic = Counter(data)
# => Counter({'osaka': 2, 'tokyo': 1})

文字列もイテラブルなため集計可能です。

from collections import Counter

data = "すもももももももものうち"
dic = Counter(data)
# => Counter({'も': 8, 'す': 1, 'の': 1, 'う': 1, 'ち': 1})

dictの初期化と同じように、Counterインスタンスを生成できます。

from collections import Counter

data = {"osaka": 1, "tokyo": 2}
dic = Counter(data)
# => Counter({'tokyo': 2, 'osaka': 1})

dic = Counter(osaka=1, tokyo=2))
# => Counter({'tokyo': 2, 'osaka': 1})

Counterは、dictのサブクラスのためdictと同じように扱うことができます。
ただし、いくつか集計用に拡張されたメソッドや異なる挙動が存在するので紹介します。

値を取得する

dictと同じようにキーを指定し、値にアクセスできます。
キーが存在しない場合は、dictと挙動が異なり0を返します。

from collections import Counter

data = ["tokyo", "osaka", "osaka"]
dic = Counter(data)
dic["chiba"]
# => 0

update

dict.updateと同じように、複数のdictをマージできます。
dictでは、衝突したキーの値を上書きしますが、Counterにおいては値を加算します。

from collections import Counter

data = ["tokyo", "osaka", "osaka"]
dic = Counter(data)

dic.update({"tokyo": 2})
# => Counter({'tokyo': 3, 'osaka': 2})

subtract

複数のdictをマージし、衝突したキーの値は減算します。
updateの逆の挙動になります。

from collections import Counter

data = ["tokyo", "osaka", "osaka"]
dic = Counter(data)

dic.subtract({"tokyo": 2})
# => Counter({'osaka': 2, 'tokyo': -1})

most_common

値の降順で要素を返します。
引数nを指定でき、n個の要素を返すか、省略またはNoneの場合はすべての要素が返されます。

from collections import Counter

data = ["tokyo", "osaka", "osaka"]
dic = Counter(data)

for x in dic.most_common(1):
  print(x)
# => ('osaka', 2)

for x in dic.most_common():
  print(x)
# => ('osaka', 2)
# => ('tokyo', 1)

elements

それぞれの値と同じ回数キーを繰り返すイテレータを返します。要素が1未満の場合は、キーは返されません。

counter = Counter(a=4, b=2, c=0, d=-2)
sorted(counter.elements())
# => ['a', 'a', 'a', 'a', 'b', 'b']

不変な辞書を作成する

不変性をもつ(作成後にオブジェクトの状態を変えることができない)オブジェクトをイミュータブルなオブジェクトと呼びます。

イミュータブルなコンテナ型はtuplefrozensetがありますが、辞書にイミュータブルな型はないので、別の方法で代替することになります。

辞書のイミュータブル化について、次のPEPで議論されていますので、興味があればご覧ください。

MappingProxyType

MappingProxyTypeは、読み出し専用の動的辞書ビューを提供します。

from types import MappingProxyType

src = {"test": 1}
dic = MappingProxyType(src)

dic["test"]
# => 1

dic["test"] = 2
# => TypeError: 'mappingproxy' object does not support item assignment

ソース辞書を変更すると同期的に動作するので、本当の意味での不変は保証されませんが、多くのケースで十分に要求を満たすでしょう。

from types import MappingProxyType

src = {"test": 1}
dic = MappingProxyType(src)

src["test"] = 2
dic["test"]
# => 2

dataclass

Python3.7から利用できるdataclassfrozen=Trueオプションを指定すると、イミュータブルなインスタンスを生成できます。

辞書として扱いたい場合は辞書に変換する必要があります。

辞書への変換方法は、dataclassを辞書に変換するを参照ください。

from dataclasses import dataclass

@dataclass(frozen=True)
class Person:
  name: str = "test"
  age: int = 20

obj = Person()

obj.name = "test"
# => dataclasses.FrozenInstanceError: cannot assign to field 'name'

NamedTuple

NamedTupletupleのサブクラスのように(生成されたクラスはNamedTupleのサブクラスでなく、tupleのサブクラスとなる)振る舞い、各インデックスに属性名を付与できます。
tupleはイミュータブルですので、インスタンスの変更は許可されません。

辞書として扱いたい場合は辞書に変換する必要があります。

辞書への変換方法は、NamedTupleを辞書に変換するを参照ください。

from typing import NamedTuple

class Person(NamedTuple):
  name: str = "test"
  age: int = 20

obj = Person()

obj.name
# => "test"

obj[0]
# => "test"

obj.name = ""
# => AttributeError: can't set attribute

obj["name"] = "test"
# => TypeError: 'Employee' object does not support item assignment

辞書から動的にNamedTuple生成もできますが、コードが直感的でないので、あまり使うべきではありません。

from collections import namedtuple
dic = {"name": "test", "age": 20}

Person = namedtuple('Person', dic.keys())  # クラスを生成
obj = Person(**dic)  # インスタンスを生成
# => Person(name='test', age=20)

Pydantic

Pydanticについては〇〇で紹介しますので、ここではイミュータブルにする例のみ紹介します。

辞書として扱いたい場合は辞書に変換する必要があります。

辞書への変換方法は、Pydanticを辞書に変換するを参照ください。

from pydantic import BaseModel

class Person(BaseModel):
  name: str = "bob"
  age: int = 20
  class Config:
    allow_mutation = False  # 変更を許可するか否か。デフォルトはTrue

obj = Person()
obj.name = "test"
# => TypeError: "Person" is immutable and does not support item assignment

不変における注意点

紹介したどの方法も、ネストした辞書に対する書き込みは禁止できません。

from types import MappingProxyType

src = {"nest": {}}
dic = MappingProxyType(src)

dic["nest"]["new"] = 1
dic
# => mappingproxy({'nest': {'new': 1}})

listsetの場合も同様です。
完全なイミュータブルを目指す場合は、ネストしたミュータブルなオブジェクトをMappingProxyTypetuplefrozenset等に置き換えましょう。

from types import MappingProxyType

src = {"dict": MappingProxyType({}), "list": tuple(), "set": frozenset()}
dic = MappingProxyType(src)

dic["dict"]["new"] = 1
# => TypeError: 'mappingproxy' object does not support item assignment

dic["list"].append(1)
# => AttributeError: 'tuple' object has no attribute 'append'

dic["set"].add(1)
# => AttributeError: 'frozenset' object has no attribute 'add'

どの方法を使うべきか

基本的には、MappingProxyTypeで十分です。
後は、ケースに応じて使い分けましょう(大量にデータを処理する必要がなければ、どれも大差ありません)。

  • 単に辞書を読み取り専用にしたい: MappingProxyType
  • IDEによるサポートと検証機能を重視したい: Pydantic
  • IDEによるサポートとデータアクセス速度を重視したい: dataclass
  • IDEによるサポートとメモリ省力化を重視したい:NamedTuple

次の記事も参考にしてください。

検証編

辞書は汎用性が高いですが、どんな構造になっているか、想定しない値が混在していないかなど、ときどきデータの信頼性に疑念をもつ時があります。

ここでは、辞書の構造を定義し、堅牢なプログラミングを行う方法を紹介します。

TypedDict

Python3.8から、TypedDict(特定のキーと型をもっていることを期待する辞書)を宣言できます。
実行時に値の検証はされませんが、mypyなどの静的解析ツールでコーディング時の誤りに気付けます。

from typing import TypedDict

class Person(TypedDict):
  name: str
  age: int

(TODO: 執筆中)

Pydantic

PydanticはPython3.6から対応している、堅牢なプログラミングを行うためのライブラリです。

Pydanticは主に提供する機能は次のとおりです。

  • 検証機能
  • 親切なエラー情報
  • PythonオブジェクトとJSON互換オブジェクトの相互変換
  • OpenAPI生成

Python3.7から導入されたdataclassと似ていますが、Pydanticでは型を活用した多彩な要求に対応できます。

公式ドキュメント

導入方法や詳しい利用方法は公式ドキュメント等を参照ください。

https://pydantic-docs.helpmanual.io/

辞書の検証

基本的な使い方は、BaseModelを継承したクラスのフィールドに型ヒントを付与すると、インスタンス生成時に値を検証します。

from datetime import datetime
from pydantic import BaseModel

class Person(BaseModel):
  name: str
  age: int
  birth_date: datetime = None

obj = Person(name="bob", age=20)
# => Person(name="bob", age=20, birth_date=None)

# ageに数字以外が混入しているのでエラー
Person(name="bob", age="20歳")
# => ValidationError age value is not a valid integer

# 型が違っていても、解釈可能なら検証に成功し、型変換を行う(日付はISO形式の文字列を解釈可能)
obj = Person(name=1, age="20", birth_date="2020-1-1T00:00:00Z")
# => Person(name="1", age=20, datetime.datetime(2020, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))

辞書との相互変換は非常に簡単です。
ネストした構造も苦になりません。

import datetime
from typing import List
from pydantic import BaseModel

class Child(BaseModel):
  name: str
  birth_date: datetime.datetime

class Parent(BaseModel):
  name: str
  birth_date: datetime.datetime
  children: List[Child] = []

dic = {
    "name": "bob", "birth_date": datetime.datetime(2000,1,1),
    "children": [
        {"name": "tom", "birth_date": datetime.datetime(2018,1,1)},
        {"name": "mary", "birth_date": datetime.datetime(2020,1,1)}
    ]
}

obj = Parent(**dic)
# => Parent(name='bob', birth_date=datetime.datetime(2000, 1, 1, 0, 0), children=[Child(name='tom', birth_date=datetime.datetime(2018, 1, 1, 0, 0)), Child(name='mary', birth_date=datetime.datetime(2020, 1, 1, 0, 0))])

dic = obj.dict()
# => {'name': 'bob', 'birth_date': datetime.datetime(2000, 1, 1, 0, 0), 'children': [{'name': 'tom', 'birth_date': datetime.datetime(2018, 1, 1, 0, 0)}, {'name': 'mary', 'birth_date': datetime.datetime(2020, 1, 1, 0, 0)}]}

多様するのは好ましくないですが、Pydanticではこんなラフなこともできてしまいます。

from typing import Union
from pydantic import BaseModel, parse_obj_as

class Person1(BaseModel):
  name: str
  age: int

class Person2(BaseModel):
  fullname: str
  age: int

data = [
    {"fullname": "bob", "age": 20},
    {"name": "mary", "age": 18}
]

for row in data:
  print(parse_obj_as(Union[Person1, Person2], row))
  # => Person2(fullname='bob', age=20)
  # => Person1(name='mary', age=18)

従来なら、愚直にif文で分岐処理を書くところですが、Union(いずれかの型)を指定すると、いずれかの型で解釈する、という書き方ができます。

雑にデータ分析したい場面や、Webアプリケーションで厳密に定義されたデータを返したい場合など、さまざまな状況に応じた使い方ができます。

環境変数を検証する

辞書的をもつ代表的なリソースの1つが環境変数です。

os.environ["PATH"]

(TODO: 執筆中)

変換編

あるオブジェクトを辞書に変換する方法や、辞書データを一般的なフォーマットに変換する方法を紹介します。

dataclassを辞書に変換する

dataclassは、asdict関数で辞書化できます。

from typing import List
from dataclasses import dataclass, asdict

@dataclass
class Child:
  name: str

@dataclass
class Parent:
  name: str
  children: List[Child]

obj = Parent(name="bob", children=[Child(name="tom"), Child(name="mary")])
asdict(obj)
# => {'name': 'bob', 'children': [{'name': 'tom'}, {'name': 'mary'}]}

__dict__メソッドは、ネストしたdataclassを再帰的に辞書化しないため利用してはいけません。

from typing import List
from dataclasses import dataclass, asdict

@dataclass
class Child:
  name: str

@dataclass
class Parent:
  name: str
  children: List[Child]

obj = Parent(name="bob", children=[Child(name="tom"), Child(name="mary")])
obj.__dict__
# => {'name': 'bob', 'children': [Child(name='tom'), Child(name='mary')]}

NamedTupleを辞書に変換する

NamedTupleは、_asdictメソッドで辞書化できます。

from typing import NamedTuple

class Person(NamedTuple):
  name: str

obj = Person(name="bob")
obj._asdict()
# => {'name': 'bob'}

なお、_asdictは再帰的な辞書化に対応していません。
ネストさせないようにしましょう。

from typing import NamedTuple

class Child(NamedTuple):
  name: str

class Parent(NamedTuple):
  name: str
  children: List[Child]

obj = Parent(name="bob", children=[Child(name="tom"), Child(name="mary")])
obj._asdict()
# => {'name': 'bob', 'children': [Child(name='tom'), Child(name='mary')]}

Pydanticを辞書に変換する

pydantic.BaseModelは、dictメソッドで辞書化できます。

from pydantic import BaseModel

class Child(BaseModel):
  name: str

class Parent(BaseModel):
  name: str
  children: List[Child]

obj = Parent(name="bob", children=[Child(name="tom"), Child(name="mary")])
obj.dict()
# => {'name': 'bob', 'children': [{'name': 'tom'}, {'name': 'mary'}]}

また、includeオプションとexcludeオプションで任意の属性のみ出力が可能です。

from pydantic import BaseModel

class Person(BaseModel):
  name: str
  age: int
  sex: str

obj = Person(name="bob", age=20, sex="male")

obj.dict(include={"name"})
# => {'name': 'bob'}

obj.dict(exclude={"name"})
# => {'age': 20, 'sex': 'male'}

辞書とJSON文字列を相互変換する

jsonモジュールを使ったシリアル化

Python標準ライブラリのjsonモジュールを利用すると、PythonオブジェクトとJSON文字列の相互変換が可能です。

jsonモジュールをで利用可能なPythonオブジェクトは次になります。

  • 文字列型
  • 数値型
  • None
  • bool型
  • 辞書型
  • リスト型

ここでは、辞書データをJSON文字列にシリアライズ(ある環境のオブジェクトをバイト列や特定のフォーマットに変換。シリアル化とも呼ぶ)してみましょう。

import json

dic = {"name": "test"}
json_str = json.dumps(dic)
# => '{"name": "test"}'

辞書をJSON文字列にシリアライズしたら、次のようにファイルへ書き出してみましょう。

json_str = '{"name": "test"}'

# ファイルへ書き込む
with open("sample.json", mode="w") as f:
  f.write(json_str)

書き出したJSONファイルを読み込み、辞書にデシリアライズ(外部データをある環境のオブジェクトとして復元。逆シリアル化とも呼ぶ)してみましょう。

import json

# ファイルを読み込む
with open("sample.json") as f:
  json_str = f.read(json_str)
  # => '{"name": "test"}'

dic = json.loads(json_str)
# => {"name": "test"}

これで、辞書とJSONを相互変換できるようになりました。

JSON未対応データ型を含んだ辞書とJSON文字列を相互変換する

便利なjsonモジュールですが、すべての型をJSON文字列として出力できるわけではありません。

次の例を見てみましょう。

import datetime
import json
json.dumps({"date": datetime.datetime(2000,1,1)})
# => TypeError: Object of type 'datetime' is not JSON serializable

JSONで利用可能な型に日付型は含まれないため、シリアライズに失敗してしまいました。
このような場合は、独自に処理を実装する必要があります。

日付型のシリアライズ処理とデシリアライズ処理を行ってみましょう。

import datetime
import json

dic = {"date": datetime.datetime(2000,1,1)}

# 日付型を文字列にしてからJSON文字列にシリアライズする
dic["date"] = datetime.datetime.isoformat(dic["date"])
json_str = json.dumps(dic)
# => '{"date": "2000-01-01T00:00:00"}'

# JSON文字列を辞書にデシリアライズし、文字列を日付型に戻す
dic = json.loads(json_str)
dic["date"] = datetime.datetime.fromisoformat(dic["date"])
# => {'date': datetime.datetime(2000, 1, 1, 0, 0)}

datetimeオブジェクトはisoformatメソッドで、日付型をISO 8601形式の文字列に変換でき、fromisoformatメソッドでISO 8601形式の文字列を日付型に復元できます。

ただし、fromisoformatが利用できるのは、Python3.7からです。
Python3.7未満は、別の方法で処理する必要があります。

import datetime

dic = {"date": "2000-01-01T00:00:00"}
dic["date"] = datetime.datetime.strptime(dic["date"], "%Y-%m-%dT%H:%M:%S")
# => {'date': datetime.datetime(2000, 1, 1, 0, 0)}

Pydanticを使ったシリアル化

Pydanticを使うと、簡潔で高度なシリアル化処理ができます。

シリアライズは次のようにできます。

import json
from datetime import datetime
from pydantic import BaseModel

class Person(BaseModel):
  name: str
  birth_date: datetime

obj = Person(name="bob", birth_date=datetime(2000,1,1))
# => Person(name='bob', birth_date=datetime.datetime(2000, 1, 1, 0, 0))

json_str = obj.json()
# => '{"name": "bob", "birth_date": "2000-01-01T00:00:00"}'

デシリアライズは次のようにできます。

import json
from datetime import datetime
from pydantic import BaseModel

class Person(BaseModel):
  name: str
  birth_date: datetime

json_str = '{"name": "bob", "birth_date": "2000-01-01T00:00:00"}'

dic = json.loads(json_str)
# => {'name': 'bob', 'birth_date': '2000-01-01T00:00:00'}
obj = Person(**dic)
# => Person(name='bob', birth_date=datetime.datetime(2000, 1, 1, 0, 0))

# または
obj = Person.parse_raw(json_str)
# => Person(name='bob', birth_date=datetime.datetime(2000, 1, 1, 0, 0))

文字列"2000-01-01T00:00:00"は日付型datetime.datetime(2000, 1, 1, 0, 0)としてデシリアライズされていますね。

Personクラスのbirth_datedatetimeと定義されていたので、Pydanticがよしなにデータ解釈をしてくれました。

チートシート

これまでの検証結果をまとめました。俯瞰的に挙動を理解するためにご活用ください。

同じ結果を返す複数の実現方法がある場合は、ニュアンスが伝わりやすい方法を用いましょう。

要求 コード例 備考 バージョン
作成 dic = {"key": "value"} dict関数に比べ性能が2倍以上優れる
作成 dic = dict(key="value") 言語仕様上、キーワード引数に予約語や数値リテラルは使用できない
作成 dic = dict([("key", "value")]) キーと値のタプルからdictを作成
作成 dic = {key:value for key, value in [("key", "value")]}
作成 dic = dict.fromkeys(["a", "b"], 0) キーのリストからdictを生成する。ミュータブルな値を初期値(第2引数)とする場合に、副作用がある。辞書内包表記を使おう
登録 dic["name"] = val __setitem__の糖衣構文
登録/取得 dic.setdefault("key") キーが存在しない場合、Noneを登録したうえで、キーの値を返す
登録/取得 dic.setdefault("key", None) キーが存在しない場合、第2引数の値を登録したうえで、キーの値を返す
取得 dic["name"] キーが存在しない場合、KeyErrorが発生。__getitem__の糖衣構文
取得 dic.get("name") キーが存在しない場合、Noneを返す
取得 dic.get("name", None) キーが存在しない場合、第2引数の値を返す
削除 del dic["name"] キーが存在しない場合、KeyErrorが発生
削除/取得 dic.pop("name") キーが存在しない場合、KeyErrorが発生
削除/取得 dic.pop("a", None) キーが存在しない場合、第2引数の値を返す
削除/取得 dic.popitem() 最後の要素を削除し、キーと値のタプルを返す[2:3]。要素が空の場合、KeyErrorとなる。Python3.7未満は、無作為な順序で削除する
キー変更 dic["new"] = dic.pop("old")
全削除 dic.clear()
列挙 for key in dic: keysを使おう
列挙 for key in dic.keys(): キーを列挙する
列挙 for value in dic.values(): 値を列挙する
列挙 for k, v in dic.items(): キーと値のタプルを列挙する
列挙 for k, v in dic.iteritems(): Python3でitemsに統合された[8] <=2.*
コピー dict(**dic) copyメソッドを使おう
コピー dict(dic) copyメソッドを使おう
コピー dic.copy() シャローコピーする
コピー copy.deepcopy(dic) ディープコピーする
マージ/作成 dict(dic1, **dic2) 和集合演算子` `を使おう
マージ/作成 {**dic1, **dic2} 和集合演算子` `を使おう
マージ/作成 `dic1 dic2` キーが衝突する場合、右辺の値で上書き
マージ/更新 dic1.update(dic2) 累算代入演算子` =`を使おう
マージ/更新 `dic1 = dic2` キーが衝突する場合、右辺の値で上書き
マージ/作成 dict(key1=1, **dic2) キーが衝突する場合、TypeErrorが発生
マージ/作成 dict(**dic1, **dic2) キーが衝突する場合、TypeErrorが発生 ^3.5
問合 "a" in dic キーの存在を確認する
問合 "a" in dic.values() 値の存在を確認する
問合 dict(filter(lambda key_value: key_value[0] in cond, dic.items())) 評価関数に一致する要素を抽出する。なるべく辞書内包表記を使おう
問合 {key:value for key, value in dic.items() if key in cond}
問合 len(dic) 要素数を取得する
問合 Counter(dic.values()) イテラブルの集計結果(同じ要素の個数)をdictサブクラスにまとめる
ソート sorted(dic.items()) 昇順でソートされたリストを返す
ソート sorted(dic.items(), key=lambda x: x) 任意の評価関数でソートされたリストを返す
ソート sorted(dic.items(), reverse=True) 逆順にソートされたリストを返す
ソート reversed(dic.items()) 逆順にソートするイテレータを返す ^3.7[7:1]
初期値保持 defaultdict(list) コンストラクタに渡したファクトリ関数の戻り値を初期値とするdictを作成
挿入順保持 OrderedDict() Python3.7以降はdictがOrderedDict相当の順序を保持するようになったため不要 <=3.6.*
集計 Counter(data) イテラブルの集計結果(同じ要素の個数)をdictサブクラスにまとめる
不変な辞書 types.MappingProxyType(dic) 読み出し専用の辞書ビューを提供する ^3.3
不変な辞書 @dataclasses.dataclass(frozen=True) イミュータブルなdataclassで代替する ^3.7
不変な辞書 class Dummy(typing.NamedTuple): イミュータブルなNamedTupleで代替する
不変な辞書 class Dummy(pydantic.BaseModel): イミュータブルなPydanticオブジェクトで代替する。allow_mutation = Falseでイミュータブルになる ^3.6
検証 class Dummy(typing.TypedDict): 型ヒントが利用できる辞書。ランタイムでの検証はしない ^3.8
変換 json.dumps(dic) PythonオブジェクトからJSON文字列を出力する
変換 json.loads(json_str) JSON文字列からPythonオブジェクトを復元する

最後に

自分でまとめているうちに、なんとなく使っていた機能や知らなかった機能を発見できました。
Pythonは歴史があり、いろいろな実装方法があったりするので、少しは混乱を解消できた気がします。

脚注
  1. イテラブル: __iter__をもつ反復可能なオブジェクト ↩︎

  2. pep-0468にて、Python3.7よりdictの要素は順序を保持するようになった ↩︎ ↩︎ ↩︎ ↩︎

  3. pep-0448にて、Python3.5よりアンパック記法がより汎用的となった ↩︎

  4. pep-0584にて、Python3.9よりマージ用の演算子が実装された ↩︎

  5. イテレータ: __iter____next__をもつ反復可能なオブジェクト ↩︎

  6. リバーシブル: __reversed__をもつオブジェクト ↩︎

  7. issue33462にて、Python3.8よりdictはリバーシブルになった ↩︎ ↩︎

  8. pep-3106にて、Python3よりiteritemsitemsに統合された ↩︎

Discussion