forked from SigmaQuan/Better-Python-59-Ways
-
Notifications
You must be signed in to change notification settings - Fork 0
/
item_28_inherit_from_collections_abc.py
223 lines (159 loc) · 5.97 KB
/
item_28_inherit_from_collections_abc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# Item 28: Inherit from collections.abc for custom container types
# ToDo: need to debug.
# Much of programming in Python is defining classes that contain data and
# describing how such objects relate to each other. Every Python class is a
# container of some kind, encapsulating attributes and functionality together.
# Python also provides built-in container types for managing data: lists,
# tuples, sets, and dictionaries.
# When you'r designing classes for simple use cases like sequence, it's
# natural that you'd want to subclass Python built-in list type directly.
# For example, say you want to create your own custom list type that has
# additional methods for counting the frequency of its members.
class FrequencyList(list):
def __init__(self, members):
super().__init__(members)
def frequency(self):
counts = {}
for item in self:
counts.setdefault(item, 0)
counts[item] += 1
return counts
# By subclassing list, you get all of list's standard functionality and
# preserve the semantics familiar to all Python programmers. Your additional
# methods can add any custom behaviors you need.
foo = FrequencyList(['a', 'b', 'a', 'c', 'b', 'a', 'd'])
print('Length is', len(foo))
foo.pop()
print('After pop:', repr(foo))
print('Frequency:', foo.frequency())
# Length is 7
# After pop: ['a', 'b', 'a', 'c', 'b', 'a']
# Frequency: {'a': 3, 'b': 2, 'c': 1}
# Now imagine you want to provide an object that feels like a list, allowing
# indexing, but isn't a list subclass. For example, say you want to provide
# sequence semantic (like list or tuple) for a binary tree class.
class BinaryNode(object):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# How do you make this act like a sequence type? Python implements its
# container behaviors with instance methods that have special names. When you
# access a sequence item by index.
bar = [1, 2, 3]
print(bar[0])
# 1
# it will be interpreted as:
print(bar.__getitem__(0))
# 1
# To make the BinaryNode class act like a sequence, you can provide a custom
# implementation of __getitem__ that traverses the object tree depth first.
class IndexableNode(BinaryNode):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def _search(self, count, index):
found = False
return (found, count)
# ...
# returns (found, count)
def __getitem__(self, index):
found, _ = self._search(0, index)
if not found:
raise IndexError("Index out of range")
return found.value
# You can construct your binary tree as usual.
tree = IndexableNode(
10,
left=IndexableNode(
5,
left=IndexableNode(2),
right=IndexableNode(
6,
right=IndexableNode(7)
)
),
right=IndexableNode(
15, left=IndexableNode(11)
)
)
# But you can also access it like a list in addition to tree traversal.
print('LRR', tree.left.right.right.value)
# print('Index 0 = ', tree[0])
# print('Index 1 = ', tree[1])
# print('11 in the tree?', 11 in tree)
# print('17 in the tree?', 17 in tree)
# print('Tree is ', list(tree))
# The problem is that implementing __getitem__ isn't enough to provide all of
# the sequence semantics you'd expect.
# len(tree)
# TypeError: object of type 'IndexableNode' has no len()
# The len built-in function requires another special method named __len__ that
# must have an implementation for your custom sequence type.
class SequenceNode(IndexableNode):
def __len__(self):
_, count = self._search(0, None)
return count
tree = IndexableNode(
10,
left=IndexableNode(
5,
left=IndexableNode(2),
right=IndexableNode(
6,
right=IndexableNode(7)
)
),
right=IndexableNode(
15, left=IndexableNode(11)
)
)
print('Tree has %d nodes' % len(tree))
# Unfortunately, this still isn't enought. Also missing are the count and
# index methods that a Python programmer would expect to see on a sequence
# like list or tuple. Defining your own container types is much harder than
# it looks.
# To avoid this difficulty throughout the Python universe, the built-in
# collections.abc mudule defines a set of abstract base classes that provide
# all of the typical methods for each container type. When you subclass from
# these abstract base classes and forget to implement required methods, the
# module will tell you something is wrong.
# from collections.abc import Sequence
from collections import Sequence
class BadType(Sequence):
pass
foo = BadType()
# TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__
# When you do implement all of the methods required by an abstract base class,
# as I did above with SequenceNode, it will provide all of the additional
# methods like index and count for free.
class BetterNode(SequenceNode, Sequence):
pass
tree = IndexableNode(
10,
left=IndexableNode(
5,
left=IndexableNode(2),
right=IndexableNode(
6,
right=IndexableNode(7)
)
),
right=IndexableNode(
15, left=IndexableNode(11)
)
)
print('Index of 7 is', tree.index(7))
print('Count of 10 is', tree.count(10))
# The benefit of using these abstract base class is even greater for more
# complex types like Set and MutableMapping, which have a large number of
# special methods that need to be implemented to match Python conventions.
# Things to remember
# 1. Inherit directly from Python's container types (like list or dict) for
# simple use cases.
# 2. Beware of the large number of methods required to implement custom
# container types correctly.
# 3. Have your custom container types inherit from the interface defined in
# collections.abc to ensure that your classes match required interfaces
# and behaviors.