Beyond .reverse() and reversed() – Real Python


Sometimes you need to process Python lists starting from the last element down to the first—in other words, in reverse order. In general, there are two main challenges related to working with lists in reverse:

To meet the first challenge, you can use either .reverse() or a loop that swaps items by index. For the second, you can use reversed() or a slicing operation. In the next sections, you’ll learn about different ways to accomplish both in your code.

Reversing Lists in Place

Like other mutable sequence types, Python lists implement .reverse(). This method reverses the underlying list in place for memory efficiency when you’re reversing large list objects. Here’s how you can use .reverse():

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> digits.reverse()
>>> digits
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

When you call .reverse() on an existing list, the method reverses it in place. This way, when you access the list again, you get it in reverse order. Note that .reverse() doesn’t return a new list but None:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> reversed_digits = digits.reverse()
>>> reversed_digits is None
True

Trying to assign the return value of .reverse() to a variable is a common mistake related to using this method. The intent of returning None is to remind its users that .reverse() operates by side effect, changing the underlying list.

Okay! That was quick and straightforward! Now, how can you reverse a list in place by hand? A common technique is to loop through the first half of it while swapping each element with its mirror counterpart on the second half of the list.

Python provides zero-based positive indices to walk sequences from left to right. It also allows you to navigate sequences from right to left using negative indices:

Python list with indices

This diagram shows that you can access the first element of the list (or sequence) using either 0 or -5 with the indexing operator, like in sequence[0] and sequence[-5], respectively. You can use this Python feature to reverse the underlying sequence in place.

For example, to reverse the list represented in the diagram, you can loop over the first half of the list and swap the element at index 0 with its mirror at index -1 in the first iteration. Then you can switch the element at index 1 with its mirror at index -2 and so on until you get the list reversed.

Here’s a representation of the whole process:

Reverse Lists in Python

To translate this process into code, you can use a for loop with a range object over the first half of the list, which you can get with len(digits) // 2. Then you can use a parallel assignment statement to swap the elements, like this:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> for i in range(len(digits) // 2):
...     digits[i], digits[-1 - i] = digits[-1 - i], digits[i]
...

>>> digits
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

This loop iterates through a range object that goes from 0 to len(digits) // 2. Each iteration swaps an item from the first half of the list with its mirror counterpart in the second half. The expression -1 - i inside the indexing operator, [], guarantees access to the mirror item. You can also use the expression -1 * (i + 1) to provide the corresponding mirror index.

Besides the above algorithm, which takes advantage of index substitution, there are a few different ways to reverse lists by hand. For example, you can use .pop() and .insert() like this:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> for i in range(len(digits)):
...     last_item = digits.pop()
...     digits.insert(i, last_item)
...

>>> digits
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In the loop, you call .pop() on the original list without arguments. This call removes and returns the last item in the list, so you can store it in last_item. Then .insert() moves last_item to the position at index i.

For example, the first iteration removes 9 from the right end of the list and stores it in last_item. Then it inserts 9 at index 0. The next iteration takes 8 and moves it to index 1, and so on. At the end of the loop, you get the list reversed in place.

Creating Reversed Lists

If you want to create a reversed copy of an existing list in Python, then you can use reversed(). With a list as an argument, reversed() returns an iterator that yields items in reverse order:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> reversed_digits = reversed(digits)
>>> reversed_digits
<list_reverseiterator object at 0x7fca9999e790>

>>> list(reversed_digits)
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In this example, you call reversed() with digits as an argument. Then you store the resulting iterator in reversed_digits. The call to list() consumes the iterator and returns a new list containing the same items as digits but in reverse order.

An important point to note when you’re using reversed() is that it doesn’t create a copy of the input list, so changes on it affect the resulting iterator:

>>>

>>> fruits = ["apple", "banana", "orange"]

>>> reversed_fruit = reversed(fruits)  # Get the iterator
>>> fruits[-1] = "kiwi"  # Modify the last item
>>> next(reversed_fruit)  # The iterator sees the change
'kiwi'

In this example, you call reversed() to get the corresponding iterator over the items in fruits. Then you modify the last fruit. This change affects the iterator. You can confirm that by calling next() to get the first item in reversed_fruit.

If you need to get a copy of fruits using reversed(), then you can call list():

>>>

>>> fruits = ["apple", "banana", "orange"]

>>> list(reversed(fruits))
['orange', 'banana', 'apple']

As you already know, the call to list() consumes the iterator that results from calling reversed(). This way, you create a new list as a reversed copy of the original one.

Python 2.4 added reversed(), a universal tool to facilitate reverse iteration over sequences, as stated in PEP 322. In general, reversed() can take any objects that implement a .__reversed__() method or that support the sequence protocol, consisting of the .__len__() and .__getitem__() special methods. So, reversed() isn’t limited to lists:

>>>

>>> list(reversed(range(10)))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

>>> list(reversed("Python"))
['n', 'o', 'h', 't', 'y', 'P']

Here, instead of a list, you pass a range object and a string as arguments to reversed(). The function does its job as expected, and you get a reversed version of the input data.

Another important point to highlight is that you can’t use reversed() with arbitrary iterators:

>>>

>>> digits = iter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

>>> reversed(digits)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list_iterator' object is not reversible

In this example, iter() builds an iterator over your list of numbers. When you call reversed() on digits, you get a TypeError.

Iterators implement the .__next__() special method to walk through the underlying data. They’re also expected to implement the .__iter__() special method to return the current iterator instance. However, they’re not expected to implement either .__reversed__() or the sequence protocol. So, reversed() doesn’t work for them. If you ever need to reverse an iterator like this, then you should first convert it to a list using list().

Another point to note is that you can’t use reversed() with unordered iterables:

>>>

>>> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

>>> reversed(digits)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object is not reversible

In this example, when you try to use reversed() with a set object, you get a TypeError. This is because sets don’t keep their items ordered, so Python doesn’t know how to reverse them.

Reversing Lists Through Slicing

Since Python 1.4, the slicing syntax has had a third argument, called step. However, that syntax initially didn’t work on built-in types, such as lists, tuples, and strings. Python 2.3 extended the syntax to built-in types, so you can use step with them now. Here’s the full-blown slicing syntax:

This syntax allows you to extract all the items in a_list from start to stop − 1 by step. The third offset, step, defaults to 1, which is why a normal slicing operation extracts the items from left to right:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> digits[1:5]
[1, 2, 3, 4]

With [1:5], you get the items from index 1 to index 5 - 1. The item with the index equal to stop is never included in the final result. This slicing returns all the items in the target range because step defaults to 1.

If you use a different step, then the slicing jumps as many items as the value of step:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> digits[0::2]
[0, 2, 4, 6, 8]

>>> digits[::3]
[0, 3, 6, 9]

In the first example, [0::2] extracts all items from index 0 to the end of digits, jumping over two items each time. In the second example, the slicing jumps 3 items as it goes. If you don’t provide values to start and stop, then they are set to 0 and to the length of the target sequence, respectively.

If you set step to -1, then you get a slice with the items in reverse order:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> # Set step to -1
>>> digits[len(digits) - 1::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

>>> digits
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

This slicing returns all the items from the right end of the list (len(digits) - 1) back to the left end because you omit the second offset. The rest of the magic in this example comes from using a value of -1 for step. When you run this trick, you get a copy of the original list in reverse order without affecting the input data.

If you fully rely on implicit offsets, then the slicing syntax gets shorter, cleaner, and less error-prone:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> # Rely on default offset values
>>> digits[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Here, you ask Python to give you the complete list ([::-1]) but going over all the items from back to front by setting step to -1. This is pretty neat, but reversed() is more efficient in terms of execution time and memory usage. It’s also more readable and explicit. So these are points to consider in your code.

Another technique to create a reversed copy of an existing list is to use slice(). The signature of this built-in function is like this:

This function works similarly to the indexing operator. It takes three arguments with similar meaning to those used in the slicing operator and returns a slice object representing the set of indices returned by range(start, stop, step). That sounds complicated, so here are some examples of how slice() works:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> slice(0, len(digits))
slice(0, 10, None)

>>> digits[slice(0, len(digits))]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> slice(len(digits) - 1, None, -1)
slice(9, None, -1)

>>> digits[slice(len(digits) - 1, None, -1)]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

The first call to slice() is equivalent to [0:len(digits)]. The second call works the same as [len(digits) - 1::-1]. You can also emulate the slicing [::-1] using slice(None, None, -1). In this case, passing None to start and stop means that you want a slice from the beginning to the end of the target sequence.

Here’s how you can use slice() to create a reversed copy of an existing list:

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> digits[slice(None, None, -1)]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

The slice object extracts all the items from digits, starting from the right end back to the left end, and returns a reversed copy of the target list.



Source link

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top