Sorting by Item or by Attribute

Credit: Matthew Wood

Problem

You need to sort a list of (x, y) coordinates by item y, or, more generally, sort a list of objects by any attribute or item.

Solution

You might first think of something like the following class, based on the simple but slow approach of passing a comparison function to the sort method:

class Sorter:
    # Notice how _ _compare is dependent on self._ _whichindex
    def _ _compare(self, x, y):
        return cmp(x[self._ _whichindex], y[self._ _whichindex])

    # Pass the sort function the _ _compare function defined above
    def _ _call_ _(self, data, whichindex = None):
        if whichindex is None :
            data.sort(  )
        else :
            self._ _whichindex = whichindex
            data.sort(self._ _compare)
        return data               # handy for inlining, and low-cost

The trick is to use a bound method that accesses instance state as the comparison function to determine which item (or attribute, but this version deals only with items) to fetch. Unfortunately, this makes the approach nonreentrant and not thread-safe.

Thanks to the faster, more robust, and more flexible DSU idiom, it’s not hard to make a more general version that allows attributes as well as items, guarantees a stable sort, is both reentrant and thread-safe, and is speedier to boot:

class Sorter:
    def _helper(self, data, aux, inplace):
        aux.sort(  )
        result = [data[i] for junk, i in aux]
        if inplace: data[:] = result
        return result

    def byItem(self, data, itemindex=None, inplace=1):
        if itemindex is None:
            if inplace:
                data.sort(  )
                result = data
            else:
                result = data[:]
                result.sort(  )
            return result
        else:
            aux = [(data[i][itemindex], i) for i in range(len(data))]
            return self._helper(data, aux, inplace)

    # a couple of handy synonyms
    sort = byItem
    _ _call_ _ = byItem

    def byAttribute(self, data, attributename, inplace=1):
        aux = [(getattr(data[i],attributename),i) for i in range(len(data))]
        return self._helper(data, aux, inplace)

Of course, since the second version doesn’t use its “classhood” in any way, making it a class is somewhat peculiar. It would be far more Pythonic, and clearer, to use a module with free-standing functions, rather than an artificial class with instances that have no state.

Discussion

How do you efficiently sort a list of (x, y) coordinates by y? More generally, how do you sort a list of dictionaries, lists, or class instances by a particular item or attribute? I hate not being able to sort by any item or attribute, so I wrote an auxiliary class to do it for me.

The DSU idiom is much faster than passing sort a comparison function, as discussed in other recipes. The second version of Sorter in this recipe uses DSU because of this, as well as auxiliary flexibility advantages. This second version gets no benefit from being a class rather than just a couple of functions, but casting it as a class makes it drop-in compatible with the first, inferior version, which did use some state as a trick (losing reentrancy and thread-safety in the process).

Here is some example code (note that it instantiates the Sorter class only once, another hint that it is not at all an optimal architecture to wrap this functionality as a class):

sort = Sorter(  )

if _ _name_ _ == '_ _main_ _' :
      list = [(1, 2), (4, 8), (0, 3)]
      dict = [{'a': 3, 'b': 4}, {'a': 5, 'b': 2}, {'a': 0, 'b': 0},
          {'a': 9, 'b': 9}]
      dumb = [1, 4, 6, 7, 2, 5, 9, 2, 4, 6]

      print 'list normal:', list
      sort(list, 0)
      print 'sort by [0]:', list
      sort(list, 1)
      print 'sort by [1]:', list

      print

      print "dict normal:", dict
      sort(dict, 'a')
      print "sort by 'a':",  dict
      sort(dict, 'b')
      print "sort by 'b':",  dict

      print

      print 'dumb normal:', dumb
      sort(dumb)
      print 'normal sort:', dumb

Returning the sorted list is cheap (it’s just an extra reference, since Python, fortunately, never does any copying of data unless you specifically request it) and offers uniform behavior between in-place and non-in-place cases. Often, we only want to do:

for x in sort(something, inplace=0):

Returning a reference to the sorted data gives us just the right amount of flexibility for this.

See Also

Recipe 2.4 and Recipe 4.10.

Get Python Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.