4
\$\begingroup\$

I found a suspicious code snipped in an existing project in which I am involved as a developer. The code sorts a dict by its keys, so that if iterated over the dict, the elements are returned in the sorted order. In order to have some stand-alone code I refactored out a function which does that job. However, as I was suspicious about the existing implementation I wrote a second stand-alone function which does the same job but a little different.

Please review my two implementations from which I will improve the better one given the suggestions and then add it to the project. The performance and number of statements within the function does not matter so much in this case. However, I need to be sure that the items in the dict are returned in the correct order when iterating over it.

1.)

def sort_dct1(dct: dict) -> dict:
    return dict(sorted(dct.items()))

Test:

print(sort_dct({3: 100, 2: 10, 44: 20, 1: 80}))  # just for testing

> {1: 80, 2: 10, 3: 100, 44: 20}

2.)

import collections

def sort_dct2(dct: dict) -> collections.OrderedDict:
    return collections.OrderedDict(sorted(dct.items()))

Test:

print(sort_dct2({3: 100, 2: 10, 44: 20, 1: 80}))

> OrderedDict([(1, 80), (2, 10), (3, 100), (44, 20)])

The second implementation also passes the isinstance test:

isinstance(sort_dct2({3: 100, 2: 10, 44: 20, 1: 80}), dict)
True
\$\endgroup\$
7
  • 1
    \$\begingroup\$ Neither implementation sorts the dict by its keys. Both implementations also take into consideration the values, since both algorithms sort by the dict's items, which are tuples of (key, value) pairs. The outcome is still the same as sorting by keys only, since the keys in a dict are guaranteed to be unique, but still... \$\endgroup\$ Commented Jan 24, 2023 at 10:51
  • \$\begingroup\$ So you would say, I should extract keys, sort by those and then reassign values in the sorted order? \$\endgroup\$ Commented Jan 24, 2023 at 10:55
  • \$\begingroup\$ Also, in your title you talk about sorting by keys whereas in your question's body you assert that the algorithm sorts by value which is not true at all. \$\endgroup\$ Commented Jan 24, 2023 at 10:55
  • \$\begingroup\$ That depends on the use case. Depending on the actual dict's values sorting by items might be slower than only sorting by keys. You've given too little context for me to recommend anything. \$\endgroup\$ Commented Jan 24, 2023 at 10:57
  • \$\begingroup\$ Sorry, as key and value are standing concepts and I wanted to say that I wanted to sort by the "value" of the keys and not by the "values" in the dict, I should clarify. Will update the question. \$\endgroup\$ Commented Jan 24, 2023 at 10:57

2 Answers 2

3
\$\begingroup\$

python dictionaries (dict) can only be assumed to retain their order for python 3.7 and later. For earlier versions of python it is implementation dependent, if order is retained. Sometimes it might work, sometimes it does not.

This means unless you can guarantee, that you run on python >= 3.7, the first code might introduce very nasty dangerous bugs.

Even if you guarantee that you run on python 3.7, it would be nice to know that you rely on order. Instead of writing a comment the collections.OrderedDict is self-documenting and tells any future developer that you indeed need order. It also tells other libraries in the python ecosystem, that they should preserve the order of the dictionary. This is for example the case for pandas.DataFrames, when they are initialized from dictionaries.

So all in all, I would go for the second option.

As mentioned already in the comments, you are not sorting by key, but by a (key, value) pair.

\$\endgroup\$
1
  • \$\begingroup\$ Yeah, I was also thinking about being more specific when using OrderedDict. However, I won't go for pandas because it would introduce another dependency. I don't refrain it totally, it's just beyond scope of my goal. Anyway, it's good to know for future. \$\endgroup\$ Commented Jan 25, 2023 at 12:54
1
\$\begingroup\$

The second implementation using collections.OrderedDict is better in my opinion for all the reasons mentioned by @mcocdawc.

Additionally, in the sake of reusability, I would suggest changing the API to match the built-in sorted() function and documenting your method:

  • rename sort_dct to sorted_dict: the built-in sort function sorts iterables in place, and sorted creates a sorted copy. It's always good to follow existing conventions
  • Allow passing a key and a reverse flag
  • Add a docstring
from collections import OrderedDict

def sorted_dict(dictionary, /, *, key=None, reverse=False) -> OrderedDict:
    '''
    Return a new OrderedDict containing all items from the dictionary in
    ascending key order

    A custom key function can be supplied to customize the sort order, and the
    reverse flag can be set to request the result in descending order.
    '''
    return OrderedDict(sorted(dictionary.items(), key=key, reverse=reverse))

Notes:

  • the / and * in the signature denote that arguments before the / are positional only and those after the * are keyword only, matching the signature of the built-in sorted() function
  • using the key is kind of janky, as items() returns a interable of tuples. This could maybe be improved by unpacking items into a namedTuple, improving the docstring, or some other way, but I went for simplicity here
  • With very minor changes you could implement a sort_dict function for in-place sorting

Usage:

>>> foo = {1: 'a', 3: 'b', 2: 'c'}
>>> sorted_dict(foo)
OrderedDict([(1, 'a'), (2, 'c'), (3, 'b')])
>>> sorted_dict(foo, reverse=True)
OrderedDict([(3, 'b'), (2, 'c'), (1, 'a')])
>>> sorted_dict(foo, key=lambda item: item[1], reverse=True)
OrderedDict([(2, 'c'), (3, 'b'), (1, 'a')])
\$\endgroup\$
3
  • \$\begingroup\$ Why don't you just use , *, instead of /, *,? Thank you about the hint on sort via sorted. Would it be possible to use lambda tup: tup[0] as the key argument to sorted, according to Example 3: Sort the list using sorted() having a key function, programiz.com/python-programming/methods/built-in/sorted? \$\endgroup\$ Commented Jan 25, 2023 at 12:59
  • \$\begingroup\$ @LooserUser Omitting / means that parameters that come before * are positional-or-keyword, instead of positional only. See: docs.python.org/3/tutorial/controlflow.html#special-parameters It would also work, but I wanted to stay as close as I could to the sorted convention. For the key argument, the argument name used in the lambda function doesn't matter, tup would works just as well as item or anything else. Note that using tup[0] uses the dictionary keys as the sorting key, which would have the same effect as the default sorting operation. \$\endgroup\$
    – gazoh
    Commented Jan 25, 2023 at 13:30
  • \$\begingroup\$ @RichardNeumann As you said: "Neither implementation sorts the dict by its keys. Both implementations also take into consideration the values, since both algorithms sort by the dict's items, which are tuples of (key, value) pairs." : Would you prefer the lambda to clarify the dict is sorted by it's key and not by the (key, value)-pair? \$\endgroup\$ Commented Jan 25, 2023 at 13:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.