Searching or Sorting a list of Objects Based on an Attribute in Python | by Allison Stafford | Towards Data Science

Often, when you have a group of objects, you might be looking to sort them based not on their location in memory, but on an attribute of interest. Today, we’ll look at Python’s built in tool for doing just this: operator.attrgetter, as well as the related tools in this toolkit and alternate methods to achieve similar goals (using lambdas and iterators/comprehensions).
For our data set today, we’re going to be using a fictional data set of a pack of 100 dogs. The dog student class has attributes: name, yr, strengths, areas_for_growth, works_well_with, not_works_well_with, has_sat_by, has_not_sat_by, and notes. It has methods that add, get, remove, and these attributes, plus get_n_not_works_well and get_n_works_well that returns the number of friends/frenemies in their respective lists.
Attrgetter is an attribute getter. You can use it in a couple ways:
You can create an attrgetter “callable object that fetches attr from its operand” (from the docs).
You can create an attrgetter “callable object that fetches attr from its operand” (from the docs).
from operator import attrgetterfriend_getter = attrgetter('works_well_with')print(friend_getter(dog_pack[1]))
Here friend_getter returns dog_pack[1]’s works_well_with list:
[Student('Qeb', 12), Student('Putty', 6), Student('Douglas', 3), Student('Denali', 5), Student('Asher', 12), Student('Portia', 3), Student('Suki', 2), Student('Olive', 14), Student('Peri', 8), Student('Mari', 5), Student('Snowflake', 2), Student('Guayaba', 12)]
I’m still looking for a context in which you would do this over dog_pack[1].works_well_with. We’ll see one example I ran into below, but I’m not sure I’m convinced.
2. You can use attrgetter with functions that expect a function argument. For example, as the key to sorted(), max(), min(); or with map(), itertools.groupby(), etc.
To get the oldest dog:
# to get the object with the characteristic (max yr)oldest = max(dog_pack, key=attrgetter("yr"))print(f"Oldest Dog\nname: {oldest.name}\nage: {oldest.yr}",       f"Strengths: {oldest.strengths}")
printing:
Oldest Dogname: Abbeyage: 15Strengths: ['snuggly', 'snuggly', 'plays well', 'fetch']
Here we actually have multiple oldest dogs, so we’ll want to use a filtering/conditional list comprehension to get a list oldest dogs:
[d for d in dog_pack if d.yr == max(d.yr for d in dog_pack)]
returning
[Student(Abbey, 15), Student(Radar, 15), Student(Pinot, 15), Student(Lucci, 15), Student(Leona Mae Alcott Helmsley Cole, 15), Student(Stella, 15), Student(Whiskers, 15)]
Don’t forget that if we want the values themselves, and not the objects with the value, there’s no need to use attrgetter.
max(d.yr for d in dog_pack)
Which returns 15.
Methodcaller works very similarly to attrgetter, but when you want to use the return of a class’ method instead of a class’ attribute.
For example, if I want to get the dog student that works well with the fewest other students:
min_works_well = min(dog_pack, key=methodcaller('get_n_works_well'))print(f"""Works Well With The Fewest Dogs          name: {min_works_well.name}          works well with: {min_works_well.get_n_works_well()} dogs          strengths: {min_works_well.strengths}          areas for growth: {min_works_well.areas_for_growth}""")
returning:
Works Well With The Fewest Dogsname: Pringlesworks well with: 4 dogsstrengths: ['leave it', 'snuggly', 'comes when called']areas for growth: ['playing with others']
Another difference between attrgetter and methodcaller is that with attrgetter, additional arguments are additional attributes to get, while with methodcaller they are passed as arguments to the method.
attrgetter works on classes, while itemgetter works on other data structures (lists, tuples, dictionaries, etc).
If our dog_pack were structured as a list of dictionaries:
oldest = max(dogs_dict_list, key=itemgetter('yr'))print(f"Oldest dog\nname: {oldest['name']}",      f"strengths: {oldest['strengths']}")
itemgetter uses ‘yr’ as the key and returns:
Oldest dogname: Dolliestrengths: ['plays well', 'ignores food on the sidewalk']
If it were structured as this list of tuples:
dogs_tups = [('Bella', 5, 'labrador', 'yellow'),              ('Dollie', 12, 'austrailian sheppard', 'cream, brown')]youngest = min(dogs_tups, key=itemgetter(1))print(f"Youngest Dog:\nname: {youngest[0]}\nbreed: {youngest[2]}")
itemgetter would use the argument 1 as the index and return
Youngest Dog:name: Bellabreed: labrador
Like attrgetter, itemgetter returns a tuple of lookup values when supplied multiple arguments.
If you wanted to sort our dogs, not by one of these first-tier attributes, but by a component of an attribute, I’ve seen four options:
If the attribute is a class, you can access an attribute of your attribute by passing the second attribute name to attrgetter along with the first. For example if we alter the name attribute to create an instance of the class Name, we can sort by first name with sorted(dog_pack, key=attrgetter("name.first") as in the docs’ example.If you have access to the class, or the class has a method that returns the sub-attribute you are interested in, you can use methodcaller to access/sort by that sub-attribute.
If the attribute is a class, you can access an attribute of your attribute by passing the second attribute name to attrgetter along with the first. For example if we alter the name attribute to create an instance of the class Name, we can sort by first name with sorted(dog_pack, key=attrgetter("name.first") as in the docs’ example.
If you have access to the class, or the class has a method that returns the sub-attribute you are interested in, you can use methodcaller to access/sort by that sub-attribute.
from operator import methodcallersorted(dog_pack, key=methodcaller("get_area_for_growth", 0))
Here growth_sorted is a list of the dogs sorted by their first area for growth (alphabetically).
Note again that methodcaller passes additional arguments to the method. This is in contrast with attrgetter, where additional arguments are read as additional attributes to get, supporting multi-attribute keys. For multiple-attribute keys with methodcaller, you would need a lambda function.
3. Use a lambda or defined function.
sorted(dog_pack, key=lambda x: (x.get_area_for_growth(0),                                x.name.first)
or equivalently:
def first_growth_first_name(x):    "Returns the first area for growth and first name of Dog x"    first_growth = x.get_area_for_growth(0)    first_name = x.name.first    return (first_growth, first_name)sorted(dog_pack, key=first_growth_first_name)
4. Combine lambda and attrgetter/methodcaller.
sorted(dog_pack[0:5], key = lambda x: attrgetter("strengths")(x)[0])
This example feels pretty convoluted to me and depends on every dog in the pack having strength at that location. But it might be the way to go in some situation. I am not sure about the relative compute time here with the combination of attrgetter and lambda as compared to lambda and direct attribute access.
It seems like the built in functions operate somewhat faster, which may or may not be important for your scenario. Folks on the internet seem to disagree as to which is more elegant/pythonic/nice looking. The use of strings blocks the autocomplete functionality of many code editors, which requires one to type/spell/know what attribute they want. Not ideal. Not a dealbreaker for me.
Each of these can easily be substituted with a lambda function (or defined function):
Sorting our dog pack by year then name:
sorted(dog_pack, key=attrgetter(“yr”, "name.first")) becomes
sorted(dog_pack, key=lambda x: (x.yr, x.name.first))
dogs_tups = [('Bella', 5, 'labrador', 'yellow'),              ('Dollie', 12, 'austrailian sheppard', 'cream, brown')]
Sorting these dog tuples by breed:
sorted(dogs_tups, key=itemgetter(2) becomes
sorted(dog_pack, key=lambda x: x[2])
Sorting our dog pack by the number of dogs they play well with:
sorted(dog_pack, key=methodcaller(“get_n_works_well”)) becomes
sorted(dog_pack, key=lambda x: x.get_n_works_well())
If you need to process the attribute data before sorting/getting the max/getting the min/whatever, then you may need to use an alternative to attrgetter.
For example, originally, my dog names had non-standardized capitalization. If you don’t have access to the class, you can correct for this using:
sorted(dog_pack, key=lambda x: x.name.first.lower())
or if you are looking for dogs with names that start with A:
[d for d in dog_pack if d.name.first.lower() < 'b']
Check out the GitHub repository here. Happy coding!