01-13: Python Fundamentals Review#

Let’s do a quick review session of some of the Python basics we covered last week:

IMPORTANT: Before running code in this notebook please update the libraries in your conda environment. After activating the environment use the following command:

pip install ipywidgets

Review pt 1#

We create variables using the = operator:

x = 10  # integer
x
y = 3.5  # float
y

Adding integers and floats will automatically convert them to floats!

This is because Python is will never throw away extra precision, but instead convert everything to extra precision needed when performing an operation. In this case, integers are less precise than floats, so they will be converted to floats.

z = x + y
type(z)

But we don’t need extra precision if we’re adding two integers:

xx = x + x
type(xx)

We create strings using single or double quotes:

course_name = 'PSYC201B'  # string
course_name

You can think of strings as an iterable (a sequence you can manipulate by its elements) of letters, each of which is itself a string. Let’s check how long the string is:

len(course_name)

We can access individual parts of any iterable by indexing or slicing to get a subset of its elements. In this case that would give us the letters of the string 'PSYC201B'

Python usese 0-based indexing, which means the first letter is at index 0, the second letter is at index 1, and so on:

# Get the first letter 'P'
course_name[0]
# 'P' is also string
type(course_name[0])

Using type() to check the type of a variable is an example of using a function in Python. To call a function you simple type its name following up parathesis (), which include any arguments the function requires.

You can always find out more about how to use any function by typing function_name? in a jupyter notebook cell or using the built-in help function help(function_name):

type?

Ok back to iterables.

To slice the elements of an iterable we use the notation: iterable[start:stop:step] where start is the index of the first element to include, stop is the index of the first element to exclude, and step is optional step size between elements:

Here we get indices 1-3 exclusive, meaning we include the values from position 1 up to but not including position 3. This should give us the 2nd and 3rd letters:

course_name[1:3]

To slice from the start you can ommit stop and replace it with :

course_name[1:]

We can still use step with this pattern. Here we grab every 2nd element, starting at the 1st index until the end of the iterable:

course_name[1::2]

Python also supports negative indexing, which conveniently allows you to access elements from the end of an iterable:

course_name[-1]

We can also slice using negative indices. You can read this as start from the 1st index and go up-to the 2nd-to-last index:

course_name[1:-2]

You can also use negative steps, which is a convenient way to slice an iterable in reverse, starting from the end -> beginning.

Here we include the beginning and step through the string use a step size of -1, which turns out to be a quick and convenient way to reverse an iterable!

course_name[::-1]

You can combine strings using the + operator:

course_name + " is a terrible class"

Mini-exercise#

Use slicing, string manipulation and f-strings transform the variable course_name to a new string that says: "But PSYC201A was much better"

# Use slicing, string manipulation and f-strings transform the variable `course_name` to a new string that says: `"But PSYC201A was much better"`
# YOUR CODE HERE
course_name = 'PSYCH_201B'
# Hint
# you can use course_name[index] access specific letters
# and course_name[start:end:step] to slice ranges of letters

Review pt 2#

Strings have convenient methods on them for manipulating text. Remember methods are functions that belong to some object (variable). To see what methods are available for an object we can use the dir() function:

dir(course_name)

Because course_name is a string, would get back the same result if we asked dir about str objects:

dir(str)

We can invoke a method using dot notation. For example we can lower-case a string using the lower() method:

course_name.lower()

Lists are another type of iterable in Python. They can contain any type of other objects, that can all be different, including other lists:

pet_names = ["Yggy", "Franny", "Cynthia"]
pet_names
pet_names[1]

Lists have their own methods, but a common one you’ll use often is the append() method. This method adds an element to the end of the list, growing the list by a single element. Unlike some other languages, Python lists are mutable, meaning that you can change their contents and size after they have been created without having to create a new list or pre-specifying the size of the list in advance:

pet_names.append("Charlie")

Wait nothing was output? Remember this changes the original list so we can inspect it to see what happened:

pet_names

What happens if we run the append method again?

# Demo

We can add lists together using the + operator just like we did with strings:

more_pets = pet_names + ["Mufasa", "Airbud", "Goofy"]
more_pets

We can loop over any iterable using a for statement:

# Take each element of pet_names
for pet in pet_names:
    # Because each element is a string, we can use the lower() method
    print(pet.lower())

lists have a short-hand syntax for looping called list-comprehensions. These are a concise to loop over a list if you know that you want a list back, but you don’t have to use this syntax if you find it confusing.

lower_case_pet_names = [pet.lower() for pet in pet_names]
lower_case_pet_names

These can also include simple conditional statements using if statements:

lower_case_cats = [pet.lower() for pet in pet_names if pet != "Franny"]
lower_case_cats

We can get the index of each element while looping using the enumerate() function:

for idx, pet in enumerate(pet_names):
    # Print the index, number, and name of the pet
    to_print = f"Index: {idx}, Number: {idx + 1}, Name: {pet}"
    print(to_print)

This can be handy when you want to loop over multiple iterables at once:

pet_ages = [2.25, 1, 1.5]

for idx, pet in enumerate(pet_names):
    print(f"{pet} is {pet_ages[idx]} years old")

Mini-exercise#

Oops something went wrong with our loop above.
What happened & how can we fix it?

# WHAT SHOULD WE CHANGE?
pet_ages = [2.25, 1, 1.5]

for idx, pet in enumerate(pet_names):
    print(f"{pet} is {pet_ages[idx]} years old")

Review pt 3#

A more convient way to iterate over iterables of the same length is to use the zip function. This function takes two or more iterables as arguments and returns an iterator that returns a tuple containing the corresponding elements from each iterable:

pet_names = ["Yggy", "Franny", "Cynthia"]
pet_ages = [2.25, 1, 1.25]

for name, age in zip(pet_names, pet_ages):
    print(f"{name} is {age} years old.")

Let’s understand what’s happening here. zip is creating a new iterable that contains a sequence of pairs from both lists.

zipped = zip(pet_names, pet_ages)
zipped
len(zipped)
zipped[0]

zipped is an example of a generator, a special type of lazy iterable that generates values on the fly. You can think of them as producing values at each position only when needed, and until they are exhausted. This can be handy for when you don’t need/want to create another list (that can be huge and take up a lot of memory) and simply need to “consume” each element for some one-off operation:

zip?

Because generators are lazy, they don’t have a length and we can’t index into them or slice them! Their contents don’t yet exist!

Instead we need to convert them to lists to force them to generate all their elements:

list(zipped)

We didn’t save that list to variable last time. So let’s do that now:

pairs = list(zipped)
pairs

What why is pairs empty?! Remember generators are lazy and we already consumed zipped when we converted to a list! We’ll need to create a new generator to iterate over the zipped pairs.
When we use the for loop we’re creating the generator of names and pairs on-the-fly, and then using their values in the print statement:

pet_names = ["Yggy", "Franny", "Cynthia"]
pet_ages = [2.25, 1, 1.25]

for name, age in zip(pet_names, pet_ages):
    print(f"{name} is {age} years old.")

Finally we talked about conditional statements that allow use to use if/elif/else logic. We can use comparators like ==, >, <, >=, <= to compare values. We can also use the not operator to negate a condition:

Numerical comparisons:

for name, age in zip(pet_names, pet_ages):
    if age <= 1:
        print(f"{name} is just a baby")
    elif 1 < age < 2:
        print(f"{name} is growing")
    else:
        print(f"{name} is an adult")

String comparisons:

for name, age in zip(pet_names, pet_ages):
    if name.startswith("F"):
        print(f"{name} is a dog")
    else:
        print(f"{name} is a cat")

Inverting the logic:

for name, age in zip(pet_names, pet_ages):
    if name != "Franny":
        print(f"{name} is a cat")
    else:
        print(f"{name} is a dog")

Challenge: Putting it all together#

Use the following description of the 9 realms of Norse mythology to complete the follow excercises:

Yggdrasil, the legendary tree from Norse mythology, connects the nine realms of existence. These realms include Midgard, Asgard, Jotunheim, Niflheim, Muspelheim, Vanaheim, Nidavellir, Alfheim, and Helheim.
Each of these nine realms is unique in its nature and inhabitants. Midgard, often referred to as the land of humans and lies at the center.
Asgard is home to Odin, Thor, and the other gods who reside in great halls like Valhalla and rule over the cosmos.
Jotunheim, the land of the giants, is a rugged and mountainous realm, where frost and stone giants dwell in defiance of the gods.
Niflheim is a cold, dark world of ice and mist, said to be the oldest of the realms and the source of the primordial rivers that formed the world.
Muspelheim, on the other hand, is a fiery and chaotic realm ruled by the fire giant Surtr, its intense heat.
Vanaheim, the lush and mysterious home of the Vanir gods, is known for its fertility, magic, and wisdom.
Nidavellir, sometimes called Svartalfheim, is the underground domain of the dwarves, master craftsmen who forged legendary artifacts like Thor’s hammer, Mjolnir.
Alfheim is a bright and ethereal realm inhabited by the light elves, beings of great beauty and grace who are considered minor deities of nature and fertility.
Finally, Helheim, the realm of the dead, is ruled by Hel, the daughter of Loki, where those who die of old age, illness, or other inglorious means find their afterlife, in stark contrast to the heroic dead who are welcomed into Valhalla.

Realms of Norse Mythology
  1. Split the passage into a list of sentences of completence sentences that include a period but no special characters e.g. '\n'

  2. Count the number of sentences and check if they’re equal to the number of realms (9)

  3. Process each sentence such that:

  • if the sentence ONLY references the realm Asgard, capitalize all the words in the sentence

  • if the sentence ONLY references the realm Jotunheim, reverse the entire sentence

  • if the sentence ONLY references the realm Helheim, replace all the words in the sentence with the word “dead”

  1. Combine any unchanged sentences along with your modified sentences into a single new paragraph such that each sentence is on a new line

1. Split the paragraph into complete sentences#

# 1. Split the passage into a list of sentences of completence sentences that include a period but no special characters e.g. '\n'

paragraph = """
Yggdrasil, the legendary tree from Norse mythology, connects the nine realms of existence. These realms include Midgard, Asgard, Jotunheim, Niflheim, Muspelheim, Vanaheim, Nidavellir, Alfheim, and Helheim.

Each of these nine realms is unique in its nature and inhabitants. Midgard, often referred to as the land of humans and lies at the center. Asgard is home to Odin, Thor, and the other gods who reside in great halls like Valhalla and rule over the cosmos. Jotunheim, the land of the giants, is a rugged and mountainous realm, where frost and stone giants dwell in defiance of the gods. Niflheim is a cold, dark world of ice and mist, said to be the oldest of the realms and the source of the primordial rivers that formed the world. Muspelheim, on the other hand, is a fiery and chaotic realm ruled by the fire giant Surtr, its intense heat. Vanaheim, the lush and mysterious home of the Vanir gods, is known for its fertility, magic, and wisdom. Nidavellir, sometimes called Svartalfheim, is the underground domain of the dwarves, master craftsmen who forged legendary artifacts like Thor's hammer, Mjolnir. Alfheim is a bright and ethereal realm inhabited by the light elves, beings of great beauty and grace who are considered minor deities of nature and fertility. Finally, Helheim, the realm of the dead, is ruled by Hel, the daughter of Loki. It is a place where those who die of old age, illness, or other inglorious means find their afterlife, in stark contrast to the heroic dead who are welcomed into Valhalla.
"""

# Split the paragraph
# Process any special characters
# Output the sentences

2. Count the number of sentences and compare to number of realms (9)#

You can use the assert statement to check if a condition is true. If the condition is false, an AssertionError will be raised, for example:

x = 2
y = 3
assert x + x == 4, f"{x} + {x} does not equal 4" # no output
assert x + y == 4, f"{x} + {y} does not equal 4" # Will output "AssertionError: 2 + 3 does not equal 4"
# Step 2: Count the number of sentences
# Print the result
# Compare to the number of realms using assert

3. Manipulate sentences based on realm#

'Asgard' -> capitalize each word
'Jotunheim' -> reverse entire sentence
'Helheim' -> replace each word with 'dead'

# Hint:
# Use for-loops or list-comprehensions to iterate over sentences, words, or letters
# Use the function scaffold below to process each sentence separately
# Use if/elif/else statements to check for each realm individually

def process_sentence(sentence):
    # Check for each realm individually

    # Capitalize for Asgard only 
    # Reverse for Jotunheim only 
    # Replace with 'dead' for Helheim only 
    # Otherwise return the sentence unchanged

    return sentence
# Apply the function to each sentence to generate a new list of processed_sentences

4. Combine sentences into a new paragraph#

# Hint: are there string methods that can be helpful here?

Submit your completed notebook!#

Remember to submit your completed notebook to make sure you can work with Github Classroom! Run the following commands in your terminal from the same directory that you cloned the assignment:

  1. Take a peek at what git thinks has changed in your current repository:
    git status

  2. Add this notebook file you modified to stage it for a commit (prepare for a snapshot):
    git add 01_Python_Fundamentals_Review.ipynb

  3. Commit your changes to your local repository (take a local snapshot):
    git commit -m "completed review of python fundamentals"

  4. Push to github to update your assignment:
    git push