Tuples and Sorting

Today, we will discuss the following:

  • A new immutable sequence: tuples

  • Sorting with reverse and a key function

New Immutable Sequence: Tuples

  • Tuples are an immutable sequence of values separated by commas and enclosed within parentheses ( )

  • Tuples support any sequence operations that don’t involve mutation: e.g., len(), indexing, slicing, concatenation, etc

  • Tuples support simple and nifty assignment syntax, which makes them really convenient

# string tuple
names = ('Rohit', 'Jeannie', 'Steve', 'Lida')

# num tuple
primes = (2, 3, 5, 7, 11)

# singleton
num = (5,)

# parens are optional
values = 5, 6

# empty tuple
emp = ()
type(values)
tuple

Tuples as a Sequence

Like strings and lists, we can use sequence operations and functions to manipulate tuples. See examples below.

nameTuple = ('Rohit', 'Jeannie', 'Steve')
len(nameTuple)
3
nameTuple[2]
'Steve'
# Can't do this because tuples are immutable!
nameTuple[2] = 'Lida' 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_14594/1722425898.py in <module>
      1 # Can't do this because tuples are immutable!
----> 2 nameTuple[2] = 'Lida'

TypeError: 'tuple' object does not support item assignment
# concatenation returns a new sequence
nameTuple + ('Lida', ) 
('Rohit', 'Jeannie', 'Steve', 'Lida')
# what will this do?
nameTuple * 2 
('Rohit', 'Jeannie', 'Steve', 'Rohit', 'Jeannie', 'Steve')
numTuple = (1, 1, 2, 3, 5, 8, 13)
# slicing returns a new tuple
numTuple[3:6]
(3, 5, 8)
numTuple[::-1]
(13, 8, 5, 3, 2, 1, 1)
colors = ('red', 'blue', 'orange', 'white', 'black')
# can use in and not in for testing membership
'green' not in colors
True
'Red' in colors
False
# can iterate over tuples using for loop like any other sequence
for c in colors:
    print(c)
red
blue
orange
white
black

Multiple Assignments and Sequence Unpacking with Tuples

Tuples are very useful for:

  • assigning multiple values in a single line

  • simple sequence “unpacking”

  • returning multiple values from functions

a, b = 4, 5
a
4
b
5
# easily swap values 
b, a = a, b 
a, b
(5, 4)
a, b, c = (1, 2, 3)
print(a, b, c)
1 2 3
# will this work? (no, too many values)
a, b = 1, 2, 3 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_14594/4170298029.py in <module>
      1 # will this work? (no, too many values)
----> 2 a, b = 1, 2, 3

ValueError: too many values to unpack (expected 2)
studentInfo = ['Harry Potter', 11, 'Gryffindor']
#name = studentInfo[0]  
#age = studentInfo[1]  
#house = studentInfo[2]

# nifty short hand for three separate assignments (shown above) using a tuple
name, age, house = studentInfo
print(name, age, house)
Harry Potter 11 Gryffindor
name
'Harry Potter'
# multiple return values as a tuple
def arithmetic(num1, num2):
    '''Takes two numbers and returns the sum and product'''
    return num1 + num2, num1 * num2
arithmetic(10, 2)
(12, 20)
type(arithmetic(3, 4))
tuple

Conversion between Sequences

We can convert between str, list, range, and tuple types by using the corresponding functions.

word = "Williamstown"
# create list of characters
charList = list(word)
charList
['W', 'i', 'l', 'l', 'i', 'a', 'm', 's', 't', 'o', 'w', 'n']
# create tuple of characters
charTuple = tuple(charList)
charTuple
('W', 'i', 'l', 'l', 'i', 'a', 'm', 's', 't', 'o', 'w', 'n')
# tuple to list
list((1, 2, 3, 4, 5)) 
[1, 2, 3, 4, 5]
numRange = range(len(word))
# range to list
list(numRange)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
# list to string
str(list(numRange))
'[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]'
# tuple to string
str(('hello', 'world'))
"('hello', 'world')"

Sorting Sequences

We have talked about the special sort() method for lists that takes advantage of list mutability to sort lists in place. sort() is a methond and only works for lists!

The sorted() function that takes a sequence and returns a new sorted sequence as a list.

By default, sorted() sorts the sequence in ascending order (for numbers) and alphabetical (dictionary) order for strings.

Furthermore, strings are sorted based on the ASCII value of their characters: special characters come before capital letters, which come before lower-case letters.

IMPORTANT sorted() does not alter the sequence it is called on. It returns always returns a new list.

# calling sorted() on a tuple returns a sorted list
nums = (42, -20, 13, 10, 0, 11, 18)
sorted(nums)
[-20, 0, 10, 11, 13, 18, 42]
letters = ('a', 'c', 'e', 'p', 'z')
sorted(letters)
['a', 'c', 'e', 'p', 'z']
# sorted() will sort the characters in the string and return a list
sorted("Rohit")
['R', 'h', 'i', 'o', 't']
sorted("Jeannie")
['J', 'a', 'e', 'e', 'i', 'n', 'n']
sorted("*hello! world!*")
[' ', '!', '!', '*', '*', 'd', 'e', 'h', 'l', 'l', 'l', 'o', 'o', 'r', 'w']
# this will sort the string, create a new list, and then turn the list into a string
''.join(sorted("*hello! world!*"))
' !!**dehllloorw'

ASCII values of characters

Strings are sorted based on the ASCII values of their characters. We can investigate these values using ord() and chr().

# ord() returns the ASCII value (int) of a string
ord('a')
97
ord('A')
65
ord('!')
33
# chr returns the string for a given ASCII value (int)
chr(33)
'!'
chr(65)
'A'
chr(97)
'a'

Sorting Tuples and More

Recall the sorted() function that takes a sequence and returns a new sorted sequence as a list.

By default, sorted() function on a sequence containing tuples has the following behavior:

  • Sorts tuples by first item of each tuple

  • If there is a tie (e.g., two tuples have the same first item), it sorts them by comparing their second item, so on.

This sorting behavior is pretty standard and is referred to as lexigraphical sorting.

Note: sorted() also behaves this way with lists and lists of lists.

fruits = [('12', 'apples'), ('kiwis', '5'), ('4', 'bananas'), ('27', 'grapes')]
sorted(fruits)
[('12', 'apples'), ('27', 'grapes'), ('4', 'bananas'), ('kiwis', '5')]
pairs = [('4', '5'), ('0', '2'), ('12', '1'), ('11', '3')]
sorted(pairs)
[('0', '2'), ('11', '3'), ('12', '1'), ('4', '5')]
pairs
[('4', '5'), ('0', '2'), ('12', '1'), ('11', '3')]
triples = [(1, 2, 3), (1, 3, 2), (2, 2, 1), (1, 2, 1)]
sorted(triples)
[(1, 2, 1), (1, 2, 3), (1, 3, 2), (2, 2, 1)]
characters = [(8, 'a', '$'), (7, 'c', '@'),
              (7, 'b', '+'), (8, 'a', '!')] 

sorted(characters)
[(7, 'b', '+'), (7, 'c', '@'), (8, 'a', '!'), (8, 'a', '$')]
sorted((4,5,1,2))
[1, 2, 4, 5]

Changing the Default Sorting Behavior

Sometimes we may want to sort based on the second item in a tuple, or perhaps in descending order. We can tell Python how to sort and override the default sequence sorting behavior. To do so, let us explore the sorted() function and its optional arguments.

help(sorted)
Help on built-in function sorted in module builtins:

sorted(iterable, /, *, key=None, reverse=False)
    Return a new list containing all items from the iterable in ascending order.
    
    A custom key function can be supplied to customize the sort order, and the
    reverse flag can be set to request the result in descending order.

Sorting using reverse

sorted([8, 2, 3, 1, 3, 1, 2], reverse=True)
[8, 3, 3, 2, 2, 1, 1]
sorted(['a', 'c', 'e', 'p', 'z'], reverse=True)
['z', 'p', 'e', 'c', 'a']
fruits = [(12, 'apples'), (5, 'kiwis'), (4, 'bananas'), (27, 'grapes')]
sorted(fruits, reverse=True)
[(27, 'grapes'), (12, 'apples'), (5, 'kiwis'), (4, 'bananas')]

Sorting using key function

Now suppose we have a list of tuples that we want to sort by something other than the first item.

For example:

  • Consider a list of tuples, where the first item is a course name, second item is the cap, and third item is the term.

  • Say we want to sort these courses by their capacity: courses with higher capacity should come first.

We can accomplish this by supplying the sorted() function with a key function that tells it how to compare the tuples to each other.

courses = [('CS134', 74, 'Spring'), ('CS136', 60, 'Spring'),
           ('AFR206', 30, 'Spring'), ('ECON233', 30, 'Fall'),
           ('MUS112', 10, 'Fall'), ('STAT200', 50, 'Spring'), 
           ('PSYC201', 50, 'Fall'), ('MATH110', 74, 'Spring')]
# by default, Python sorts by first element in tuples
sorted(courses) 
[('AFR206', 30, 'Spring'),
 ('CS134', 74, 'Spring'),
 ('CS136', 60, 'Spring'),
 ('ECON233', 30, 'Fall'),
 ('MATH110', 74, 'Spring'),
 ('MUS112', 10, 'Fall'),
 ('PSYC201', 50, 'Fall'),
 ('STAT200', 50, 'Spring')]
def capacity(courseTuple):
    '''Takes a sequence and returns item at index 1'''
    return courseTuple[1]
# we can tell sorted() to sort by capacity instead
sorted(courses, key=capacity)
[('MUS112', 10, 'Fall'),
 ('AFR206', 30, 'Spring'),
 ('ECON233', 30, 'Fall'),
 ('STAT200', 50, 'Spring'),
 ('PSYC201', 50, 'Fall'),
 ('CS136', 60, 'Spring'),
 ('CS134', 74, 'Spring'),
 ('MATH110', 74, 'Spring')]
# we can also combine the key and reverse parameters
# to sort by capacity in descending order
sorted(courses, key=capacity, reverse=True)
[('CS134', 74, 'Spring'),
 ('MATH110', 74, 'Spring'),
 ('CS136', 60, 'Spring'),
 ('STAT200', 50, 'Spring'),
 ('PSYC201', 50, 'Fall'),
 ('AFR206', 30, 'Spring'),
 ('ECON233', 30, 'Fall'),
 ('MUS112', 10, 'Fall')]