0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Python] UserDict / UserList - カスタムコレクションの作成

0
Last updated at Posted at 2026-01-12

はじめに

UserDictUserListUserStringは、辞書・リスト・文字列を継承してカスタマイズするためのラッパークラスです。直接dictlistを継承するより安全にカスタマイズできます。

01-intro.png

なぜ UserDict / UserList を使うのか

02-why-user-classes.png

dict を直接継承する問題

# dict を直接継承
class MyDict(dict):
    def __setitem__(self, key, value):
        print(f"Setting {key} = {value}")
        super().__setitem__(key, value)

d = MyDict()
d['a'] = 1  # Setting a = 1 ← 動作する

# しかし update() は __setitem__ を呼ばない!
d.update({'b': 2})  # 何も表示されない
print(d)  # {'a': 1, 'b': 2}

注意: dict/list を直接継承すると、一部メソッド(update() など)が CPython の C 実装を直接呼び出すため、オーバーライドが効かない場合があります。これは CPython の内部実装に依存する動作であり、すべてのメソッドでこの問題が起きるわけではありませんが、予測しにくいため UserDict/UserList を使う方が安全です。

UserDict なら安全

from collections import UserDict

class MyDict(UserDict):
    def __setitem__(self, key, value):
        print(f"Setting {key} = {value}")
        super().__setitem__(key, value)

d = MyDict()
d['a'] = 1       # Setting a = 1
d.update({'b': 2})  # Setting b = 2 ← 正しく動作!

UserDict の基本

03-userdict-basics.png

構造

from collections import UserDict

class MyDict(UserDict):
    # self.data で内部の辞書にアクセス
    pass

d = MyDict({'a': 1})
print(d.data)  # {'a': 1}

基本的なカスタマイズ

from collections import UserDict

class CaseInsensitiveDict(UserDict):
    """キーの大文字小文字を区別しない辞書"""

    def __setitem__(self, key, value):
        super().__setitem__(key.lower(), value)

    def __getitem__(self, key):
        return super().__getitem__(key.lower())

    def __contains__(self, key):
        return super().__contains__(key.lower())

d = CaseInsensitiveDict()
d['Name'] = 'Tanaka'
print(d['name'])   # Tanaka
print(d['NAME'])   # Tanaka
print('name' in d) # True
print(d.data)      # {'name': 'Tanaka'}(キーは小文字で保存される)

補足: この実装ではキーはすべて小文字に変換して保存されるため、元のキーの表記('Name' など)は保持されません。元の表記を保持したい場合は、別途元キーを記録する実装が必要です。

UserDict の実践例

04-userdict-practice.png

バリデーション付き辞書

from collections import UserDict

class TypedDict(UserDict):
    """型をバリデーションする辞書"""

    def __init__(self, key_type, value_type, *args, **kwargs):
        self.key_type = key_type
        self.value_type = value_type
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        if not isinstance(key, self.key_type):
            raise TypeError(f"Key must be {self.key_type.__name__}")
        if not isinstance(value, self.value_type):
            raise TypeError(f"Value must be {self.value_type.__name__}")
        super().__setitem__(key, value)

# 使用例
d = TypedDict(str, int)
d['age'] = 25       # OK
# d['age'] = 'twenty' # TypeError: Value must be int
# d[123] = 456        # TypeError: Key must be str

履歴付き辞書

from collections import UserDict
from datetime import datetime

class HistoryDict(UserDict):
    """変更履歴を記録する辞書"""

    def __init__(self, *args, **kwargs):
        self.history = []
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        old_value = self.data.get(key)
        self.history.append({
            'timestamp': datetime.now().isoformat(),
            'action': 'update' if key in self.data else 'create',
            'key': key,
            'old_value': old_value,
            'new_value': value
        })
        super().__setitem__(key, value)

    def __delitem__(self, key):
        self.history.append({
            'timestamp': datetime.now().isoformat(),
            'action': 'delete',
            'key': key,
            'old_value': self.data.get(key),
            'new_value': None
        })
        super().__delitem__(key)

# 使用例
d = HistoryDict()
d['name'] = 'Tanaka'
d['name'] = 'Yamada'
del d['name']

for record in d.history:
    print(f"{record['action']}: {record['key']} = {record['old_value']} -> {record['new_value']}")

デフォルト値付き辞書(カスタム)

from collections import UserDict

class DefaultDict(UserDict):
    """アクセス時にデフォルト値を返す(存在しないキーは作らない)"""

    def __init__(self, default, *args, **kwargs):
        self.default = default
        super().__init__(*args, **kwargs)

    def __getitem__(self, key):
        if key in self.data:
            return self.data[key]
        return self.default

# defaultdictとの違い: キーを自動作成しない
d = DefaultDict(0)
print(d['missing'])  # 0
print('missing' in d)  # False

UserList の基本

05-userlist-basics.png

構造

from collections import UserList

class MyList(UserList):
    # self.data で内部のリストにアクセス
    pass

lst = MyList([1, 2, 3])
print(lst.data)  # [1, 2, 3]

基本的なカスタマイズ

from collections import UserList

class PositiveList(UserList):
    """正の数のみを許可するリスト"""

    def _validate(self, value):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError("Only positive numbers allowed")

    def append(self, value):
        self._validate(value)
        super().append(value)

    def insert(self, i, value):
        self._validate(value)
        super().insert(i, value)

    def __setitem__(self, i, value):
        self._validate(value)
        super().__setitem__(i, value)

# 使用例
lst = PositiveList()
lst.append(5)
lst.append(10)
# lst.append(-3)  # ValueError
print(lst)  # [5, 10]

補足: この例では append()insert()__setitem__() など代表的なメソッドのみをオーバーライドしています。完全な実装では +=__iadd__)やスライス代入などもカバーする必要がありますが、記事では主要なパターンを示すことを目的としています。

UserList の実践例

06-userlist-practice.png

ユニークリスト

from collections import UserList

class UniqueList(UserList):
    """重複を許さないリスト"""

    def __init__(self, iterable=None):
        super().__init__()
        if iterable:
            for item in iterable:
                self.append(item)

    def append(self, item):
        if item not in self.data:
            super().append(item)

    def insert(self, i, item):
        if item not in self.data:
            super().insert(i, item)

    def extend(self, other):
        for item in other:
            self.append(item)

# 使用例
lst = UniqueList([1, 2, 2, 3, 3, 3])
print(lst)  # [1, 2, 3]

lst.append(2)
print(lst)  # [1, 2, 3](変化なし)

上限付きリスト

from collections import UserList

class BoundedList(UserList):
    """最大長を持つリスト"""

    def __init__(self, maxlen, iterable=None):
        self.maxlen = maxlen
        super().__init__()
        if iterable:
            for item in iterable:
                self.append(item)

    def _check_size(self):
        while len(self.data) > self.maxlen:
            self.data.pop(0)

    def append(self, item):
        super().append(item)
        self._check_size()

    def insert(self, i, item):
        super().insert(i, item)
        self._check_size()

    def extend(self, other):
        super().extend(other)
        self._check_size()

# 使用例
lst = BoundedList(3)
lst.extend([1, 2, 3, 4, 5])
print(lst)  # [3, 4, 5]

ソート済みリスト

from collections import UserList
import bisect

class SortedList(UserList):
    """常にソートされた状態を維持するリスト"""

    def __init__(self, iterable=None):
        super().__init__()
        if iterable:
            for item in iterable:
                self.add(item)

    def add(self, item):
        """ソート順を維持して追加"""
        bisect.insort(self.data, item)

    def append(self, item):
        self.add(item)

    def insert(self, i, item):
        self.add(item)

    def extend(self, other):
        for item in other:
            self.add(item)

# 使用例
lst = SortedList([3, 1, 4, 1, 5])
print(lst)  # [1, 1, 3, 4, 5]

lst.add(2)
print(lst)  # [1, 1, 2, 3, 4, 5]

UserString の基本

07-userstring_qiita.png

from collections import UserString

class ReversibleString(UserString):
    """反転メソッドを持つ文字列"""

    def reversed(self):
        return ReversibleString(self.data[::-1])

s = ReversibleString("hello")
print(s.reversed())  # olleh
print(s.upper())     # HELLO(通常のメソッドも使える)

まとめ

08-summary.png

クラス 内部データ 用途
UserDict self.data (dict) カスタム辞書
UserList self.data (list) カスタムリスト
UserString self.data (str) カスタム文字列

使い分け

  • UserDict/UserList/UserString を使う場合:

    • メソッドをオーバーライドしたい
    • 安全なカスタマイズが必要
    • 既存の操作を拡張したい
  • 直接継承を使う場合:

    • パフォーマンスが重要
    • 内部実装を理解している

動画解説(自分用)

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?