84

I want to sort a list of named tuples without having to remember the index of the fieldname. My solution seems rather awkward and was hoping someone would have a more elegant solution.

from operator import itemgetter
from collections import namedtuple

Person = namedtuple('Person', 'name age score')
seq = [
    Person(name='nick', age=23, score=100),
    Person(name='bob', age=25, score=200),
]

# sort list by name
print(sorted(seq, key=itemgetter(Person._fields.index('name'))))
# sort list by age
print(sorted(seq, key=itemgetter(Person._fields.index('age'))))

Thanks, Nick

jamylak
  • 128,818
  • 30
  • 231
  • 230
Nick
  • 873
  • 1
  • 6
  • 7

5 Answers5

108
from operator import attrgetter
from collections import namedtuple

Person = namedtuple('Person', 'name age score')
seq = [Person(name='nick', age=23, score=100),
       Person(name='bob', age=25, score=200)]

Sort list by name

sorted(seq, key=attrgetter('name'))

Sort list by age

sorted(seq, key=attrgetter('age'))
jamylak
  • 128,818
  • 30
  • 231
  • 230
69
sorted(seq, key=lambda x: x.name)
sorted(seq, key=lambda x: x.age)
clyfish
  • 10,240
  • 2
  • 31
  • 23
  • 11
    I think this is more elegant than using `attrgetter` – zenpoy Aug 23 '12 at 08:55
  • 3
    I prefer the attrgetter, but that is just taste. An advantage is also if I were to get the fields to sort on dynamically. Then I could just pass the string. – Nick Aug 23 '12 at 09:02
  • 3
    @zenpoy Keep in mind `attrgetter` performs much better and `lambda`s arent usually considered elegant – jamylak Dec 10 '14 at 22:45
  • 2
    and `sorted(seq, key=lambda x: [x.age, x.name])` sorts by multiple attributes – SpeedCoder5 May 24 '19 at 00:00
19

I tested the two alternatives given here for speed, since @zenpoy was concerned about performance.

Testing script:

import random
from collections import namedtuple
from timeit import timeit
from operator import attrgetter

runs = 10000
size = 10000
random.seed = 42
Person = namedtuple('Person', 'name,age')
seq = [Person(str(random.randint(0, 10 ** 10)), random.randint(0, 100)) for _ in range(size)]

def attrgetter_test_name():
    return sorted(seq.copy(), key=attrgetter('name'))

def attrgetter_test_age():
    return sorted(seq.copy(), key=attrgetter('age'))

def lambda_test_name():
    return sorted(seq.copy(), key=lambda x: x.name)

def lambda_test_age():
    return sorted(seq.copy(), key=lambda x: x.age)

print('attrgetter_test_name', timeit(stmt=attrgetter_test_name, number=runs))
print('attrgetter_test_age', timeit(stmt=attrgetter_test_age, number=runs))
print('lambda_test_name', timeit(stmt=lambda_test_name, number=runs))
print('lambda_test_age', timeit(stmt=lambda_test_age, number=runs))

Results:

attrgetter_test_name 44.26793992166096
attrgetter_test_age 31.98247099677627
lambda_test_name 47.97959511074551
lambda_test_age 35.69356267603864

Using lambda was indeed slower. Up to 10% slower.

EDIT:

Further testing shows the results when sorting using multiple attributes. Added the following two test cases with the same setup:

def attrgetter_test_both():
    return sorted(seq.copy(), key=attrgetter('age', 'name'))

def lambda_test_both():
    return sorted(seq.copy(), key=lambda x: (x.age, x.name))

print('attrgetter_test_both', timeit(stmt=attrgetter_test_both, number=runs))
print('lambda_test_both', timeit(stmt=lambda_test_both, number=runs))

Results:

attrgetter_test_both 92.80101586919373
lambda_test_both 96.85089983147456

Lambda still underperforms, but less so. Now about 5% slower.

Testing is done on Python 3.6.0.

André C. Andersen
  • 8,955
  • 3
  • 53
  • 79
4

since nobody mentioned using itemgetter(), here how you do using itemgetter().

from operator import itemgetter
from collections import namedtuple

Person = namedtuple('Person', 'name age score')
seq = [
    Person(name='nick', age=23, score=100),
    Person(name='bob', age=25, score=200),
]

# sort list by name
print(sorted(seq, key=itemgetter(0)))

# sort list by age
print(sorted(seq, key=itemgetter(1)))
sir.Suhrab
  • 51
  • 3
4

This might be a bit too 'magical' for some, but I'm partial to:

# sort list by name
print(sorted(seq, key=Person.name.fget))

Edit: this assumes namedtuple uses the property() built-in to implement the accessors, because it leverages the fget attribute on such a property (see documentation). This may still be true in some implementations but it seems CPython no longer does that, which I think is related to optimization work referenced in https://bugs.python.org/issue32492 (so, since 3.8). Such fragility is the cost of the "magic" I mentioned; namedtuple certainly doesn't promise to use property().

Writing Person.name.__get__ is better (works before & after the implementation change) but is maybe not worth the arcaneness vs. just writing it more plainly as lambda p: p.name

Sumudu Fernando
  • 1,763
  • 2
  • 11
  • 17
  • Could you please add some context to your answer? I tried it but am getting *AttributeError: '_collections._tuplegetter' object has no attribute 'fget'*. – simlev Oct 12 '21 at 08:52
  • Sure. This approach used to work but it depended on a particular implementation detail of namedtuple. That seems to have been changed in https://bugs.python.org/issue32492. Will add an edit to the answer shortly. – Sumudu Fernando Oct 13 '21 at 09:16