Raymond Hettinger | 7892b1c | 2004-04-12 18:10:01 +0000 | [diff] [blame] | 1 | """ Test Iterator Length Transparency |
| 2 | |
| 3 | Some functions or methods which accept general iterable arguments have |
| 4 | optional, more efficient code paths if they know how many items to expect. |
| 5 | For instance, map(func, iterable), will pre-allocate the exact amount of |
| 6 | space required whenever the iterable can report its length. |
| 7 | |
| 8 | The desired invariant is: len(it)==len(list(it)). |
| 9 | |
| 10 | A complication is that an iterable and iterator can be the same object. To |
| 11 | maintain the invariant, an iterator needs to dynamically update its length. |
| 12 | For instance, an iterable such as xrange(10) always reports its length as ten, |
| 13 | but it=iter(xrange(10)) starts at ten, and then goes to nine after it.next(). |
| 14 | Having this capability means that map() can ignore the distinction between |
| 15 | map(func, iterable) and map(func, iter(iterable)). |
| 16 | |
| 17 | When the iterable is immutable, the implementation can straight-forwardly |
| 18 | report the original length minus the cumulative number of calls to next(). |
| 19 | This is the case for tuples, xrange objects, and itertools.repeat(). |
| 20 | |
| 21 | Some containers become temporarily immutable during iteration. This includes |
| 22 | dicts, sets, and collections.deque. Their implementation is equally simple |
| 23 | though they need to permantently set their length to zero whenever there is |
| 24 | an attempt to iterate after a length mutation. |
| 25 | |
| 26 | The situation slightly more involved whenever an object allows length mutation |
| 27 | during iteration. Lists and sequence iterators are dynanamically updatable. |
| 28 | So, if a list is extended during iteration, the iterator will continue through |
| 29 | the new items. If it shrinks to a point before the most recent iteration, |
| 30 | then no further items are available and the length is reported at zero. |
| 31 | |
| 32 | Reversed objects can also be wrapped around mutable objects; however, any |
| 33 | appends after the current position are ignored. Any other approach leads |
| 34 | to confusion and possibly returning the same item more than once. |
| 35 | |
| 36 | The iterators not listed above, such as enumerate and the other itertools, |
| 37 | are not length transparent because they have no way to distinguish between |
| 38 | iterables that report static length and iterators whose length changes with |
| 39 | each call (i.e. the difference between enumerate('abc') and |
| 40 | enumerate(iter('abc')). |
| 41 | |
| 42 | """ |
| 43 | |
| 44 | import unittest |
| 45 | from test import test_support |
Raymond Hettinger | 6b27cda | 2005-09-24 21:23:05 +0000 | [diff] [blame] | 46 | from itertools import repeat |
Raymond Hettinger | 7892b1c | 2004-04-12 18:10:01 +0000 | [diff] [blame] | 47 | from collections import deque |
| 48 | from UserList import UserList |
Raymond Hettinger | 6b27cda | 2005-09-24 21:23:05 +0000 | [diff] [blame] | 49 | from __builtin__ import len as _len |
Raymond Hettinger | 7892b1c | 2004-04-12 18:10:01 +0000 | [diff] [blame] | 50 | |
| 51 | n = 10 |
| 52 | |
Raymond Hettinger | 6b27cda | 2005-09-24 21:23:05 +0000 | [diff] [blame] | 53 | def len(obj): |
| 54 | try: |
| 55 | return _len(obj) |
| 56 | except TypeError: |
| 57 | try: |
| 58 | return obj._length_cue() |
| 59 | except AttributeError: |
| 60 | raise TypeError |
| 61 | |
Raymond Hettinger | 7892b1c | 2004-04-12 18:10:01 +0000 | [diff] [blame] | 62 | class TestInvariantWithoutMutations(unittest.TestCase): |
| 63 | |
| 64 | def test_invariant(self): |
Tim Peters | 27f8836 | 2004-07-08 04:22:35 +0000 | [diff] [blame] | 65 | it = self.it |
| 66 | for i in reversed(xrange(1, n+1)): |
| 67 | self.assertEqual(len(it), i) |
| 68 | it.next() |
| 69 | self.assertEqual(len(it), 0) |
| 70 | self.assertRaises(StopIteration, it.next) |
| 71 | self.assertEqual(len(it), 0) |
Raymond Hettinger | 7892b1c | 2004-04-12 18:10:01 +0000 | [diff] [blame] | 72 | |
| 73 | class TestTemporarilyImmutable(TestInvariantWithoutMutations): |
| 74 | |
| 75 | def test_immutable_during_iteration(self): |
| 76 | # objects such as deques, sets, and dictionaries enforce |
| 77 | # length immutability during iteration |
| 78 | |
| 79 | it = self.it |
| 80 | self.assertEqual(len(it), n) |
| 81 | it.next() |
| 82 | self.assertEqual(len(it), n-1) |
| 83 | self.mutate() |
| 84 | self.assertRaises(RuntimeError, it.next) |
| 85 | self.assertEqual(len(it), 0) |
| 86 | |
| 87 | ## ------- Concrete Type Tests ------- |
| 88 | |
| 89 | class TestRepeat(TestInvariantWithoutMutations): |
| 90 | |
| 91 | def setUp(self): |
| 92 | self.it = repeat(None, n) |
| 93 | |
| 94 | def test_no_len_for_infinite_repeat(self): |
| 95 | # The repeat() object can also be infinite |
| 96 | self.assertRaises(TypeError, len, repeat(None)) |
| 97 | |
| 98 | class TestXrange(TestInvariantWithoutMutations): |
| 99 | |
| 100 | def setUp(self): |
| 101 | self.it = iter(xrange(n)) |
| 102 | |
| 103 | class TestXrangeCustomReversed(TestInvariantWithoutMutations): |
| 104 | |
| 105 | def setUp(self): |
| 106 | self.it = reversed(xrange(n)) |
| 107 | |
| 108 | class TestTuple(TestInvariantWithoutMutations): |
| 109 | |
| 110 | def setUp(self): |
| 111 | self.it = iter(tuple(xrange(n))) |
| 112 | |
| 113 | ## ------- Types that should not be mutated during iteration ------- |
| 114 | |
| 115 | class TestDeque(TestTemporarilyImmutable): |
| 116 | |
| 117 | def setUp(self): |
| 118 | d = deque(xrange(n)) |
| 119 | self.it = iter(d) |
| 120 | self.mutate = d.pop |
| 121 | |
| 122 | class TestDequeReversed(TestTemporarilyImmutable): |
| 123 | |
| 124 | def setUp(self): |
| 125 | d = deque(xrange(n)) |
| 126 | self.it = reversed(d) |
| 127 | self.mutate = d.pop |
| 128 | |
| 129 | class TestDictKeys(TestTemporarilyImmutable): |
| 130 | |
| 131 | def setUp(self): |
| 132 | d = dict.fromkeys(xrange(n)) |
| 133 | self.it = iter(d) |
| 134 | self.mutate = d.popitem |
| 135 | |
| 136 | class TestDictItems(TestTemporarilyImmutable): |
| 137 | |
| 138 | def setUp(self): |
| 139 | d = dict.fromkeys(xrange(n)) |
| 140 | self.it = d.iteritems() |
| 141 | self.mutate = d.popitem |
| 142 | |
| 143 | class TestDictValues(TestTemporarilyImmutable): |
| 144 | |
| 145 | def setUp(self): |
| 146 | d = dict.fromkeys(xrange(n)) |
| 147 | self.it = d.itervalues() |
| 148 | self.mutate = d.popitem |
| 149 | |
| 150 | class TestSet(TestTemporarilyImmutable): |
| 151 | |
| 152 | def setUp(self): |
| 153 | d = set(xrange(n)) |
| 154 | self.it = iter(d) |
| 155 | self.mutate = d.pop |
| 156 | |
| 157 | ## ------- Types that can mutate during iteration ------- |
| 158 | |
| 159 | class TestList(TestInvariantWithoutMutations): |
| 160 | |
| 161 | def setUp(self): |
| 162 | self.it = iter(range(n)) |
| 163 | |
| 164 | def test_mutation(self): |
| 165 | d = range(n) |
| 166 | it = iter(d) |
| 167 | it.next() |
| 168 | it.next() |
| 169 | self.assertEqual(len(it), n-2) |
| 170 | d.append(n) |
| 171 | self.assertEqual(len(it), n-1) # grow with append |
| 172 | d[1:] = [] |
| 173 | self.assertEqual(len(it), 0) |
| 174 | self.assertEqual(list(it), []) |
| 175 | d.extend(xrange(20)) |
| 176 | self.assertEqual(len(it), 0) |
| 177 | |
| 178 | class TestListReversed(TestInvariantWithoutMutations): |
| 179 | |
| 180 | def setUp(self): |
| 181 | self.it = reversed(range(n)) |
| 182 | |
| 183 | def test_mutation(self): |
| 184 | d = range(n) |
| 185 | it = reversed(d) |
| 186 | it.next() |
| 187 | it.next() |
| 188 | self.assertEqual(len(it), n-2) |
| 189 | d.append(n) |
| 190 | self.assertEqual(len(it), n-2) # ignore append |
| 191 | d[1:] = [] |
| 192 | self.assertEqual(len(it), 0) |
| 193 | self.assertEqual(list(it), []) # confirm invariant |
| 194 | d.extend(xrange(20)) |
| 195 | self.assertEqual(len(it), 0) |
| 196 | |
| 197 | class TestSeqIter(TestInvariantWithoutMutations): |
| 198 | |
| 199 | def setUp(self): |
| 200 | self.it = iter(UserList(range(n))) |
| 201 | |
| 202 | def test_mutation(self): |
| 203 | d = UserList(range(n)) |
| 204 | it = iter(d) |
| 205 | it.next() |
| 206 | it.next() |
| 207 | self.assertEqual(len(it), n-2) |
| 208 | d.append(n) |
| 209 | self.assertEqual(len(it), n-1) # grow with append |
| 210 | d[1:] = [] |
| 211 | self.assertEqual(len(it), 0) |
| 212 | self.assertEqual(list(it), []) |
| 213 | d.extend(xrange(20)) |
| 214 | self.assertEqual(len(it), 0) |
| 215 | |
| 216 | class TestSeqIterReversed(TestInvariantWithoutMutations): |
| 217 | |
| 218 | def setUp(self): |
| 219 | self.it = reversed(UserList(range(n))) |
| 220 | |
| 221 | def test_mutation(self): |
| 222 | d = UserList(range(n)) |
| 223 | it = reversed(d) |
| 224 | it.next() |
| 225 | it.next() |
| 226 | self.assertEqual(len(it), n-2) |
| 227 | d.append(n) |
| 228 | self.assertEqual(len(it), n-2) # ignore append |
| 229 | d[1:] = [] |
| 230 | self.assertEqual(len(it), 0) |
| 231 | self.assertEqual(list(it), []) # confirm invariant |
| 232 | d.extend(xrange(20)) |
| 233 | self.assertEqual(len(it), 0) |
| 234 | |
| 235 | |
| 236 | |
| 237 | if __name__ == "__main__": |
| 238 | |
| 239 | unittests = [ |
| 240 | TestRepeat, |
| 241 | TestXrange, |
| 242 | TestXrangeCustomReversed, |
| 243 | TestTuple, |
| 244 | TestDeque, |
| 245 | TestDequeReversed, |
| 246 | TestDictKeys, |
| 247 | TestDictItems, |
| 248 | TestDictValues, |
| 249 | TestSet, |
| 250 | TestList, |
| 251 | TestListReversed, |
| 252 | TestSeqIter, |
| 253 | TestSeqIterReversed, |
| 254 | ] |
| 255 | test_support.run_unittest(*unittests) |