Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-36057 Update docs and tests for ordering in collections.Counter [no behavior change] #11962

Merged
merged 3 commits into from
Feb 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Doc/library/collections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,20 @@ For example::

.. versionadded:: 3.1

.. versionchanged:: 3.7 As a :class:`dict` subclass, :class:`Counter`
Inherited the capability to remember insertion order. Math operations
on *Counter* objects also preserve order. Results are ordered
according to when an element is first encountered in the left operand
and then by the order encountered in the right operand.

Counter objects support three methods beyond those available for all
dictionaries:

.. method:: elements()

Return an iterator over elements repeating each as many times as its
count. Elements are returned in arbitrary order. If an element's count
is less than one, :meth:`elements` will ignore it.
count. Elements are returned in the order first encountered. If an
element's count is less than one, :meth:`elements` will ignore it.

>>> c = Counter(a=4, b=2, c=0, d=-2)
>>> sorted(c.elements())
Expand All @@ -272,10 +277,10 @@ For example::
Return a list of the *n* most common elements and their counts from the
most common to the least. If *n* is omitted or ``None``,
:meth:`most_common` returns *all* elements in the counter.
Elements with equal counts are ordered arbitrarily:
Elements with equal counts are ordered in the order first encountered:

>>> Counter('abracadabra').most_common(3) # doctest: +SKIP
[('a', 5), ('r', 2), ('b', 2)]
>>> Counter('abracadabra').most_common(3)
[('a', 5), ('b', 2), ('r', 2)]

.. method:: subtract([iterable-or-mapping])

Expand Down
4 changes: 2 additions & 2 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,8 @@ def most_common(self, n=None):
'''List the n most common elements and their counts from the most
common to the least. If n is None, then list all element counts.

>>> Counter('abcdeabcdabcaba').most_common(3)
[('a', 5), ('b', 4), ('c', 3)]
>>> Counter('abracadabra').most_common(3)
[('a', 5), ('b', 2), ('r', 2)]

'''
# Emulate Bag.sortedByCount from Smalltalk
Expand Down
57 changes: 57 additions & 0 deletions Lib/test/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,63 @@ def test_init(self):
self.assertRaises(TypeError, Counter, (), ())
self.assertRaises(TypeError, Counter.__init__)

def test_order_preservation(self):
# Input order dictates items() order
self.assertEqual(list(Counter('abracadabra').items()),
[('a', 5), ('b', 2), ('r', 2), ('c', 1), ('d', 1)])
# letters with same count: ^----------^ ^---------^

# Verify retention of order even when all counts are equal
self.assertEqual(list(Counter('xyzpdqqdpzyx').items()),
[('x', 2), ('y', 2), ('z', 2), ('p', 2), ('d', 2), ('q', 2)])

# Input order dictates elements() order
self.assertEqual(list(Counter('abracadabra simsalabim').elements()),
['a', 'a', 'a', 'a', 'a', 'a', 'a', 'b', 'b', 'b','r',
'r', 'c', 'd', ' ', 's', 's', 'i', 'i', 'm', 'm', 'l'])

# Math operations order first by the order encountered in the left
# operand and then by the order encounted in the right operand.
ps = 'aaabbcdddeefggghhijjjkkl'
qs = 'abbcccdeefffhkkllllmmnno'
order = {letter: i for i, letter in enumerate(dict.fromkeys(ps + qs))}
def correctly_ordered(seq):
'Return true if the letters occur in the expected order'
positions = [order[letter] for letter in seq]
return positions == sorted(positions)

p, q = Counter(ps), Counter(qs)
self.assertTrue(correctly_ordered(+p))
self.assertTrue(correctly_ordered(-p))
self.assertTrue(correctly_ordered(p + q))
self.assertTrue(correctly_ordered(p - q))
self.assertTrue(correctly_ordered(p | q))
self.assertTrue(correctly_ordered(p & q))

p, q = Counter(ps), Counter(qs)
p += q
self.assertTrue(correctly_ordered(p))

p, q = Counter(ps), Counter(qs)
p -= q
self.assertTrue(correctly_ordered(p))

p, q = Counter(ps), Counter(qs)
p |= q
self.assertTrue(correctly_ordered(p))

p, q = Counter(ps), Counter(qs)
p &= q
self.assertTrue(correctly_ordered(p))

p, q = Counter(ps), Counter(qs)
p.update(q)
self.assertTrue(correctly_ordered(p))

p, q = Counter(ps), Counter(qs)
p.subtract(q)
self.assertTrue(correctly_ordered(p))

def test_update(self):
c = Counter()
c.update(self=42)
Expand Down