Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Attrgetter that handles None values and can be used in a loop

Tags:

python

sorting

I have a function that sorts repeatedly based on a list of tuples of attributes and reversed booleans, like so:

def multisort(lst, sorting):
  for attr, reverse in reversed(sorting):
    lst.sort(key=operator.attrgetter(attr), reverse=reverse)
  return lst

Example input to sorting var would be something like [('attr_1', True), ('attr_2', False)].

I'm upgrading code to Py3 and this no longer works because occasionally the attribute values are None (can't compare NoneType to non-NoneTypes). There's lots of solutions on stackoverflow for this type of problem that recommend swapping out attrgetter for a lambda function like lambda x: (getattr(x, attr) is None, getattr(x, attr)).

Unfortunately, this doesn't work for me because the attributes I'm sorting on can have dots in them, e.g. 'attr_1.sub_attr_1'. operator.attrgetter supports this, but of course native getattr does not.

Any suggestions on how I can either write a wrapper around attrgetter to handle this or write a custom key function that would work here? Thanks in advance.

like image 379
Chad Avatar asked Oct 16 '25 04:10

Chad


1 Answers

You can use an alternative getter that will return a key function that will generate tuples (value is not None, value) (if you want None to come first, which was the case with Python 2 where it is less than anything).

import operator


def none_aware_attrgetter(attr):
    getter = operator.attrgetter(attr)
    def key_func(item):
        value = getter(item)
        return (value is not None, value)
    return key_func

def multisort(lst, sorting):
    for attr, reverse in reversed(sorting):
        lst.sort(key=none_aware_attrgetter(attr), reverse=reverse)
    return lst

A sample run:

class C:
    def __repr__(self):
        return f'<a1:{self.a1}, a2.s: {self.a2.s}>'

a = C()
b = C()
c = C()

a.a1 = 10
b.a1 = None
c.a1 = 0


a.a2 = C()
a.a2.s = 10

b.a2 = C()
b.a2.s = None

c.a2 = C()
c.a2.s = 0


lst = [a, b, c]
print(multisort(lst, [('a1', False)]))
# [<a1:None, a2.s: None>, <a1:0, a2.s: 0>, <a1:10, a2.s: 10>]

print(multisort(lst, [('a2.s', True)]))
# [<a1:10, a2.s: 10>, <a1:0, a2.s: 0>, <a1:None, a2.s: None>]
like image 147
Thierry Lathuille Avatar answered Oct 17 '25 19:10

Thierry Lathuille