Documentation Python

Python 3.7以降では、辞書(dict)が挿入順序を保持することが公式に保証されています。この記事では、バージョンごとの違いと実際の使用例を解説します。

Pythonバージョンごとの辞書の順序

バージョン順序保持保証備考
Python 3.5以前×-順序は不定
Python 3.6実装依存CPythonのみ
Python 3.7以降公式保証全実装で保証

辞書の順序保持の確認

# Python 3.7以降で動作確認
d = {}
d['first'] = 1
d['second'] = 2
d['third'] = 3
d['fourth'] = 4

print(list(d.keys()))    # ['first', 'second', 'third', 'fourth']
print(list(d.values()))  # [1, 2, 3, 4]
print(list(d.items()))   # [('first', 1), ('second', 2), ('third', 3), ('fourth', 4)]

# forループでの順序も保持される
for key, value in d.items():
    print(f"{key}: {value}")
# first: 1
# second: 2
# third: 3
# fourth: 4

辞書リテラルでも順序が保持される

# 辞書リテラルの記述順序が保持される
config = {
    'host': 'localhost',
    'port': 8080,
    'debug': True,
    'timeout': 30
}

for key in config:
    print(key)
# host
# port
# debug
# timeout

dict vs OrderedDict

Python 3.7以降でもOrderedDictには固有の機能があります。

比較

機能dictOrderedDict
挿入順序の保持
順序を考慮した等価比較×
move_to_end()×
popitem(last=False)△(3.7+でlast=Trueのみ)
メモリ効率×

等価比較の違い

from collections import OrderedDict

# 通常のdictは順序を無視して比較
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2)  # True(順序は無視)

# OrderedDictは順序も含めて比較
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2)  # False(順序が異なる)

# dictとOrderedDictの比較は順序を無視
print(d1 == od1)  # True

move_to_end()の活用

from collections import OrderedDict

# LRUキャッシュの簡易実装
class SimpleLRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = OrderedDict()

    def get(self, key):
        if key in self.cache:
            # アクセスされたら末尾に移動
            self.cache.move_to_end(key)
            return self.cache[key]
        return None

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            # 最も古い項目を削除
            self.cache.popitem(last=False)

# 使用例
cache = SimpleLRUCache(3)
cache.put('a', 1)
cache.put('b', 2)
cache.put('c', 3)
print(list(cache.cache.keys()))  # ['a', 'b', 'c']

cache.get('a')  # 'a'にアクセス
print(list(cache.cache.keys()))  # ['b', 'c', 'a']

cache.put('d', 4)  # 容量超過で'b'が削除
print(list(cache.cache.keys()))  # ['c', 'a', 'd']

実践的な例

設定ファイルの順序保持

import json

# 設定を定義順で保持
config = {
    'app_name': 'MyApp',
    'version': '1.0.0',
    'database': {
        'host': 'localhost',
        'port': 5432,
        'name': 'mydb'
    },
    'logging': {
        'level': 'INFO',
        'file': 'app.log'
    }
}

# JSONに出力しても順序が保持される
print(json.dumps(config, indent=2, ensure_ascii=False))

データの順序付きグルーピング

from collections import defaultdict

data = [
    ('2024-01', 100),
    ('2024-02', 150),
    ('2024-03', 120),
    ('2024-01', 80),
    ('2024-02', 200),
]

# グループ化(挿入順序が保持される)
grouped = defaultdict(list)
for month, value in data:
    grouped[month].append(value)

# 最初に出現した順序で出力
for month, values in grouped.items():
    print(f"{month}: {values}")
# 2024-01: [100, 80]
# 2024-02: [150, 200]
# 2024-03: [120]

キーの並び替え

# 辞書のキーをソートして新しい辞書を作成
original = {'c': 3, 'a': 1, 'b': 2}

# キーでソート
sorted_by_key = dict(sorted(original.items()))
print(sorted_by_key)  # {'a': 1, 'b': 2, 'c': 3}

# 値でソート
sorted_by_value = dict(sorted(original.items(), key=lambda x: x[1]))
print(sorted_by_value)  # {'a': 1, 'b': 2, 'c': 3}

# 値で降順ソート
sorted_desc = dict(sorted(original.items(), key=lambda x: x[1], reverse=True))
print(sorted_desc)  # {'c': 3, 'b': 2, 'a': 1}

順序を保持した辞書の更新

# 既存のキーを更新しても順序は変わらない
d = {'a': 1, 'b': 2, 'c': 3}
d['a'] = 100  # 既存キーの更新
print(list(d.keys()))  # ['a', 'b', 'c'](順序は変わらない)

# 削除して追加すると末尾に移動
del d['a']
d['a'] = 1
print(list(d.keys()))  # ['b', 'c', 'a']('a'が末尾に)

Python 3.5以前との互換性

古いバージョンとの互換性が必要な場合の対応方法です。

import sys
from collections import OrderedDict

def create_ordered_dict(*args, **kwargs):
    """バージョンに応じて適切な辞書を返す"""
    if sys.version_info >= (3, 7):
        return dict(*args, **kwargs)
    else:
        return OrderedDict(*args, **kwargs)

# Python 3.6以前でも順序を保証したい場合
config = create_ordered_dict([
    ('host', 'localhost'),
    ('port', 8080),
    ('debug', True)
])

辞書の内部実装

Python 3.6以降の辞書は、コンパクトなハッシュテーブルを使用しています。

# 辞書の内部構造を確認
import sys

d = {'a': 1, 'b': 2, 'c': 3}

# メモリサイズの確認
print(f"dict size: {sys.getsizeof(d)} bytes")

# 同じデータをOrderedDictで
from collections import OrderedDict
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
print(f"OrderedDict size: {sys.getsizeof(od)} bytes")
# OrderedDictの方がメモリ使用量が多い

よくある誤解と注意点

キーの型と順序

# 異なる型のキーでも挿入順序は保持
d = {}
d[1] = 'one'
d['a'] = 'alpha'
d[(1, 2)] = 'tuple'
d[3.14] = 'pi'

print(list(d.keys()))  # [1, 'a', (1, 2), 3.14]

辞書のコピーと順序

original = {'c': 3, 'a': 1, 'b': 2}

# 各種コピー方法で順序は保持される
copy1 = original.copy()
copy2 = dict(original)
copy3 = {**original}

print(list(copy1.keys()))  # ['c', 'a', 'b']
print(list(copy2.keys()))  # ['c', 'a', 'b']
print(list(copy3.keys()))  # ['c', 'a', 'b']

JSON読み込み時の順序

import json

json_str = '{"c": 3, "a": 1, "b": 2}'

# Python 3.7以降では順序が保持される
data = json.loads(json_str)
print(list(data.keys()))  # ['c', 'a', 'b']

# 明示的にOrderedDictを使う場合
from collections import OrderedDict
data_ordered = json.loads(json_str, object_pairs_hook=OrderedDict)

まとめ

状況推奨
Python 3.7以降で順序が必要通常のdictを使用
順序を考慮した等価比較が必要OrderedDictを使用
move_to_end()が必要OrderedDictを使用
Python 3.6以前との互換性OrderedDictを使用
メモリ効率重視通常のdictを使用

Python 3.7以降では、通常のdictが挿入順序を保持することが公式に保証されているため、ほとんどの場合はdictで十分です。特別な機能が必要な場合のみOrderedDictを使用しましょう。

参考文献

円