Skip to content

Commit c76288f

Browse files
author
raymond.hettinger
committed
Promote combinations_with_replacement() from a recipe to a regular itertool.
git-svn-id: http://svn.python.org/projects/python/trunk@69001 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent 67cd023 commit c76288f

5 files changed

Lines changed: 379 additions & 59 deletions

File tree

Doc/library/collections.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,7 @@ counts less than one::
291291
Section 4.6.3, Exercise 19*\.
292292

293293
* To enumerate all distinct multisets of a given size over a given set of
294-
elements, see :func:`combinations_with_replacement` in the
295-
:ref:`itertools-recipes` for itertools::
294+
elements, see :func:`itertools.combinations_with_replacement`.
296295

297296
map(Counter, combinations_with_replacement('ABC', 2)) --> AA AB AC BB BC CC
298297

Doc/library/itertools.rst

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,53 @@ loops that truncate the stream.
139139

140140
.. versionadded:: 2.6
141141

142+
.. function:: combinations_with_replacement(iterable, r)
143+
144+
Return *r* length subsequences of elements from the input *iterable*
145+
allowing individual elements to be repeated more than once.
146+
147+
Combinations are emitted in lexicographic sort order. So, if the
148+
input *iterable* is sorted, the combination tuples will be produced
149+
in sorted order.
150+
151+
Elements are treated as unique based on their position, not on their
152+
value. So if the input elements are unique, the generated combinations
153+
will also be unique.
154+
155+
Equivalent to::
156+
157+
def combinations_with_replacement(iterable, r):
158+
# combinations_with_replacement('ABC', 2) --> AA AB AC BB BC CC
159+
pool = tuple(iterable)
160+
n = len(pool)
161+
if not n and r:
162+
return
163+
indices = [0] * r
164+
yield tuple(pool[i] for i in indices)
165+
while 1:
166+
for i in reversed(range(r)):
167+
if indices[i] != n - 1:
168+
break
169+
else:
170+
return
171+
indices[i:] = [indices[i] + 1] * (r - i)
172+
yield tuple(pool[i] for i in indices)
173+
174+
The code for :func:`combinations_with_replacement` can be also expressed as
175+
a subsequence of :func:`product` after filtering entries where the elements
176+
are not in sorted order (according to their position in the input pool)::
177+
178+
def combinations_with_replacement(iterable, r):
179+
pool = tuple(iterable)
180+
n = len(pool)
181+
for indices in product(range(n), repeat=r):
182+
if sorted(indices) == list(indices):
183+
yield tuple(pool[i] for i in indices)
184+
185+
The number of items returned is ``(n+r-1)! / r! / (n-1)!`` when ``n > 0``.
186+
187+
.. versionadded:: 2.7
188+
142189
.. function:: compress(data, selectors)
143190

144191
Make an iterator that filters elements from *data* returning only those that
@@ -691,22 +738,6 @@ which incur interpreter overhead.
691738
s = list(iterable)
692739
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
693740

694-
def combinations_with_replacement(iterable, r):
695-
"combinations_with_replacement('ABC', 2) --> AA AB AC BB BC CC"
696-
# number items returned: (n+r-1)! / r! / (n-1)!
697-
pool = tuple(iterable)
698-
n = len(pool)
699-
indices = [0] * r
700-
yield tuple(pool[i] for i in indices)
701-
while 1:
702-
for i in reversed(range(r)):
703-
if indices[i] != n - 1:
704-
break
705-
else:
706-
return
707-
indices[i:] = [indices[i] + 1] * (r - i)
708-
yield tuple(pool[i] for i in indices)
709-
710741
def unique_everseen(iterable, key=None):
711742
"List unique elements, preserving order. Remember all elements ever seen."
712743
# unique_everseen('AAAABBBCCDAABBB') --> A B C D

Lib/test/test_itertools.py

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,76 @@ def combinations2(iterable, r):
127127
self.assertEqual(len(set(map(id, combinations('abcde', 3)))), 1)
128128
self.assertNotEqual(len(set(map(id, list(combinations('abcde', 3))))), 1)
129129

130+
def test_combinations_with_replacement(self):
131+
cwr = combinations_with_replacement
132+
self.assertRaises(TypeError, cwr, 'abc') # missing r argument
133+
self.assertRaises(TypeError, cwr, 'abc', 2, 1) # too many arguments
134+
self.assertRaises(TypeError, cwr, None) # pool is not iterable
135+
self.assertRaises(ValueError, cwr, 'abc', -2) # r is negative
136+
self.assertEqual(list(cwr('ABC', 2)),
137+
[('A','A'), ('A','B'), ('A','C'), ('B','B'), ('B','C'), ('C','C')])
138+
139+
def cwr1(iterable, r):
140+
'Pure python version shown in the docs'
141+
# number items returned: (n+r-1)! / r! / (n-1)! when n>0
142+
pool = tuple(iterable)
143+
n = len(pool)
144+
if not n and r:
145+
return
146+
indices = [0] * r
147+
yield tuple(pool[i] for i in indices)
148+
while 1:
149+
for i in reversed(range(r)):
150+
if indices[i] != n - 1:
151+
break
152+
else:
153+
return
154+
indices[i:] = [indices[i] + 1] * (r - i)
155+
yield tuple(pool[i] for i in indices)
156+
157+
def cwr2(iterable, r):
158+
'Pure python version shown in the docs'
159+
pool = tuple(iterable)
160+
n = len(pool)
161+
for indices in product(range(n), repeat=r):
162+
if sorted(indices) == list(indices):
163+
yield tuple(pool[i] for i in indices)
164+
165+
def numcombs(n, r):
166+
if not n:
167+
return 0 if r else 1
168+
return fact(n+r-1) / fact(r)/ fact(n-1)
169+
170+
for n in range(7):
171+
values = [5*x-12 for x in range(n)]
172+
for r in range(n+2):
173+
result = list(cwr(values, r))
174+
175+
self.assertEqual(len(result), numcombs(n, r)) # right number of combs
176+
self.assertEqual(len(result), len(set(result))) # no repeats
177+
self.assertEqual(result, sorted(result)) # lexicographic order
178+
179+
regular_combs = list(combinations(values, r)) # compare to combs without replacement
180+
if n == 0 or r <= 1:
181+
self.assertEquals(result, regular_combs) # cases that should be identical
182+
else:
183+
self.assert_(set(result) >= set(regular_combs)) # rest should be supersets of regular combs
184+
185+
for c in result:
186+
self.assertEqual(len(c), r) # r-length combinations
187+
noruns = [k for k,v in groupby(c)] # combo without consecutive repeats
188+
self.assertEqual(len(noruns), len(set(noruns))) # no repeats other than consecutive
189+
self.assertEqual(list(c), sorted(c)) # keep original ordering
190+
self.assert_(all(e in values for e in c)) # elements taken from input iterable
191+
self.assertEqual(noruns,
192+
[e for e in values if e in c]) # comb is a subsequence of the input iterable
193+
self.assertEqual(result, list(cwr1(values, r))) # matches first pure python version
194+
self.assertEqual(result, list(cwr2(values, r))) # matches second pure python version
195+
196+
# Test implementation detail: tuple re-use
197+
self.assertEqual(len(set(map(id, cwr('abcde', 3)))), 1)
198+
self.assertNotEqual(len(set(map(id, list(cwr('abcde', 3))))), 1)
199+
130200
def test_permutations(self):
131201
self.assertRaises(TypeError, permutations) # too few arguments
132202
self.assertRaises(TypeError, permutations, 'abc', 2, 1) # too many arguments
@@ -716,6 +786,10 @@ def test_combinations(self):
716786
self.assertEqual(list(combinations(range(4), 3)),
717787
[(0,1,2), (0,1,3), (0,2,3), (1,2,3)])
718788

789+
def test_combinations_with_replacement(self):
790+
self.assertEqual(list(combinations_with_replacement('ABC', 2)),
791+
[('A','A'), ('A','B'), ('A','C'), ('B','B'), ('B','C'), ('C','C')])
792+
719793
def test_compress(self):
720794
self.assertEqual(list(compress('ABCDEF', [1,0,1,0,1,1])), list('ACEF'))
721795

@@ -799,6 +873,10 @@ def test_combinations(self):
799873
a = []
800874
self.makecycle(combinations([1,2,a,3], 3), a)
801875

876+
def test_combinations_with_replacement(self):
877+
a = []
878+
self.makecycle(combinations_with_replacement([1,2,a,3], 3), a)
879+
802880
def test_compress(self):
803881
a = []
804882
self.makecycle(compress('ABCDEF', [1,0,1,0,1,0]), a)
@@ -1291,21 +1369,6 @@ def __init__(self, newarg=None, *args):
12911369
... s = list(iterable)
12921370
... return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
12931371
1294-
>>> def combinations_with_replacement(iterable, r):
1295-
... "combinations_with_replacement('ABC', 3) --> AA AB AC BB BC CC"
1296-
... pool = tuple(iterable)
1297-
... n = len(pool)
1298-
... indices = [0] * r
1299-
... yield tuple(pool[i] for i in indices)
1300-
... while 1:
1301-
... for i in reversed(range(r)):
1302-
... if indices[i] != n - 1:
1303-
... break
1304-
... else:
1305-
... return
1306-
... indices[i:] = [indices[i] + 1] * (r - i)
1307-
... yield tuple(pool[i] for i in indices)
1308-
13091372
>>> def unique_everseen(iterable, key=None):
13101373
... "List unique elements, preserving order. Remember all elements ever seen."
13111374
... # unique_everseen('AAAABBBCCDAABBB') --> A B C D
@@ -1386,29 +1449,6 @@ def __init__(self, newarg=None, *args):
13861449
>>> list(powerset([1,2,3]))
13871450
[(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
13881451
1389-
>>> list(combinations_with_replacement('abc', 2))
1390-
[('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'), ('c', 'c')]
1391-
1392-
>>> list(combinations_with_replacement('01', 3))
1393-
[('0', '0', '0'), ('0', '0', '1'), ('0', '1', '1'), ('1', '1', '1')]
1394-
1395-
>>> def combinations_with_replacement2(iterable, r):
1396-
... 'Alternate version that filters from product()'
1397-
... pool = tuple(iterable)
1398-
... n = len(pool)
1399-
... for indices in product(range(n), repeat=r):
1400-
... if sorted(indices) == list(indices):
1401-
... yield tuple(pool[i] for i in indices)
1402-
1403-
>>> list(combinations_with_replacement('abc', 2)) == list(combinations_with_replacement2('abc', 2))
1404-
True
1405-
1406-
>>> list(combinations_with_replacement('01', 3)) == list(combinations_with_replacement2('01', 3))
1407-
True
1408-
1409-
>>> list(combinations_with_replacement('2310', 6)) == list(combinations_with_replacement2('2310', 6))
1410-
True
1411-
14121452
>>> list(unique_everseen('AAAABBBCCDAABBB'))
14131453
['A', 'B', 'C', 'D']
14141454

Misc/NEWS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ Library
153153

154154
- Issue #4863: distutils.mwerkscompiler has been removed.
155155

156-
- Added a new function: itertools.compress().
156+
- Added a new itertools functions: combinations_with_replacement()
157+
and compress().
157158

158159
- Fix and properly document the multiprocessing module's logging
159160
support, expose the internal levels and provide proper usage

0 commit comments

Comments
 (0)