CSCI 0050: Introduction to Python Notes

7/22/19

Will Patterson

A motivational question

How do programming languages store and manage your data?

Inside the memory of the computer, the programming language sets aside two areas to store info.

Our first concept, that we're familiar with, is the 'known names' area (traditionally known as the Environment).

We might visualize it as:

rate => 10
name => "Kathi"

When we think about storing data, we will now introduce a new area for data storage called the ~heap.

The heap

The heap can be thought of as labelled slots in memory. This is used in conjuction with the Environment.

Given an Environment with the following:

noon = time(12,0)
midnight = time(0,0)

The heap might look something like:

label slot
1000 time(12,0)
1001 time(0,0)
1002 slot

With lists, this becomes a little more obfuscated:

Given an Environment with the following:

appts = link(time(9,30),
            link(time(10,45),
                link(time(14,0),
                    empty)))

The heap might look something like:

label slot
1000 time(12,0)
1001 time(0,0)
1002 time(9,30)
1003 time(10,45)
1004 time(14,0)
1005 link(loc 1004,empty)
1006 link(loc 1003,loc 1005)
1007 link(loc 1002,loc 1006)

Now that we've gotten this concept out of the way, let's start to migrate away from Pyret.


Defining functions

It's time to move into Python!

To start learning Python, we will translate the pen-cost program we worked through in Pyret.

In order to define a function, we will use the command def. When defining the parameters of the function, you should note some important differences.

  • For function names, do not use hyphens. You can use underscores (called snake case). This looks like my_function().
  • For variable names, you should capitalize each new word without spaces (called CamelCase). This looks like myVar.
In [1]:
# start by translating the pen-cost program

# if you don't have an integer value, you should use float type

def pen_cost(numPens: int, message: str) -> float:
    """total cost for pens at 25 cents each plus 2 cents per character"""
    return numPens * (0.25 + (len(message) * 0.02)) #use return to print to Python Console

# now, let's try our new function. this input should return 2.9
pen_cost(10, "hi")

Notice that we didn't have to use end to indicate the end of a function block. The community that developed Python was more concerned with doing things quickly (wanted to get rid of extra things like end). They used indentation to help Python know when a "new" thing starts. This has some funny implications we will be covering later.

Tabs vs. spaces

Python interprets the indentation of lines of code as signifying the beginning and end of things. This indentation can be achieved by either tabs or 4 spaces. Pick one and stick with it. You must be consistent throughout your code.

But how do we write tests?

Testlight.py

This is our testing file. You must put it in your directory and import it into your program.

Importing is done as follows:

from testlight import *

There is no notion of a where: block in Python, so we will write funtions that test.

In [2]:
# this will not work as we have not imported testlight.py
# the below code is purely for demonstration

def test_pens():
    test("3 pens, 3 chars", pen_cost(3, "wow"), 0.93)
    test("10 pens, 5 chars", pen_cost(10, "smile"), 3.5)

Function Translation: Pepper-scale

This was done

In [3]:
def pepper_scale(rating: int) -> str:
    """given a scoville rating, return the name of the pepper"""
    if (rating == 0):
        return "bell pepper"
    elif ((rating >= 100) and (rating <= 1000)):
        return "paprika"
    elif ((rating >= 3500) and (rating <= 10000)):
        return "jalapeno"
    else:
        return "unknown"

pepper_scale(0) # should return "bell pepper"
Out[3]:
'bell pepper'
In [4]:
pepper_scale(250) #should return "paprika"
Out[4]:
'paprika'
In [5]:
pepper_scale(60000000) #should return "unknown"
Out[5]:
'unknown'

What's the difference between print and return?

In [6]:
def printAndReturnNothing():
    x = "hello"
    print(x)

def printAndReturn():
    x = "hello"
    print(x)
    return x

def main():
    ret = printAndReturn()
    other = printAndReturnNothing()

    print("ret is: %s" % ret)
    print("other is: %s" % other)

main()
hello
hello
ret is: hello
other is: None

print takes its arguments/expressions and dumps them to standard output, so in the functions I made up, print will output the value of x, which is hello.

printAndReturn will return x to the caller of the method, so:

ret = printAndReturn()

ret will have the same value as x, i.e. "hello"

printAndReturnNothing doesn't return anything, so:

other = printAndReturnNothing()

other actually becomes None because that is the default return from a Python function. Python functions always return something, but if no return is declared, the function will return None.

Learning about Lists

In python, the [] operator indicates a list.

We can add to a list using the .append() method. .append() adds an element to the end of a list.

IMPORTANT: When we .append() to a list, this mutates the list! It does not create a new instantiation of the list.

In [7]:
# imagine we want to make a list of the courses we're taking

# instantiate an empty list
taking = []

# append a course to the list
taking.append("CSCI50")

print(taking)

# append another course to the list
taking.append("VISA100")

print(taking)
['CSCI50']
['CSCI50', 'VISA100']

Python

When making a list, Python grabs a whole bunch of slots at once for the heap. We can illustrate this with our example above.

At first, when instantiating the list, it might look something like:

Environment (Known Names)

taking => loc 1015

Python then grabs 4 'slots', for example.

Python Heap

label slot
1015
1016
1017
1018

Now, we can illustrate adding a course to our taking list. After the taking.append("CSCI50") call.

Environment (Known Names)

taking => loc 1015

Python Heap

label slot
1015 "CSCI50"
1016
1017
1018

This is the reason why you can't ask for the rest of a list (like you can in Pyret).

List Functions

Let's work through some examples that illustrate how we could use Python for course registration.

In [8]:
from typing import List #only bringing this in to get type name for list

# register for a course by adding to a registration list
def register(regList: List[str], courseName: str) -> List:
    """consumes a list of registered courses and a new course to register
    for, returns the list of registered courses"""
    regList.append(courseName)
    return regList
In [9]:
register(taking, "PHIL130")
Out[9]:
['CSCI50', 'VISA100', 'PHIL130']

But if we add PHIL130 again, we see a problem.

In [10]:
register(taking, "PHIL130")
Out[10]:
['CSCI50', 'VISA100', 'PHIL130', 'PHIL130']

It got added twice!

Let's write a function that fixes this issue by checking if something is a member of a list.

In [15]:
def registerImproved(regList: List[str], courseName: str) -> List:
    """consumes a list of registered courses and a new course to register
    for, returns the list of registered courses"""
    if courseName not in regList:
        regList.append(courseName)
    return regList
In [18]:
# instantiate a new taking list
taking2 = []

# append a course to the list
registerImproved(taking2, "CSCI50")

print(taking2)

# append a course to the list
registerImproved(taking2, "VISA100")

print(taking2)

# append the same course to the list
registerImproved(taking2, "VISA100")

print(taking2)
['CSCI50']
['CSCI50', 'VISA100']
['CSCI50', 'VISA100']

What if we wanted to raise an error to let the user know that they're trying to register for a course they already have in their registered course list?

We can use raise!

In [24]:
# what if we wanted to raise an error if re-register?

def registerWithError(regList: List[str], courseName: str) -> List:
    if courseName not in regList:
        regList.append(courseName)
    else: raise Exception("Already taking this course")
    return regList
In [26]:
# instantiate a new taking list
taking2 = []

# append a course to the list
registerWithError(taking2, "CSCI50")

print(taking2)

# append a course to the list
registerWithError(taking2, "VISA100")

print(taking2)

# append the same course to the list
registerWithError(taking2, "VISA100")
['CSCI50']
['CSCI50', 'VISA100']
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-26-7daa64aac70a> in <module>
     13 
     14 # append the same course to the list
---> 15 registerWithError(taking2, "VISA100")

<ipython-input-24-e4bc2d73309c> in registerWithError(regList, courseName)
      4     if courseName not in regList:
      5         regList.append(courseName)
----> 6     else: raise Exception("Already taking this course")
      7     return regList

Exception: Already taking this course

Now, what if we wanted to drop a class? We can do this too. We will use the .remove() function on our list.

In [28]:
# perhaps we decided that we didn't like art
# we can drop VISA100

# peek at the list
print(taking2)

taking2.remove("VISA100")

# peek at the list after removing 
print(taking2)
['CSCI50']
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-28-aff945e723ad> in <module>
      5 print(taking2)
      6 
----> 7 taking2.remove("VISA100")
      8 
      9 # peek at the list after removing

ValueError: list.remove(x): x not in list

We can use the in operator to test membership. This can look a number of different ways.

In [29]:
# in can be used to test membership in a list

"CSCI50" in taking2
Out[29]:
True
In [31]:
# in can also be used to test for substrings

"CS" in "CSCI50"
Out[31]:
True

We can define a function that gets the names of registered courses from a given department in our "taking" list.

This serves as an introduction to anonymous functions (lambda functions) in Python.

Syntax of a Lambda Function in Python

lambda arguments: expression

Lambda functions can have any number of arguments but only one expression. The expression is evaluated and returned. Lambda functions can be used wherever function objects are required.

In [32]:
# get names of reg courses from a given department

def from_dept(regList: List[str], deptName: str) -> List[str]:
    """return names of all courses in list with give n deptName in them"""
    return list(filter(lambda course: deptName in course, regList))

Why do we need the list() constructor from filter?

The filter function does not internally return a list. We need to use list() to have our output converted to a list.

Rewriting our phone_charges function from Quiz 1

In [ ]:
# how can we alter our code from quiz 1 such that it can work for both mins and texts?

BASE_RATE = 20

def phone_charges(numMinutes: int, numTexts: int) -> float:
    """compute phone plan charges based on minutes used and texts sent"""
    # introduce a global variable total to be returned at end of function
    total = BASE_RATE
    if numMinutes > 120:
        total = total + ((numMinutes - 120) * 0.05)
    if numTexts > 10:
        total = total + ((numTexts - 10) * 0.10)
    return total