0% found this document useful (0 votes)
23 views

Working With The Python Operator Module

Uploaded by

Garuma Abdisa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
23 views

Working With The Python Operator Module

Uploaded by

Garuma Abdisa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 17

Working With the Python operator Module

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


Tweet Share Email

Table of Contents

 Using the Python operator Module’s Basic Functions


o Learning How the Basic Functions Work
o Passing Operators as Arguments Into Higher-Order Functions
o Serializing operator Module Functions
o Investigating operator Function Performance Against the Alternatives
 Using the Python operator Module’s Higher-Order Functions
o Selecting Values From Multidimensional Collections With itemgetter()
o Sorting Multidimensional Collections With itemgetter()
o Retrieving Attributes From Objects With attrgetter()
o Sorting and Searching Lists of Objects by Attribute With attrgetter()
o Calling Methods on Objects With methodcaller()
 Key Takeaways

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.

Get Your Code: Click here to download the free sample code that shows you how to use
Python’s operator module.
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.

Note: Most of the operator module functions contain two names, a dunder version and a
without-dunder version. In the previous example, operator.__add__(5, 3) is the dunder
version because it includes double underscores. From this point forward, you’ll use only the
without-dunder versions, such as operator.add(5, 3). The dunder versions are for backward
compatibility with the Python 2 version of operator.

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

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

Note: Python’s timeit module allows you to time small pieces of your code. You usually
invoke timeit from the command line using python -m timeit followed by a string containing
the command that you want to measure. You use the -s switch to indicate code that you want to
run once just before the timing begins. In the example above, you used -s to import pow() and
add() from the operator module before timing your code.

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.

Note: You may be wondering where .__init__() has gone. One of the benefits of using a data
class is that there’s no longer a need for an explicit initializer. To create an object, you pass in
values for each of the attributes in the class. In the Musician class above, the first attribute gets
assigned to .id, the second to .fname, and so on.

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.
Note: You may have noticed that you used *musician to pass each list when creating class
objects. The asterisk tells Python to unpack the list into its individual elements when creating the
objects. In other words, the first object will have an .id attribute of 1, an .fname attribute of
"Brian", and so on.

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!

You might also like