Pythonの辞書操作チートシート
概要
※本記事は、随時執筆中です。
本記事では、一般的な辞書操作とそれらの細かい挙動に応じた使い分けと、辞書に関連するモジュールや関数を横断的に紹介しています。
本記事チートシートのような要求操作に対する実装例をコンパクトに俯瞰できるドキュメントが欲しかったのですが、画面幅の都合で詳細が伝わりきらないため、ひととおり執筆するハメになりました(汗)。
本記事をひととおりお読みいただいた後は、ガイドラインとしてチートシートを活用いただければ幸いです。
検証環境
- Python3.9.0
対象読者
読者のメインターゲットは、Python3.5以上のユーザーです。
検証環境は3.9ですが、Python3.5系以上の互換性は調査・考慮しています。
辞書にフォーカスした解説のため、基礎的な知識・単語については、その他の記事等で補完ください。
Python中級者以上の方は、目次とチートシートを見て気になった章だけお読みください。
公式ドキュメント
最新の仕様や、より正確な仕様の理解には、公式ドキュメントも参照ください。
基礎編
辞書を作成する
{}
で辞書を作成する
辞書リテラル{}
で辞書(以後、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の理解が深まるでしょう。
コピー編
シャローコピーする
シャローコピーの方法を紹介します。
いくつか実現方法がありますが、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
値の存在を調べる
値の存在を調べる場合は、in
とvalues
メソッドを併用します。
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']
不変な辞書を作成する
不変性をもつ(作成後にオブジェクトの状態を変えることができない)オブジェクトをイミュータブルなオブジェクトと呼びます。
イミュータブルなコンテナ型はtuple
やfrozenset
がありますが、辞書にイミュータブルな型はないので、別の方法で代替することになります。
辞書のイミュータブル化について、次の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から利用できるdataclass
にfrozen=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
NamedTuple
はtuple
のサブクラスのように(生成されたクラスは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}})
list
やset
の場合も同様です。
完全なイミュータブルを目指す場合は、ネストしたミュータブルなオブジェクトをMappingProxyType
、tuple
、frozenset
等に置き換えましょう。
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では型を活用した多彩な要求に対応できます。
公式ドキュメント
導入方法や詳しい利用方法は公式ドキュメント等を参照ください。
辞書の検証
基本的な使い方は、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_date
はdatetime
と定義されていたので、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は歴史があり、いろいろな実装方法があったりするので、少しは混乱を解消できた気がします。
Discussion