for loops and ways to avoid for loops

range (similar to colon in Matlab)

To get documentation on an object in Python, use help.

help(range)
Help on class range in module builtins:

class range(object)
 |  range(stop) -> range object
 |  range(start, stop[, step]) -> range object
 |  
 |  Return an object that produces a sequence of integers from start (inclusive)
 |  to stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.
 |  start defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.
 |  These are exactly the valid indices for a list of 4 elements.
 |  When step is given, it specifies the increment (or decrement).
 |  
 |  Methods defined here:
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __contains__(self, key, /)
 |      Return key in self.
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(self, key, /)
 |      Return self[key].
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __reduce__(...)
 |      Helper for pickle.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __reversed__(...)
 |      Return a reverse iterator.
 |  
 |  count(...)
 |      rangeobject.count(value) -> integer -- return number of occurrences of value
 |  
 |  index(...)
 |      rangeobject.index(value) -> integer -- return index of value.
 |      Raise ValueError if the value is not present.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  start
 |  
 |  step
 |  
 |  stop
r = range(0,10)

I think of range as creating something like a list, but the thing that’s created is not actually a list, it is a range object.

type(r)
range

We can convert it to a list by wrapping it inside list. Notice that the right endpoint 10 is not included.

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

The following is similar to 0:3:99 in Matlab.

s = range(0,100,3)
list(s)
[0,
 3,
 6,
 9,
 12,
 15,
 18,
 21,
 24,
 27,
 30,
 33,
 36,
 39,
 42,
 45,
 48,
 51,
 54,
 57,
 60,
 63,
 66,
 69,
 72,
 75,
 78,
 81,
 84,
 87,
 90,
 93,
 96,
 99]

You are only allowed to use ints when creating a range object.

range(0,1,0.1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/8j/gshrlmtn7dg4qtztj4d4t_w40000gn/T/ipykernel_90933/361496858.py in <module>
----> 1 range(0,1,0.1)

TypeError: 'float' object cannot be interpreted as an integer

If you don’t specify two input arguments, then the range object will start at 0.

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

for loops

range objects might seem kind of specialized, but they are very useful when creating a for loop. For example, here we print the word “hi” five times.

for i in range(5):
    print("hi")
hi
hi
hi
hi
hi

A convention (that I usually won’t use) is to use _ for the variable name if it’s never getting referred to again.

for _ in range(5):
    print("hi")
hi
hi
hi
hi
hi

The indentation is important. Pay attention to the difference between the following two examples. The indented part is what is repeated in the for loop, and the unindented part happens after the for loop is finished.

for i in range(5):
    print("hi")
print(i)
hi
hi
hi
hi
hi
4
for i in range(5):
    print("hi")
    print(i)
hi
0
hi
1
hi
2
hi
3
hi
4

Example

Let’s try to make the list ["lecture3", "lecture5", ..., "lecture41"].

You aren’t allowed to reference non-existent entries in a list. For example, in the following we’ve made an empty list, so we are not allowed to refer to its 2nd entry.

my_list = []
my_list[2] = "lecture7"
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/var/folders/8j/gshrlmtn7dg4qtztj4d4t_w40000gn/T/ipykernel_90933/3788743490.py in <module>
      1 my_list = []
----> 2 my_list[2] = "lecture7"

IndexError: list assignment index out of range

Instead we will start with an empty list, and then gradually append elements onto it.

my_list = []
my_list.append("lecture3")
my_list
['lecture3']
my_list.append('lecture5')
my_list
['lecture3', 'lecture5']
my_list = []
for i in range(3,43,2):
    my_list.append(i)
my_list
[3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41]

We just need to combine these ideas.

my_list = []
for i in range(3,43,2):
    my_list.append("lecture"+i)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/8j/gshrlmtn7dg4qtztj4d4t_w40000gn/T/ipykernel_90933/2841512558.py in <module>
      1 my_list = []
      2 for i in range(3,43,2):
----> 3     my_list.append("lecture"+i)

TypeError: can only concatenate str (not "int") to str

We’re not allowed to use + between a string like “lecture” and an integer like i.

type("lecture")
str

We convert i into a string by wrapping it in str. This is just like how we converted a range object into a list by wrapping it in list.

my_list = []
for i in range(3,43,2):
    my_list.append("lecture"+str(i))
my_list
['lecture3',
 'lecture5',
 'lecture7',
 'lecture9',
 'lecture11',
 'lecture13',
 'lecture15',
 'lecture17',
 'lecture19',
 'lecture21',
 'lecture23',
 'lecture25',
 'lecture27',
 'lecture29',
 'lecture31',
 'lecture33',
 'lecture35',
 'lecture37',
 'lecture39',
 'lecture41']

Miscellaneous comments

(Here are a few miscellaneous comments. We get back to the main lecture below.)

A list is allowed to have different data types in it. For example, the following list has an int, a str, and a list in it.

[2,"hi",[5,10]]
[2, 'hi', [5, 10]]

The error above was from trying to use + between a string and an integer. We are sometimes allowed to use + between different data types. Here we add an int and a float.

4+1.1
5.1
"hi"+4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/8j/gshrlmtn7dg4qtztj4d4t_w40000gn/T/ipykernel_90933/349616457.py in <module>
----> 1 "hi"+4

TypeError: can only concatenate str (not "int") to str

You can’t add an int to a list (but see further below for a way to add 1 to each entry in a list).

[2,3,5]+1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/8j/gshrlmtn7dg4qtztj4d4t_w40000gn/T/ipykernel_90933/897864291.py in <module>
----> 1 [2,3,5]+1

TypeError: can only concatenate list (not "int") to list

You can use + between two lists. In this case, it concatenates them.

[2,3,5]+[5,100,5]
[2, 3, 5, 5, 100, 5]

Introduction to list comprehension

We’re going to improve the following code by writing it with list comprehension.

my_list = []
for i in range(3,43,2):
    my_list.append("lecture"+str(i))

Here is the list comprehension way of making the same list.

my_list2 = ["lecture"+str(i) for i in range(3,43,2)]
my_list2
['lecture3',
 'lecture5',
 'lecture7',
 'lecture9',
 'lecture11',
 'lecture13',
 'lecture15',
 'lecture17',
 'lecture19',
 'lecture21',
 'lecture23',
 'lecture25',
 'lecture27',
 'lecture29',
 'lecture31',
 'lecture33',
 'lecture35',
 'lecture37',
 'lecture39',
 'lecture41']

Here is a more basic example of using list comprehension. To square a number in Python, you use **2 (you can’t use ^2).

Here is how you can make a list containing the squares of the numbers in range(5).

# to square in Python, use **
[i**2 for i in range(5)]
[0, 1, 4, 9, 16]

Here is a fast way to make a list containing the word “hi” ten times.

["hi" for i in range(10)]
['hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi']

The variable name we use is not important here.

["hi" for _ in range(10)]
['hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi']
["hi" for kdsfjlkdsajf in range(10)]
['hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi', 'hi']

Here is an example of using list comprehension to add 1 to every element in the list squares.

squares = [i**2 for i in range(5)]
[i+1 for i in squares]
[1, 2, 5, 10, 17]

Introduction to f-strings

Here is our current version without using f-strings.

my_list2 = ["lecture"+str(i) for i in range(3,43,2)]

Here is how we can make the same list using f-strings. There are two components: we put an f before the quotation marks, and we put the variable inside of curly brackets.

my_list3 = [f"lecture{i}" for i in range(3,43,2)]
my_list3
['lecture3',
 'lecture5',
 'lecture7',
 'lecture9',
 'lecture11',
 'lecture13',
 'lecture15',
 'lecture17',
 'lecture19',
 'lecture21',
 'lecture23',
 'lecture25',
 'lecture27',
 'lecture29',
 'lecture31',
 'lecture33',
 'lecture35',
 'lecture37',
 'lecture39',
 'lecture41']

f-strings are very flexible. For example, you can have Python expressions like i+10 inside the curly brackets.

[f"lecture{i+10}" for i in range(3,43,2)]
['lecture13',
 'lecture15',
 'lecture17',
 'lecture19',
 'lecture21',
 'lecture23',
 'lecture25',
 'lecture27',
 'lecture29',
 'lecture31',
 'lecture33',
 'lecture35',
 'lecture37',
 'lecture39',
 'lecture41',
 'lecture43',
 'lecture45',
 'lecture47',
 'lecture49',
 'lecture51']

f-strings are a relatively new addition to Python. They were added in Python 3.6, and the default Python version in Deepnote (as I’m writing this in March 2022) is Python 3.7. If you log on to the Python website (again as of March 2022) and try to download Python, the default is Python 3.10.

When reading Python code, you’ll often see people using the old way of formatting strings, using the format method.

# old way
["lecture{}".format(i) for i in range(3,43,2)]
['lecture3',
 'lecture5',
 'lecture7',
 'lecture9',
 'lecture11',
 'lecture13',
 'lecture15',
 'lecture17',
 'lecture19',
 'lecture21',
 'lecture23',
 'lecture25',
 'lecture27',
 'lecture29',
 'lecture31',
 'lecture33',
 'lecture35',
 'lecture37',
 'lecture39',
 'lecture41']

Especially if there are multiple variables, you’ll see why the f-string way is much easier to read.

# old way
["{}lecture{}".format(i+10,i) for i in range(3,43,2)]
['13lecture3',
 '15lecture5',
 '17lecture7',
 '19lecture9',
 '21lecture11',
 '23lecture13',
 '25lecture15',
 '27lecture17',
 '29lecture19',
 '31lecture21',
 '33lecture23',
 '35lecture25',
 '37lecture27',
 '39lecture29',
 '41lecture31',
 '43lecture33',
 '45lecture35',
 '47lecture37',
 '49lecture39',
 '51lecture41']
# f-string way
my_list4 = [f"{i+10}lecture{i}" for i in range(3,43,2)]

Example

We’ll start with a piece of Python code that is correct but which can be improved. The code can be more concise (in fact, some of the code does not do anything). Using list comprehension instead of an explicit for loop probably is not much more efficient, but it is much more Pythonic.

Try to read this code, step by step, and figure out what it does.

my_list = [3.14, 10, -4, 3, 5, 5, -1, 20.3, 14, 0]
new_list = []
for i in range(0,len(my_list)):
    if my_list[i] > 4:
        new_list.append(my_list[i])
    else:
        new_list = new_list

How can this code be improved?

First of all, the else statement is doing nothing, so we get rid of it.

new_list = []
for i in range(0,len(my_list)):
    if my_list[i] > 4:
        new_list.append(my_list[i])
new_list
[10, 5, 5, 20.3, 14]

There is no need to mention 0 as the starting value in range.

new_list = []
for i in range(len(my_list)):
    if my_list[i] > 4:
        new_list.append(my_list[i])

It’s much more elegant to iterate over the elements in my_list rather than iterating through the numbers 0 through len(my_list).

new_list = []
for x in my_list:
    if x > 4:
        new_list.append(x)
new_list
[10, 5, 5, 20.3, 14]

We can make the code more compact, and definitely more Pythonic (not a precisely defined term) by using list comprehension. This is similar to what we are doing above with list comprehension in the previous section. The main difference is the if condition here.

new_list = [x for x in my_list if x > 4]
new_list
[10, 5, 5, 20.3, 14]
my_list
[3.14, 10, -4, 3, 5, 5, -1, 20.3, 14, 0]

You can also have an else condition, but then both the if and the else need to come before the for.

[x if x > 4 else "chris" for x in my_list]
['chris', 10, 'chris', 'chris', 5, 5, 'chris', 20.3, 14, 'chris']

The DRY principle

Here is a list of UCInetIDs that we want to turn into email addresses.

uci = ["DAVISCJ", "YBAKI", "CHUPENZ", "YUFEIR"]

Something like the following will always be incorrect in Math 10, because it includes too much repetition. There is a principle in programming called DRY, which stands for Don’t Repeat Yourself. We will always try to follow the DRY principle in Math 10.

# Not DRY
[uci[0]+"@uci.edu", uci[1]+"@uci.edu", uci[2]+"@uci.edu", uci[3]+"@uci.edu"]
['DAVISCJ@uci.edu', 'YBAKI@uci.edu', 'CHUPENZ@uci.edu', 'YUFEIR@uci.edu']

How can we do the same thing using list comprehension? We want something in the form [??0 for ??1 in ??2], where ??0 represents what goes in the list, where ??1 represents the name of the variable, and where ??2 indicates what elements the variable runs through (iterates over).

[f"{uci[i]}@uci.edu" for i in range(4)]
['DAVISCJ@uci.edu', 'YBAKI@uci.edu', 'CHUPENZ@uci.edu', 'YUFEIR@uci.edu']

Here’s an even nicer way.

[f"{x}@uci.edu" for x in uci]
['DAVISCJ@uci.edu', 'YBAKI@uci.edu', 'CHUPENZ@uci.edu', 'YUFEIR@uci.edu']

If we want to switch to lowercase letters, we can use the lower method.

# example
uci[1].lower()
'ybaki'
[f"{x.lower()}@uci.edu" for x in uci]
['daviscj@uci.edu', 'ybaki@uci.edu', 'chupenz@uci.edu', 'yufeir@uci.edu']

If we wanted to use the lower method in the non-DRY version, we would have to include lower four different places. That is why it’s always preferred to use as little repetition as possible.