【 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 ) から派生させたクラスを作っちゃえば簡単です。
通常のリストに、要素の出現頻度をカウントする機能を追加してみました。
list クラスを継承しましたから、通常のリストと同じ文法で同じ機能を使えるのはもちろん、独自の機能も追加できています。
さて、list クラスのサブクラスではないけれど、リストと同じような操作ができるクラスを作成したいとしましょう。
例えば、各ノードの値をインデックス番号でも参照可能なバイナリツリー ( binary tree ) クラスであるとか。
さてどうしましょう?
Python ではインデックス番号でシーケンス要素にアクセスする機能がどのように実現されているんでしょうか?
実はこの例のようにインデックス番号で要素を参照しようした場合、Python は次のような文に変換して実行します。
ですから BinaryNode クラスをシーケンスのように扱いたいのであれば、独自の __getitem__() 特殊関数を実装すればよい、ということなんです。
(この __getitem__ の読み方ですが、"double underscore getitem" を略して 「ダンダー ( dunder ) getitem」という人もいます)
(そして、ダブルアンダースコアが前に付くメソッド全般を「ダンダーメソッド」と呼んでいたりします)
最も左側に位置するノードから順にインデックス番号を割り振る場合の実装は次のようになります。
ここで定義したバイナリツリーを図で表すとこんな感じです。
実行してみましょう!
この実装での不満点は、list のような組み込みコンテナタイプと同様の十分な機能が提供できていない、ということです。
例えば...
シーケンスの長さ、というか、バイナリツリーを構成しているノードの数、ですけれども、
__len__ 特殊関数を実装していませんから、当然エラーになっちゃいます。実装しましょう。
これでもまだまだ組み込みコンテナタイプが提供している機能には全然及びません、count() も index() も...
こうやって考えると、独自のコンテナタイプを一から実装するのは思った以上に大変なことが分かると思います。
でも Python はちゃんと救いの手を差し伸べてくれているんです。
collections.abc モジュールがそれで、その中には代表的な組み込みコンテナタイプの主要なメソッドを簡単に供給可能にしてくれる、複数の抽象ベースクラス ( abstract base classes ) が定義されています。
これらのベースクラスが定義している抽象メソッドさえ実装すれば、残りの主要メソッドは自動で対応してくれる、という優れものです。
今回でいえば、Sequence クラスを継承し、求められている __getitem__ と __len__ 関数さえ実装すれば、index() も count() も「ただ」でご利用いただけます。
これら抽象ベースクラスの便利さは、Set や MutableMapping といったより複雑なコンテナタイプを独自に実装したいときにより際立つはずです。
まとめ:
通常のリストに、要素の出現頻度をカウントする機能を追加してみました。
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}
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
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
さてどうしましょう?
Python ではインデックス番号でシーケンス要素にアクセスする機能がどのように実現されているんでしょうか?
foo = [1, 2, 3]
foo[0]
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)
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)
ここで定義したバイナリツリーを図で表すとこんな感じです。
実行してみましょう!
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]
# 左端のノードの値: 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
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
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 ... -