Operations on Sequences

Mutating a list

For list (but not tuple), subscription and slicing can also be used as the target of an assignment operation to mutate the list.

%%mytutor -h 300
b = [*range(10)]  # aliasing
b[::2] = b[:5]
b[0:1] = b[:5]
b[::2] = b[:5]  # fails

Last assignment fails because [::2] with step size not equal to 1 is an extended slice, which can only be assigned to a list of equal size.

What is the difference between mutation and aliasing?

In the previous code:

  • The first assignment b = [*range(10)] is aliasing, which gives the list the target name/identifier b.

  • Other assignments such as b[::2] = b[:5] are mutations that calls __setitem__ because the target b[::2] is not an identifier.

Exercise Explain the outcome of the following checks of equivalence?

%%mytutor -h 400
a = [10, 20, 30, 40]
b = a
print('a is b? {}'.format(a is b))
print('{} == {}? {}'.format(a, b, a == b))
b[1:3] = b[2:0:-1]
print('{} == {}? {}'.format(a, b, a == b))
  • a is b and a == b returns True because the assignment b = a makes b an alias of the same object a points to.

  • In particular, the operationb[1:3] = b[2:0:-1] affects the same list a points to.

Why mutate a list?

The following is another implementation of composite_sequence that takes advantage of the mutability of list.

def sieve_composite_sequence(stop):
    is_composite = [False] * stop  # initialization
    for factor in range(2,stop):
        if is_composite[factor]: continue
        for multiple in range(factor*2,stop,factor):
            is_composite[multiple] = True
    return (x for x in range(4,stop) if is_composite[x])

for x in sieve_composite_sequence(100): print(x, end=' ')
4 6 8 9 10 12 14 15 16 18 20 21 22 24 25 26 27 28 30 32 33 34 35 36 38 39 40 42 44 45 46 48 49 50 51 52 54 55 56 57 58 60 62 63 64 65 66 68 69 70 72 74 75 76 77 78 80 81 82 84 85 86 87 88 90 91 92 93 94 95 96 98 99 

The algorithm

  1. changes is_composite[x] from False to True if x is a multiple of a smaller number factor, and

  2. returns a generator that generates composite numbers according to is_composite.

Exercise Is sieve_composite_sequence more efficient than your solution composite_sequence? Why?

for x in composite_sequence(10000): pass
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-5-1eba4e6d3969> in <module>
----> 1 for x in composite_sequence(10000): pass

NameError: name 'composite_sequence' is not defined
for x in sieve_composite_sequence(1000000): pass

The line if is_composite[factor]: continue avoids the redundant computations of checking composite factors.

Exercise Note that the multiplication operation * is the most efficient way to initialize a 1D list with a specified size, but we should not use it to initialize a 2D list. Fix the following code so that a becomes [[1, 0], [0, 1]].

%%mytutor -h 250
a = [[0] * 2] * 2
a[0][0] = a[1][1] = 1
print(a)
### BEGIN SOLUTION
a = [[0] * 2 for i in range(2)]
### END SOLUTION
a[0][0] = a[1][1] = 1
print(a)
[[1, 0], [0, 1]]

Different methods to operate on a sequence

Recall the quicksort algorithm:

def quicksort(seq):
    '''Return a sorted list of items from seq.'''
    if len(seq) <= 1:
        return list(seq)
    i = random.randint(0, len(seq) - 1)
    pivot, others = seq[i], [*seq[:i], *seq[i + 1:]]
    left = quicksort([x for x in others if x < pivot])
    right = quicksort([x for x in others if x >= pivot])
    return [*left, pivot, *right]


seq = [random.randint(0, 99) for i in range(10)]
print(seq, quicksort(seq), sep='\n')
[61, 69, 90, 96, 7, 78, 35, 63, 15, 24]
[7, 15, 24, 35, 61, 63, 69, 78, 90, 96]

There is also a built-in function sorted for sorting a sequence:

sorted?
sorted(seq)
[7, 15, 24, 35, 61, 63, 69, 78, 90, 96]

Is quicksort quicker?

%%timeit
quicksort(seq)
12.1 µs ± 45.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%%timeit
sorted(seq)
209 ns ± 0.414 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Python implements the Timsort algorithm, which is very efficient.

What are other operations on sequences?

The following compares the lists of public attributes for tuple and list.

list_attributes = dir(list)
tuple_attributes = dir(tuple)

print(
    'Common attributes:', ', '.join([
        attr for attr in list_attributes
        if attr in tuple_attributes and attr[0] != '_'
    ]))

print(
    'Tuple-specific attributes:', ', '.join([
        attr for attr in tuple_attributes
        if attr not in list_attributes and attr[0] != '_'
    ]))

print(
    'List-specific attributes:', ', '.join([
        attr for attr in list_attributes
        if attr not in tuple_attributes and attr[0] != '_'
    ]))
Common attributes: count, index
Tuple-specific attributes: 
List-specific attributes: append, clear, copy, extend, insert, pop, remove, reverse, sort
  • There are no public tuple-specific attributes, and

  • all the list-specific attributes are methods that mutate the list, except copy.

The common attributes

  • count method returns the number of occurrences of a value in a tuple/list, and

  • index method returns the index of the first occurrence of a value in a tuple/list.

%%mytutor -h 300
a = (1,2,2,4,5)
print(a.index(2))
print(a.count(2))

reverse method reverses the list instead of returning a reversed list.

%%mytutor -h 300
a = [*range(10)]
print(reversed(a))
print(*reversed(a))
print(a.reverse())
  • copy method returns a copy of a list.

  • tuple does not have the copy method but it is easy to create a copy by slicing.

%%mytutor -h 400
a = [*range(10)]
b = tuple(a)
a_reversed = a.copy()
a_reversed.reverse()
b_reversed = b[::-1]

sort method sorts the list in place instead of returning a sorted list.

%%mytutor -h 300
import random
a = [random.randint(0,10) for i in range(10)]
print(sorted(a))
print(a.sort())
  • extend method that extends a list instead of creating a new concatenated list.

  • append method adds an object to the end of a list.

  • insert method insert an object to a specified location.

%%mytutor -h 300
a = b = [*range(5)]
print(a + b)
print(a.extend(b))
print(a.append('stop'))
print(a.insert(0,'start'))
  • pop method deletes and return the last item of the list.

  • remove method removes the first occurrence of a value in the list.

  • clear method clears the entire list.

We can also use the function del to delete a selection of a list.

%%mytutor -h 300
a = [*range(10)]
del a[::2]
print(a.pop())
print(a.remove(5))
print(a.clear())