Working With the Python operator Module

Working With the Python operator Module

by Ian Eyre Aug 02, 2023 intermediate data-structures python

Whenever you perform calculations in Python, you make use of built-in operators such as +, %, and **. Did you know that Python also provides an operator module? While it may seem that the purpose of operator is to provide an alternative to these existing Python operators, the module actually has a far more specialized purpose than this.

The Python operator module provides you with a set of functions, many of which correspond to the built-in operators but don’t replace them. The module also provides additional functionality, as you’ll soon discover.

In this tutorial, you’ll learn how to:

  • Use any of the basic operator-equivalent functions
  • Pass operator functions as arguments
  • Serialize operator functions for later use
  • Gauge operator function performance against common alternatives
  • Use higher-order operator functions in a range of interesting example cases

To get the most out of this tutorial, you should be familiar with the basic Python operators and the functional programming idea of one function returning another function back to its caller.

With that knowledge, you’re ready to dive in and learn how to use the greatly misunderstood operator module to your advantage.

Using the Python operator Module’s Basic Functions

In this section, you’ll learn about the operator module’s operator-equivalent functions that mimic built-in operators, and you’ll pass them as arguments to higher-order functions. You’ll also learn how to save them for later use. Finally, you’ll investigate the performance of the operator-equivalent functions and uncover why you should never use them where a built-in Python operator will do instead.

Learning How the Basic Functions Work

The Python operator module contains over forty functions, many of which are equivalent to the Python operators that you’re already familiar with. Here’s an example:

Python
>>> import operator

>>> operator.add(5, 3)  # 5 + 3
8

>>> operator.__add__(5, 3)  # 5 + 3
8

Here, you add 5 and 3 together using both add() and __add__(). Both produce the same result. On the face of it, these functions provide you with the same functionality as the Python + operator, but this isn’t their purpose.

If you take a look at the list of functions that operator provides for you, then you’ll discover that they cover not only the arithmetic operators, but also the equality, identity, Boolean, and even bitwise operators. Try out a random selection of them:

Python
>>> operator.truediv(5, 2)  # 5 / 2
2.5

>>> operator.ge(5, 2)  # 5 >= 2
True

>>> operator.is_("X", "Y")  # "X" is "Y"
False

>>> operator.not_(5 < 3)  # not 5 < 3
True

>>> bin(operator.and_(0b101, 0b110))  # 0b101 & 0b110
'0b100'

In the code above, you work with a selection from the five main categories. First, you use the equivalent of an arithmetic operator, and then you try out equality and identity operator examples in the second and third examples, respectively. In the fourth example, you try a Boolean logical operator, while the final example uses a bitwise operator The comments show the equivalent Python operators.

Before reading the rest of this tutorial, feel free to take some time to experiment with the range of operator-equivalent functions that Python’s operator module provides for you. You’ll learn how to use them next.

Passing Operators as Arguments Into Higher-Order Functions

You use the operator-equivalent functions most commonly as arguments for higher-order functions. You could write a higher-order function that performs a series of different tasks depending on the operator function passed to it. Suppose, for example, you wanted a single function that could perform addition, subtraction, multiplication, and division. One messy way of doing this would be to use an ifelif statement as follows:

Python
>>> def perform_operation(operator_string, operand1, operand2):
...     if operator_string == "+":
...         return operand1 + operand2
...     elif operator_string == "-":
...         return operand1 - operand2
...     elif operator_string == "*":
...         return operand1 * operand2
...     elif operator_string == "/":
...         return operand1 / operand2
...     else:
...         return "Invalid operator."
...

In your perform_operation() function, the first parameter is a string representing one of the four basic arithmetic operations. To test the function, you pass in each of the four operators. The results are what you’d expect:

Python
>>> number1 = 10
>>> number2 = 5
>>> calculations = ["+", "-", "*", "/"]

>>> for op_string in calculations:
...     perform_operation(op_string, number1, number2)
...
15
5
50
2.0

This code is not only messy, but also limited to the four operators defined in the elif clauses. Try, for example, passing in a modulo operator (%), and the function will return an "Invalid operator" message instead of the modulo division result that you were hoping for.

This is where you can make excellent use of the operator functions. Passing these into a function gives you several advantages:

Python
>>> def perform_operation(operator_function, operand1, operand2):
...     return operator_function(operand1, operand2)
...

This time, you’ve improved your perform_operation() function so that the first parameter can accept any of the operator module’s functions that take exactly two arguments. The second and third parameters are those arguments.

The revised test code is similar to what you did before, except you pass in operator functions for your perform_operation() function to use:

Python
>>> from operator import add, sub, mul, truediv

>>> number1 = 10
>>> number2 = 5
>>> calculations = [add, sub, mul, truediv]

>>> for op_function in calculations:
...     perform_operation(op_function, number1, number2)
...
15
5
50
2.0

This time, your calculations list contains references to the functions themselves. Note that you pass in function names and not function calls. In other words, you pass in add to perform_operation(), and not add(). You’re passing in the function object, not the result of its execution. Remember, the name of a function is actually a reference to its code. Using the () syntax calls the function.

There are two advantages to using your updated version of perform_operation(). The first is expandability. You can use the revised code with any of the other operator functions that require exactly two arguments. Indeed, you might like to experiment by passing the operator module’s mod(), pow(), and repeat() functions to both versions of your function. Your updated version works as expected, while your original version returns "Invalid operator".

The second advantage is readability. Take a look at both versions of your perform_operation() function, and you’ll notice that your second version is not only significantly shorter, but also more readable, than the original.

Passing functions as arguments to other functions is a feature that you’ll often use in functional programming. This is one of the main purposes of the operator module. You’ll study other examples of this later.

Serializing operator Module Functions

One way of saving objects, including functions, to disk is to serialize them. In other words, your code converts them into byte streams and stores them on disk for later use. Conversely, when you read serialized objects back from disk, you deserialize them, allowing them to be read from disk into a program for use.

There are several reasons why you might serialize functions, including to save them for future use in another program or to pass them between different processes running on one or more computers.

A common way to serialize functions in Python is by using the pickle module. This, along with its dictionary wrapper shelve, provides one of the most efficient ways of storing data. However, when you serialize a function using pickle, then you only serialize its fully qualified name, not the code in the function body. When you deserialize a function, the environment must provide access to the function’s code. The function can’t work otherwise.

To see an example, you’ll revisit the earlier perform_operation() example. You’ll call different operator functions to perform the different operations. The following code adds a dictionary that you’ll use to map a string operator to its matching function:

Python
>>> import operator
>>> operators = {
...     "+": operator.add,
...     "-": operator.sub,
...     "*": operator.mul,
...     "/": operator.truediv,
... }

>>> def perform_operation(op_string, number1, number2):
...     return operators[op_string](number1, number2)
...

>>> perform_operation("-", 10, 5)
5

The operations supported by perform_operation() are the ones defined in operators. As an example, you run the "-" operation, which calls operator.sub() in the background.

One way to share the supported operators between processes is to serialize the operators dictionary to disk. You can do this using pickle as follows:

Python
>>> import pickle
>>> with open("operators.pkl", mode="wb") as f:
...     pickle.dump(operators, f)
...

You open a binary file for writing. To serialize operators, you call pickle.dump() and pass the structure that you’re serializing and the handle of the destination file.

This creates the file operators.pkl in your local working directory. To demonstrate how to reuse operators in a different process, restart your Python shell and load the pickled file:

Python
>>> import pickle
>>> with open("operators.pkl", mode="rb") as f:
...     operators = pickle.load(f)
...
>>> operators
{'+': <built-in function add>, '-': <built-in function sub>,
 '*': <built-in function mul>, '/': <built-in function truediv>}

Firstly, you import pickle again and reopen the binary file for reading. To read the operator structure, you use pickle.load() and pass in the file handle. Your code then reads in the saved definition and assigns it to a variable named operators. This name doesn’t need to match your original name. This variable points to the dictionary that references the different functions, assuming they’re available.

Note that you don’t need to explicitly import operator, although the module needs to be available for Python to import in the background.

You can define perform_operation() again to see that it can refer to and use the restored operators:

Python
>>> def perform_operation(op_string, number1, number2):
...     return operators[op_string](number1, number2)
...

>>> perform_operation("*", 10, 5)
50

Great! Your code handles multiplication as you’d expect.

Now, there’s nothing special about operator supporting pickling of functions. You can pickle and unpickle any top-level function, as long as Python is able to import it in the environment where you’re loading the pickled file.

However, you can’t serialize anonymous lambda functions like this. If you implemented the example without using the operator module, then you’d probably define the dictionary as follows:

Python
>>> operators = {
...     "+": lambda a, b: a + b,
...     "-": lambda a, b: a - b,
...     "*": lambda a, b: a * b,
...     "/": lambda a, b: a / b,
... }

The lambda construct is a quick way to define simple functions, and they can be quite useful. However, because pickle doesn’t serialize the function body, only the name of the function, you can’t serialize the nameless lambda functions:

Python
>>> import pickle
>>> with open("operators.pkl", mode="wb") as f:
...     pickle.dump(operators, f)
...
Traceback (most recent call last):
  ...
PicklingError: Can't pickle <function <lambda> at 0x7f5b946cfba0>: ...

If you try to serialize lambda functions with pickle, then you’ll get an error. This is a case where you can often use operator functions instead of lambda functions.

Look back at your serialization code and notice that you imported both operator and pickle, while your deserialization code imported only pickle. You didn’t need to import operator because pickle did this automatically for you when you called its load() function. This works because the built-in operator module is readily available.

Investigating operator Function Performance Against the Alternatives

Now that you have an idea of how to use the operator-equivalent functions, you may wonder if you should use them instead of either the Python operators or lambda functions. The answer is no to the first case and yes to the second. The built-in Python operators are always significantly faster than their operator module counterparts. However, the operator module’s functions are faster than lambda functions, and they’re more readable as well.

If you wish to time the operator module’s functions against their built-in or lambda equivalents, then you can use the timeit module. The best way to do this is to run it directly from the command line:

Windows PowerShell
PS> python -m timeit "(lambda a, b: a + b)(10, 10)"
5000000 loops, best of 5: 82.3 nsec per loop
PS> python -m timeit -s "from operator import add" "add(10, 10)"
10000000 loops, best of 5: 24.5 nsec per loop
PS> python -m timeit "10 + 10"
50000000 loops, best of 5: 5.19 nsec per loop

PS> python -m timeit "(lambda a, b: a ** b)(10, 10)"
1000000 loops, best of 5: 226 nsec per loop
PS> python -m timeit -s "from operator import pow" "pow(10, 10)"
2000000 loops, best of 5: 170 nsec per loop
PS> python -m timeit "10 ** 10"
50000000 loops, best of 5: 5.18 nsec per loop

The above PowerShell session uses the timeit module to compare the performance of various implementations of addition and exponentiation. Your results show that for both operations, the built-in operator is fastest, with the operator module function only outperforming the lambda function. The actual time values themselves are machine-specific, but their relative differences are significant.

Go ahead and try the other operator functions out for yourself. Although the exact timings will vary from machine to machine, their relative differences will still show that built-in operators are always faster than the operator module equivalents, which are always faster than lambda functions.

Now you’re familiar with the operator-equivalent functions from the operator module, but you might want to spend some time exploring the rest of these functions. Once you’re ready to move on, keep reading to investigate some of the other ways to use operator.

Using the Python operator Module’s Higher-Order Functions

In this section, you’ll learn about three of the higher-order functions that Python’s operator module makes available to you: itemgetter(), attrgetter(), and methodcaller(). You’ll learn how these allow you to work with Python collections in a range of useful ways that encourage a functional style of Python programming.

Selecting Values From Multidimensional Collections With itemgetter()

The first function that you’ll learn about is operator.itemgetter(). In its basic form, you pass it a single parameter that represents an index. Then itemgetter() returns a function that, when passed a collection, returns the element at that index.

To begin with, you create a list of dictionaries:

Python
>>> musician_dicts = [
...     {"id": 1, "fname": "Brian", "lname": "Wilson", "group": "Beach Boys"},
...     {"id": 2, "fname": "Carl", "lname": "Wilson", "group": "Beach Boys"},
...     {"id": 3, "fname": "Dennis", "lname": "Wilson", "group": "Beach Boys"},
...     {"id": 4, "fname": "Bruce", "lname": "Johnston", "group": "Beach Boys"},
...     {"id": 5, "fname": "Hank", "lname": "Marvin", "group": "Shadows"},
...     {"id": 6, "fname": "Bruce", "lname": "Welch", "group": "Shadows"},
...     {"id": 7, "fname": "Brian", "lname": "Bennett", "group": "Shadows"},
... ]

Each dictionary contains a record of a musician belonging to one of two groups, the Beach Boys or the Shadows. To learn how itemgetter() works, suppose you want to select a single item from musician_dicts:

Python
>>> import operator

>>> get_element_four = operator.itemgetter(4)
>>> get_element_four(musician_dicts)
{"id": 5, "fname": "Hank", "lname": "Marvin", "group": "Shadows"}

When you pass itemgetter() an index of 4, it returns a function, referenced by get_element_four, that returns the element at index position 4 in a collection. In other words, get_element_four(musician_dicts) returns musician_dicts[4]. Remember that list elements are indexed starting at 0 and not 1. This means itemgetter(4) actually returns the fifth element in the list.

Next suppose you want to select elements from positions 1, 3, and 5. To do this, you pass itemgetter() multiple index values:

Python
>>> get_elements_one_three_five = operator.itemgetter(1, 3, 5)
>>> get_elements_one_three_five(musician_dicts)
({"id": 2, "fname": "Carl", "lname": "Wilson", "group": "Beach Boys"},
 {"id": 4, "fname": "Bruce", "lname": "Johnston", "group": "Beach Boys"},
 {"id": 6, "fname": "Bruce", "lname": "Welch", "group": "Shadows"})

Here itemgetter() creates a function that you use to find all three elements. Your function returns a tuple containing the results.

Now suppose you want to list only the first and last name values from the dictionaries at index positions 1, 3, and 5. To do this, you pass itemgetter() the "fname" and "lname" keys:

Python
>>> get_names = operator.itemgetter("fname", "lname")

>>> for musician in get_elements_one_three_five(musician_dicts):
...     print(get_names(musician))
...
("Carl", "Wilson")
("Bruce", "Johnston")
("Bruce", "Welch")

This time, itemgetter() provides a function for you to get the values associated with the "fname" and "lname" keys. Your code iterates over the tuple of dictionaries returned by get_elements_one_three_five() and passes each one to your get_names() function. Each call to get_names() returns a tuple containing the values associated with the "fname" and "lname" dictionary keys of the dictionaries at positions 1, 3, and 5 of musician_dicts.

Two Python functions that you may already be aware of are min() and max(). You can use these to find the lowest and highest elements in a list:

Python
>>> prices = [100, 45, 345, 639]
>>> min(prices)
45
>>> max(prices)
639

In the above code, you’ve gotten the lowest and highest values: min() returns the cheapest item, while max() returns the most expensive.

The min() and max() functions contain a key parameter that accepts a function. If you create the function using itemgetter(), then you can use it to instruct min() and max() to analyze specific elements within a list of lists or dictionaries. To explore this, you first create a list of lists of musicians:

Python
>>> musician_lists = [
...     [1, "Brian", "Wilson", "Beach Boys"],
...     [2, "Carl", "Wilson", "Beach Boys"],
...     [3, "Dennis", "Wilson", "Beach Boys"],
...     [4, "Bruce", "Johnston", "Beach Boys"],
...     [5, "Hank", "Marvin", "Shadows"],
...     [6, "Bruce", "Welch", "Shadows"],
...     [7, "Brian", "Bennett", "Shadows"],
... ]

The content of musician_lists is identical to musician_dicts, except each record is in a list. This time, suppose you want to find the list elements with the lowest and highest id values:

Python
>>> get_id = operator.itemgetter(0)

>>> min(musician_lists, key=get_id)
[1, "Brian", "Wilson", "Beach Boys"]
>>> max(musician_lists, key=get_id)
[7, "Brian", "Bennett", "Shadows"]

You first create a function using itemgetter() to select the first element from a list. You then pass this as the key parameter of min() and max(). The min() and max() functions will return to you the lists with the lowest and highest values in their index 0 positions, respectively.

You can do the same with a list of dictionaries by using itemgetter() to create a function that selects key names. Suppose you want the dictionary that contains the musician whose last name comes first in the alphabet:

Python
>>> get_lname = operator.itemgetter("lname")

>>> min(musician_dicts, key=get_lname)
{"id": 7, "fname": "Brian", "lname": "Bennett", "group": "Shadows"}

This time, you set up an itemgetter() function that selects the "lname" dictionary key. You then pass this as the min() function’s key parameter. The min() function returns the dictionary with the lowest value of "lname". The "Bennett" record is the result. Why not retry this with max()? Try predicting what will happen before running your code to check.

Sorting Multidimensional Collections With itemgetter()

In addition to selecting specific elements, you can use the function from itemgetter() as the key parameter to sort data. One of the common Python functions that you may have already used is sorted(). The sorted() function creates a new sorted list:

Python
>>> star_wars_movies_release_order = [4, 5, 6, 1, 2, 3, 7, 8, 9]
>>> sorted(star_wars_movies_release_order)
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Now the elements of the new list are in ascending order. If you wanted to sort the list in place, then you could use the .sort() method instead. As an exercise, you might like to try it.

It’s also possible to use itemgetter() to sort lists. This allows you to sort multidimensional lists by specific elements. To do this, you pass an itemgetter() function into the sorted() function’s key parameter.

Consider once more your musician_lists:

Python
>>> musician_lists = [
...     [1, "Brian", "Wilson", "Beach Boys"],
...     [2, "Carl", "Wilson", "Beach Boys"],
...     [3, "Dennis", "Wilson", "Beach Boys"],
...     [4, "Bruce", "Johnston", "Beach Boys"],
...     [5, "Hank", "Marvin", "Shadows"],
...     [6, "Bruce", "Welch", "Shadows"],
...     [7, "Brian", "Bennett", "Shadows"],
... ]

You’ll now sort this list using itemgetter(). To begin with, you decide to sort the list elements in descending order by id value:

Python
>>> import operator

>>> get_id = operator.itemgetter(0)
>>> sorted(musician_lists, key=get_id, reverse=True)
[[7, "Brian", "Bennett", "Shadows"],
 [6, "Bruce", "Welch", "Shadows"],
 [5, "Hank", "Marvin", "Shadows"],
 [4, "Bruce", "Johnston", "Beach Boys"],
 [3, "Dennis", "Wilson", "Beach Boys"],
 [2, "Carl", "Wilson", "Beach Boys"],
 [1, "Brian", "Wilson", "Beach Boys"]
 ]

To do this, you use itemgetter() to create a function that will select index position 0, the musician’s id. You then call sorted() and pass it musician_lists plus the reference to the function from itemgetter() as its key. To ensure the sort is in descending order, you set reverse=True. Now your list is sorted in descending order by id.

It’s also possible to perform more complex sorting of multidimensional lists. Suppose you want to sort each list in reverse order within musician_lists. First, you sort by last name, then you sort any lists with the same last name by first name. In other words, you’re sorting on first name within last name:

Python
>>> get_elements_two_one = operator.itemgetter(2, 1)
>>> sorted(musician_lists, key=get_elements_two_one, reverse=True)
[[3, "Dennis", "Wilson", "Beach Boys"],
 [2, "Carl", "Wilson", "Beach Boys"],
 [1, "Brian", "Wilson", "Beach Boys"],
 [6, "Bruce", "Welch", "Shadows"],
 [5, "Hank", "Marvin", "Shadows"],
 [4, "Bruce", "Johnston", "Beach Boys"],
 [7, "Brian", "Bennett", "Shadows"]]

This time, you pass itemgetter() two arguments, positions 2 and 1. Your list is sorted in reverse alphabetical order, first by last name (2), then by first name (1) where applicable. In other words, the three Wilson records appear first, with Dennis, Carl, and Brian in descending order. Feel free to rerun this code and use it to select other fields. Try to predict what will happen before running your code to test your understanding.

The same principles also apply to dictionaries, provided you specify the keys whose values you wish to sort. Again, you can use the musician_dicts list of dictionaries:

Python
>>> musician_dicts = [
...     {"id": 1, "fname": "Brian", "lname": "Wilson", "group": "Beach Boys"},
...     {"id": 2, "fname": "Carl", "lname": "Wilson", "group": "Beach Boys"},
...     {"id": 3, "fname": "Dennis", "lname": "Wilson", "group": "Beach Boys"},
...     {"id": 4, "fname": "Bruce", "lname": "Johnston", "group": "Beach Boys"},
...     {"id": 5, "fname": "Hank", "lname": "Marvin", "group": "Shadows"},
...     {"id": 6, "fname": "Bruce", "lname": "Welch", "group": "Shadows"},
...     {"id": 7, "fname": "Brian", "lname": "Bennett", "group": "Shadows"},
... ]

>>> get_names = operator.itemgetter("lname", "fname")
>>> sorted(musician_dicts, key=get_names, reverse=True)
[{"id": 3, "fname": "Dennis", "lname": "Wilson", "group": "Beach Boys"},
 {"id": 2, "fname": "Carl", "lname": "Wilson", "group": "Beach Boys"},
 {"id": 1, "fname": "Brian", "lname": "Wilson", "group": "Beach Boys"},
 {"id": 6, "fname": "Bruce", "lname": "Welch", "group": "Shadows"},
 {"id": 5, "fname": "Hank", "lname": "Marvin", "group": "Shadows"},
 {"id": 4, "fname": "Bruce", "lname": "Johnston", "group": "Beach Boys"},
 {"id": 7, "fname": "Brian", "lname": "Bennett", "group": "Shadows"}
 ]

This time, you pass itemgetter() the "fname" and "lname" keys. The output is similar to what you got previously, except it now contains dictionaries. While you created a function that selected the index elements 2 and 1 in the previous example, this time your function selects the dictionary keys "lname" and "fname".

Retrieving Attributes From Objects With attrgetter()

Next you’ll learn about the operator module’s attrgetter() function. The attrgetter() function allows you to get an object’s attributes. The function accepts one or more attributes to be retrieved from an object, and it returns a function that will return those attributes from whatever object you pass to it. The objects passed to attrgetter() don’t need to be of the same type. They only need to contain the attribute that you want to retrieve.

To understand how attrgetter() works, you’ll first need to create a new class:

Python
>>> from dataclasses import dataclass

>>> @dataclass
... class Musician:
...     id: int
...     fname: str
...     lname: str
...     group: str
...

You’ve created a data class named Musician. Your data class’s primary purpose is to hold data about different musician objects, although it could also contain methods, as you’ll discover later. The @dataclass decorator allows you to define the attributes directly by specifying their names and a type hint for their data types. Your class contains four attributes that describe a musician.

Next, you need a list of objects to work with. You’ll reuse musician_lists from earlier and use it to generate a list of objects named group_members:

Python
>>> musician_lists = [
...     [1, "Brian", "Wilson", "Beach Boys"],
...     [2, "Carl", "Wilson", "Beach Boys"],
...     [3, "Dennis", "Wilson", "Beach Boys"],
...     [4, "Bruce", "Johnston", "Beach Boys"],
...     [5, "Hank", "Marvin", "Shadows"],
...     [6, "Bruce", "Welch", "Shadows"],
...     [7, "Brian", "Bennett", "Shadows"],
... ]
>>> group_members = [Musician(*musician) for musician in musician_lists]

You populate group_members with Musician objects by transforming musician_lists with a list comprehension.

You now have a group_members list that contains seven Musician objects, four from the Beach Boys and three from the Shadows. You’ll next learn how you can use these with attrgetter().

Suppose you wanted to retrieve the .fname attribute from each group_members element:

Python
>>> import operator

>>> get_fname = operator.attrgetter("fname")

>>> for person in group_members:
...     print(get_fname(person))
...
Brian
Carl
Dennis
Bruce
Hank
Bruce
Brian

You first call attrgetter() and specify that its output will get the .fname attribute. The attrgetter() function then returns a function that will get you the .fname attribute of whatever object you pass to it. When you loop over your collection of Musician objects, get_fname() will return the .fname attributes.

The attrgetter() function also allows you to set up a function that can return several attributes at once. Suppose this time you want to return both the .id and .lname attributes of each object:

Python
>>> get_id_lname = operator.attrgetter("id", "lname")

>>> for person in group_members:
...     print(get_id_lname(person))
...
(1, "Wilson")
(2, "Wilson")
(3, "Wilson")
(4, "Johnston")
(5, "Marvin")
(6, "Welch")
(7, "Bennett")

This time, when you call attrgetter() and ask for both the .id and .lname attributes, you create a function capable of reading both attributes for any object. When run, your code returns both .id and .lname from the list of Musician objects passed to it. Of course, you can apply this function to any object, whether built in or custom, as long as the object contains both an .id and an .lname attribute.

Sorting and Searching Lists of Objects by Attribute With attrgetter()

The attrgetter() function also allows you to sort a collection of objects by their attributes. You’ll try this out by sorting the Musician objects in group_members by each object’s .id in reverse order.

First, make sure that you have access to group_members, as defined in the previous section of the tutorial. Then, you can use the power of attrgetter() to perform your custom sort:

Python
>>> get_id = operator.attrgetter("id")
>>> for musician in sorted(group_members, key=get_id, reverse=True):
...     print(musician)
...
Musician(id=7, fname='Brian', lname='Bennett', group='Shadows')
Musician(id=6, fname='Bruce', lname='Welch', group='Shadows')
Musician(id=5, fname='Hank', lname='Marvin', group='Shadows')
Musician(id=4, fname='Bruce', lname='Johnston', group='Beach Boys')
Musician(id=3, fname='Dennis', lname='Wilson', group='Beach Boys')
Musician(id=2, fname='Carl', lname='Wilson', group='Beach Boys')
Musician(id=1, fname='Brian', lname='Wilson', group='Beach Boys')

In this code snippet, you set up an attrgetter() function that can return an .id attribute. To sort the list in reverse by .id, you assign the get_id reference to the sorted() method’s key parameter and set reverse=True. When you print the Musician objects, the output shows that your sort has indeed worked.

If you want to show the object with the highest or lowest .id value, then you use the min() or max() function and pass it a reference to your get_id() function as its key:

Python
>>> min(group_members, key=get_id)
Musician(id=1, fname='Brian', lname='Wilson', group='Beach Boys')
>>> max(group_members, key=get_id)
Musician(id=7, fname='Brian', lname='Bennett', group='Shadows')

You first create an attrgetter() function that can locate an .id attribute. Then you pass it into the min() and max() functions. In this case, your code returns the objects containing the lowest and highest .id attribute values. In this case, those are the objects with .id values of 1 and 7. You might like to experiment with this further by sorting on other attributes.

Calling Methods on Objects With methodcaller()

The final function that you’ll learn about is methodcaller(). It’s conceptually similar to attrgetter(), except it works on methods. To use it, you pass in the name of a method, along with any parameters that the method requires. It’ll return a function that will call the method on any object that you pass to it. The objects passed to methodcaller() don’t need to be of the same type. They only need to contain the method that you’re calling.

To learn about methodcaller(), you first need to enhance your existing Musician data class with a method:

Python
>>> from dataclasses import dataclass

>>> @dataclass
... class Musician:
...     id: int
...     fname: str
...     lname: str
...     group: str
...
...     def get_full_name(self, last_name_first=False):
...         if last_name_first:
...             return f"{self.lname}, {self.fname}"
...         return f"{self.fname} {self.lname}"
...

You add a .get_full_name() method to Musician that accepts a single parameter named last_name_first with a default of False. This allows you to specify the order in which the names are returned.

Suppose you want to call .get_full_name() on each object in the previously defined group_members list:

Python
>>> import operator
>>> first_last = operator.methodcaller("get_full_name")
>>> for person in group_members:
...     print(first_last(person))
...
Brian Wilson
Carl Wilson
Dennis Wilson
Bruce Johnston
Hank Marvin
Bruce Welch

Here you use methodcaller() to create a function named first_last() that will call the .get_full_name() method of any object that you pass to it. Notice that you don’t pass any additional arguments to first_last(), so you receive back a list of the first names followed by the last names of all Musician objects.

If you want the first names to follow the last names, then you can pass in a True value for last_name_first:

Python
>>> last_first = operator.methodcaller("get_full_name", True)
>>> for person in group_members:
...     print(last_first(person))
...
Wilson, Brian
Wilson, Carl
Wilson, Dennis
Johnston, Bruce
Marvin, Hank
Welch, Bruce
Bennett, Brian

This time, you use methodcaller() to create a function named last_first() that will call the .get_full_name() method of any object passed to it, but it’ll also pass True to the last_name_first parameter. Now you receive a list of the last names then the first names of all the Musician objects.

Just like when you’re using attrgetter() to retrieve attributes, objects passed to methodcaller() can be either built in or custom. They only need to contain the method that you want to call.

Key Takeaways

You’re now familiar with the purpose and uses of Python’s operator module. You’ve covered a lot of ground, and here, you’ll find a few questions and answers that sum up the most important concepts that you’ve covered in this tutorial.

You can use these questions to check your understanding or to recap and solidify what you’ve just learned. After each question, you’ll find a brief explanation hidden in a collapsible section. Click the Show/Hide toggle to reveal the answer. Time to dive in!

The operator module’s functions don’t replace Python’s built-in operators. Instead, you use many of the operator-equivalent operator functions in functional programming by passing them to higher-order functions. This isn’t possible with the built-in operators, because you can’t easily obtain a reference to them.

The operator module also provides the higher-order attrgetter(), itemgetter(), and methodcaller() functions. You can pass the functions that these three return to many common functions, including sorted(), max(), and min().

Unlike with built-in operators, you can serialize an operator module’s function, which is great because the pickle module is one of the most efficient ways of saving data. The downside is that it can’t usually cope with functions. You can, however, use it with operator module functions.

Python built-in operators will outperform both the operator module’s versions and lambdas, while the operator functions will outperform lambdas. If you make a single call to a lambda or the operator module equivalent, then you won’t notice much difference. However, when you make repeated calls, the difference becomes significant. The operator module’s functions aren’t replacements for the built-in operators when they’re all you need.

Using the operator functions can make your code more readable than the sometimes-confusing lambda function equivalents.

Do you have an interesting example of using the operator module? If you have a clever use case, why not share with your fellow programmers? Feel free to include it as a comment below.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Ian Eyre

Ian is an avid Pythonista and Real Python contributor who loves to learn and teach others.

» More about Ian

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Tutorial Categories: intermediate data-structures python