検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
effective_python

【 Effective Python, 2nd Edition 】独自のコンテナタイプ ( custom container types ) を定義するなら collections.abc クラスから派生させると手間無しです! 投稿一覧へ戻る

Published 2020年7月14日17:51 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

シーケンス ( sequences ) データを扱い、もしちょっとだけ独自の機能を追加したいのなら、組み込みのリストタイプ ( list ) から派生させたクラスを作っちゃえば簡単です。


通常のリストに、要素の出現頻度をカウントする機能を追加してみました。


class FrequencyList(list):
def __init__(self, members):
super().__init__(members)

def frequency(self):
counts = {}
for item in self:
counts[item] = counts.get(item, 0) + 1
return counts


boo = FrequencyList(['a', 'b', 'c', 'a', 'c', 'd', 'a', 'b'])

print(f"要素の数: {len(boo)}")
# 要素の数: 8

print(f"要素の取り出し: {boo.pop()!r}")
# 要素の取り出し: 'b'

print(f"取り出し後のリスト: {repr(boo)}")
# 取り出し後のリスト: ['a', 'b', 'c', 'a', 'c', 'd', 'a']

print(f"要素の出現頻度: {boo.frequency()}")
# 要素の出現頻度: {'a': 3, 'b': 1, 'c': 2, 'd': 1}



list クラスを継承しましたから、通常のリストと同じ文法で同じ機能を使えるのはもちろん、独自の機能も追加できています。


さて、list クラスのサブクラスではないけれど、リストと同じような操作ができるクラスを作成したいとしましょう。
例えば、各ノードの値をインデックス番号でも参照可能なバイナリツリー ( binary tree ) クラスであるとか。


class BinaryNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right



さてどうしましょう?
Python ではインデックス番号でシーケンス要素にアクセスする機能がどのように実現されているんでしょうか?


foo = [1, 2, 3]
foo[0]



実はこの例のようにインデックス番号で要素を参照しようした場合、Python は次のような文に変換して実行します。


foo.__getitem__(0)



ですから BinaryNode クラスをシーケンスのように扱いたいのであれば、独自の __getitem__() 特殊関数を実装すればよい、ということなんです。
(この __getitem__ の読み方ですが、"double underscore getitem" を略して 「ダンダー ( dunder ) getitem」という人もいます)
(そして、ダブルアンダースコアが前に付くメソッド全般を「ダンダーメソッド」と呼んでいたりします)


最も左側に位置するノードから順にインデックス番号を割り振る場合の実装は次のようになります。


class IndexableNode(BinaryNode):
def _traverse(self):
if self.left is not None:
yield from self.left._traverse()
yield self
if self.right is not None:
yield from self.right._traverse()

def __getitem__(self, index):
for i, item in enumerate(self._traverse()):
if i == index:
return item.value
raise IndexError(f"インデックス [{index}] は範囲外です")


tree = IndexableNode(10)
tree.left = IndexableNode(5)
tree.left.left = IndexableNode(3)
tree.left.left.left = IndexableNode(1)
tree.left.right = IndexableNode(7)
tree.left.right.right = IndexableNode(9)
tree.right = IndexableNode(13)



ここで定義したバイナリツリーを図で表すとこんな感じです。

binary tree graph



実行してみましょう!


print(f"左端のノードの値: {tree.left.left.left.value}")
# 左端のノードの値: 1

print(f"インデックス番号でもアクセスできます: {tree[0]}")
# インデックス番号でもアクセスできます: 1

print(f"インデックス 1 のノードの値: {tree[1]}")
# インデックス 1 のノードの値: 3

print(f"値が 7 のノードはある? {7 in tree}")
# 値が 7 のノードはある? True

print(f"値が 11 のノードはある? {11 in tree}")
# 値が 11 のノードはある? False

print(f"ツリーの構成を左から: {list(tree)}")
# ツリーの構成を左から: [1, 3, 5, 7, 9, 10, 13]



この実装での不満点は、list のような組み込みコンテナタイプと同様の十分な機能が提供できていない、ということです。
例えば...


len(tree)

# Traceback...
# TypeError: object of type 'IndexableNode' has no len()




シーケンスの長さ、というか、バイナリツリーを構成しているノードの数、ですけれども、
__len__ 特殊関数を実装していませんから、当然エラーになっちゃいます。実装しましょう。


class SequenceNode(IndexableNode):
def __len__(self):
count = 0
for _ in self._traverse():
count += 1
return count


tree = SequenceNode(10)
tree.left = SequenceNode(5)
tree.left.left = SequenceNode(3)
tree.left.left.left = SequenceNode(1)
tree.left.right = SequenceNode(7)
tree.left.right.right = SequenceNode(9)
tree.right = SequenceNode(13)

print(f"ツリーに含まれるノードの数は? {len(tree)}")
# ツリーに含まれるノードの数は? 7



これでもまだまだ組み込みコンテナタイプが提供している機能には全然及びません、count() も index() も...


こうやって考えると、独自のコンテナタイプを一から実装するのは思った以上に大変なことが分かると思います。


でも Python はちゃんと救いの手を差し伸べてくれているんです。
collections.abc モジュールがそれで、その中には代表的な組み込みコンテナタイプの主要なメソッドを簡単に供給可能にしてくれる、複数の抽象ベースクラス ( abstract base classes ) が定義されています。


これらのベースクラスが定義している抽象メソッドさえ実装すれば、残りの主要メソッドは自動で対応してくれる、という優れものです。


今回でいえば、Sequence クラスを継承し、求められている __getitem__ と __len__ 関数さえ実装すれば、index() も count() も「ただ」でご利用いただけます。


from collections.abc import Sequence


class BetterNode(SequenceNode, Sequence):
pass


tree = BetterNode(10)
tree.left = BetterNode(5)
tree.left.left = BetterNode(3)
tree.left.left.left = BetterNode(1)
tree.left.right = BetterNode(7)
tree.left.right.right = BetterNode(9)
tree.right = BetterNode(13)

print(f"値が 7 のノードのインデックス番号は? {tree.index(7)}")
# 値が 7 のノードのインデックス番号は? 3

print(f"値が 9 のノードの数は? {tree.count(9)}")
# 値が 9 のノードの数は? 1



これら抽象ベースクラスの便利さは、Set や MutableMapping といったより複雑なコンテナタイプを独自に実装したいときにより際立つはずです。


まとめ:

1: list や dict のような Python の組み込みコンテナクラスを直接継承して、ちょっとした機能を追加した独自のコンテナタイプを簡単に作成することが可能です。

2: 独自のコンテナタイプを一から自分で実装し組み込みコンテナタイプと同様の機能を提供しようとするとかなりの労力が必要です。

3: collections.abc モジュール内で定義されている抽象ベースクラスを継承し実装することで、組み込みコンテナタイプと同様のインターフェースと機能をより簡単に提供できるようになります。

この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】クラスインスタンスを関数として利用可能にする __call__ 特殊関数を含んだクラスを定義してフック ( hook ) として利用することで、既存の API の機能拡張を計ろう!
【 Effective Python, 2nd Edition 】組み込みタイプ ( built-in types ) を利用していてネストが深くなってきたらクラス ( class ) を作成する頃合いです、の巻
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!
【Python 雑談・雑学 + coding challenge】collections モジュールの Counter クラスと most_common メソッドを利用してシーケンス内の最頻出要素を取得しよう!
【 Effective Python, 2nd Edition 】サブクラス定義に付随させて必ず行いたい操作がある場合は、メタクラス ( metaclass )、または、__init_subclass__ 特殊関数を利用してド忘れを防止しよう!
【 Effective Python, 2nd Edition 】@classmethod ポリモーフィズム ( polymorphism ) を利用して、複数の派生クラスをよりジェネリック ( generic ) に活用しよう!
【 Effective Python, 2nd Edition 】__set_name__ デスクリプタ専用特殊関数 ( special method for descriptor ) を利用してデスクリプタインスタンスを割り当てたクラス変数名を取得し、コードの冗長性を排除しよう!