Lesson 3

Overview

  1. Tricks for functions
  • Lambda functions
  • * and **
  • Composing functions
  • Default argument values
  1. Tricks for lists
  • List comprehension
  • sorted()
  • zip()
  • any() and all()
  1. Tricks for strings
  • lower()
  • split()
  • strip()
  • startswith()
  1. Type casting

  2. PA2 Review

1. Tricks for functions

1.1 Lambda functions

Shorthand functions for simple tasks. Especially useful for sorting (will discuss soon).

def sum_a_b_sq(a, b):
    return a + b ** 2
sum_a_b_sq = lambda a, b: a + b ** 2
print(sum_a_b_sq(2, 4))
18

1.2 * and **

* unpacks a list - especially useful for function arguments

** unpacks a dictionary - especially useful for function arguments

Unpacking a list inside another list seems impossible at first

[1, 2, [3, 4], 5]
[1, 2, [3, 4], 5]

It is possible with *

[1, 2, *[3, 4], 5]
[1, 2, 3, 4, 5]

Unpacking a dictionary inside another dictionary seems impossible at first

{'a': 1, 'b': 2, {'c': 3, 'd': 4}}
SyntaxError: ':' expected after dictionary key (192525557.py, line 1)

It is possible with **

{'a': 1, 'b': 2, **{'c': 3, 'd': 4}}
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

1.3 Composing functions

Apply multiple functions in one line

a_sq_b_cubed = lambda a, b: (a ** 2, b ** 3)
print(sum_a_b_sq(*a_sq_b_cubed(4, 12)))
2986000

Without the *, the two function outputs are treated as one argument (since they are combined into a tuple)

print(sum_a_b_sq(a_sq_b_cubed(4, 12)))
TypeError: <lambda>() missing 1 required positional argument: 'b'
a_sq_b_cubed(4, 12)
(16, 1728)

The * notation works only inside function calls

*a_sq_b_cubed(4, 12)
SyntaxError: can't use starred expression here (2410165554.py, line 1)

1.4 Default argument values

def print_name(first_name, last_name, middle_name=''):
    print(first_name, middle_name, last_name)

print_name('Adam', 'Oppenheimer', 'Alexander')
print_name('Adam', 'Alexander', 'Oppenheimer')
Adam Alexander Oppenheimer
Adam Oppenheimer Alexander
print_name(first_name='Adam', middle_name='Alexander', last_name='Oppenheimer')
Adam Alexander Oppenheimer

We can use ** notation in functions

print_name(**{'first_name': 'Adam', 'middle_name': 'Alexander', 'last_name': 'Oppenheimer'})
Adam Alexander Oppenheimer
name_dict = {'first_name': 'Adam', 'middle_name': 'Alexander', 'last_name': 'Oppenheimer'}
def remove_middle_name(name_dict):
    name_dict['middle_name'] = ''
remove_middle_name(name_dict)
print_name(**name_dict)
Adam  Oppenheimer

Once an argument has a default value, all following arguments must also have default values

def print_name(first_name, middle_name='', last_name):
    print(first_name, middle_name, last_name)
SyntaxError: parameter without a default follows parameter with a default (4160617308.py, line 1)

In-place updates to default values are maintained between function calls - this can lead to big headaches!

def append_0(a=[]):
    a.append(0)
    print('a:', a)

append_0()
append_0()
a: [0]
a: [0, 0]

Avoid this issue by setting the default value to None if you know the argument can have in-place updates (unless this is the behavior you want, although it is extremely unlikely that you want this to happen)

def append_0(a=None):
    if a is None:
        a = []
    a.append(0)
    print('a:', a)

append_0()
append_0()
a: [0]
a: [0]

2. Tricks for lists

2.1 List comprehension

Super convenient way to create lists (also works for dictionaries)

lst = []
for a in range(5):
    lst.append(a)
print(lst)
[0, 1, 2, 3, 4]
[a for a in range(5)]
[0, 1, 2, 3, 4]
[a if (a % 2 == 0) else 1 / a for a in range(5)]
[0, 1.0, 2, 0.3333333333333333, 4]
[a for a in range(5) if a != 3]
[0, 1, 2, 4]
[a if (a % 2 == 0) else 1 / a for a in range(5) if a != 2]
[0, 1.0, 0.3333333333333333, 4]

This is much more concise than the for-loop version

lst = []
for a in range(5):
    if a != 2:
        if a % 2 == 0:
            lst.append(a)
        else:
            lst.append(1 / a)
print(lst)
[0, 1.0, 0.3333333333333333, 4]

Can even nest list comprehensions. Notice that you order the for loops in the same order as if you were writing out the full loops.

[(i, j) for i in range(3) for j in range(2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
for i in range(3):
    for j in range(2):
        print(i, j)
0 0
0 1
1 0
1 1
2 0
2 1
[(i, j) for i in range(3) for j in range(i)]
[(1, 0), (2, 0), (2, 1)]

Be careful about some weird behavior with list comprehensions (this can also happen with regular loops) (see https://stackoverflow.com/questions/3431676/creating-functions-or-lambdas-in-a-loop-or-comprehension).

2.2 sorted()

Sort a list and specify the key. Notice the lambda functions to set the key for sorting.

sorted([4, 1, 8])
[1, 4, 8]
def get_first(a):
    return a[0]
def get_second(a):
    return a[1]
sorted([('d', 4), ('a', 1), ('r', 2)], key=get_first)
[('a', 1), ('d', 4), ('r', 2)]
sorted([('d', 4), ('a', 1), ('r', 2)], key=lambda a: a[0])
[('a', 1), ('d', 4), ('r', 2)]
sorted([('d', 4), ('a', 1), ('r', 2)], key=lambda a: a[1])
[('a', 1), ('r', 2), ('d', 4)]

2.3 zip()

Combine two lists element by element. Make sure to convert to a list for PA3.

list(zip(range(0, 5), range(5, 0, -1)))
[(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]

Make sure the lists are the same length! They’ll zip even if they aren’t!

list(zip(range(0, 7), range(5, 0, -1)))
[(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]

Note: zip creates a generator, which is why we need to convert it to a list

zipped_lst = zip(range(0, 5), range(5, 0, -1))
for elmt in zipped_lst:
    print(elmt)
    break
for elmt in zipped_lst:
    print(elmt)
    break
print(zipped_lst[0])
(0, 5)
(1, 4)
TypeError: 'zip' object is not subscriptable

2.4 any() and all()

any(): check whether any element of a list is True.

all(): check whether all elements of a list are True.

any([True, True, False])
True
any([False, False, False])
False
all([True, True, False])
False
all([True, True, True])
True

3. Tricks for strings

3.1 lower()

Convert text to lowercase

'HELLO'.lower()
'hello'

3.2 split()

Split string into a list, dividing words by the specified character

'Hello, I am Adam.'.split(' ')
['Hello,', 'I', 'am', 'Adam.']
'Hello, I am Adam.'.split('m')
['Hello, I a', ' Ada', '.']

3.3 strip()

Remove the specified characters from the start and end of a string

'abcdefgabcdefg'.strip('bacdg')
'efgabcdef'

3.4 startswith()

Check whether the string starts with the specified string

'hello'.startswith('he')
True
'hello'.startswith('hen')
False

4. Type casting

Convert between types by writing the name of the type you want to convert to and using the name as if it is a function

val = 1.0
print(val)
print(int(val))
1.0
1
lst = [1, 2, 3]
print(lst)
print(tuple(lst))
[1, 2, 3]
(1, 2, 3)
print(zip(range(0, 5), range(5, 0, -1)))
print(list(zip(range(0, 5), range(5, 0, -1))))
<zip object at 0x1079a6200>
[(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]

5. PA2 review

Discussion and review my solution