# Working With the Python operator Module

by Ian Eyre Aug 02, 2023

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:

>>>
``````>>> 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:

>>>
``````>>> 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 `if``elif` statement as follows:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> import operator
>>> operators = {
...     "-": 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:

>>>
``````>>> 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:

>>>
``````>>> 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`:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

``````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:

>>>
``````>>> 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`:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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`:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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`:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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`:

>>>
``````>>> 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:

>>>
``````>>> 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:

>>>
``````>>> 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`:

>>>
``````>>> 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.

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

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

What Do You Think?