On this page:
12.1 A Crash Course on Python
12.2 Python Semantics:   A Mystery Hunt
8.12

Lab 12: Python: The Full Monty🔗

Goals: To learn how to experiment with a new language, make hypotheses about its semantics, and test those hypotheses by writing example programs.

12.1 A Crash Course on Python🔗

If you already have a version of Python installed on your computer, make sure its version is something like 2.7.x, not 3.0.x. The 3.0 version family made lots of semantic changes, and while most of this lab will work the same, some details will differ.

In today’s lab, you will be learning about Python, a dynamically typed language with a significantly different syntax and semantics from Java. We recommend you use jdoodle so you can write and run programs without installing anything on your own computer. But you can also download and install Python here.

The below notes give a quick overview of Python, but you might find this tutorial to be a slightly more extensive introduction.

Syntactically, Python is more lightweight than Java. For example, here is "Hello, world" in Python. Try it out in the Python REPL (the right-hand window) at tutorialspoint.

print("Hello, world!")

Like Racket but unlike Java, Python allows you to define functions outside of a class. It also uses whitespace rather than curly braces ("{" and "}") or parentheses to delimit blocks in your code: indentation is significant! For example, here’s a function that prints a greeting for the name you give it. Enter this definition in the left-hand window in tutorialspoint, click “run”, then call it at the REPL with something like greet("Joe").

def greet(name): print("Hello, " + name)

Rather than else if for a continued block as in Java, Python uses a special elif keyword.

def sign(n): if n > 0: return "positive" elif n < 0: return "negative" else: return "zero"

Python uses the = operator to both create a new variable and to mutate an existing variable. For example, the following function computes the sum of all positive numbers in a list. The first assignment to ans creates ans as a local variable, and the other one (inside the loop) mutates it.

def sum(xs): ans = 0 for x in xs: ans = ans + x return ans

Classes in Python are declared with the class keyword. Every class inherits from some parent class, given in parentheses after the name of the defined class. The top-level class is object, which is usually what a class should inherit from.

A constructor for a class is a method called __init__. That method, and all other methods on the class, have self as their first parameter. The self keyword acts like the this keyword in Java. For example, a Person class might look like the following.

The “#” symbol denotes the start of a comment.

class Person(object): def __init__(self, name): self.name = name # returns a message greeting this person def greeting(self): return "Hello, " + self.name # instantiate the class by calling it like a function c = Person("Jane") print(c.greeting()) # prints "Hello, Jane"

Fields do not need to be declared ahead of time in Python. Instead, new fields are created dynamically whenever you assign to them, as with the assignment self.name = name above.

In Python, classes can inherit from more than one class. For example, in the following, Bat inherits from both Mammal and WingedAnimal.

The pass keyword acts like an empty block in Python, since we can’t just write { } like in Java. Here it just means we want these class bodies to be empty.

class Animal(object): # class body would go here... pass class Mammal(Animal): # class body would go here... pass class WingedAnimal(Animal): # class body would go here... pass class Bat(Mammal, WingedAnaimal): # class body would go here... pass

12.2 Python Semantics: A Mystery Hunt🔗

Your job for this lab is to approach Python as a scientist would approach a natural phenomenon like gravity or the weather: by using the scientific method to formulate and test hypotheses. Specifically, for each problem description below, you should:

  1. Work in groups of five or six people, and share ideas.

  2. Write a program that matches the description below.

  3. Predict what the program will do before running it. This is your hypothesis about that aspect of Python’s semantics.

  4. Run the program on various inputs and observe what happens. If your hypothesis did not accurately predict all of the results, come up with a new hypothesis and try again.

By observing how your program runs and refining your hypothesis over time, you can start to come up with a theory about how Python works. Your theory probably won’t be 100% accurate (there are many corner cases to worry about), but you’ll end up closer to the truth than you were when you started.

  1. Write a function that creates a new variable in only one branch of an if/else construct, then uses that variable after the if/else. Does it matter which branch of the if gets executed?

  2. Python allows functions to “close over” the variables in the scope outside where they were defined, which you can see in the code below.

    def f(): x = "closed over" def g(): return x return g() # f() ==> "closed over"

    What happens if running an inner function causes an assignment to a variable defined in an outer function? Hint: you may want to search the web for Python’s nonlocal keyword and find out what it does.

  3. Unlike Java, Python allows arbitrary statements alongside the method definitions in classes. For example, the following code will print the number 42 as soon as this program is run.

    class ClassStatements(object): x = 41 print(x + 1) def __init__(self): pass

    What happens when you try to access a variable created at that level inside a method?

  4. Class definitions can be nested inside functions and inside methods. Try building a function definition that contains a class definition that contains a method definition, each of which try to access fields or parameters that have been declared at some outer level. What strange things happen here? What’s going on?

  5. Every class has a list-valued field named __mro__. Create some classes with interesting inheritance chains (including with multiple inheritance), and find out what the value of that field is on each class (e.g. with Person.__mro__ for a class Person).

    • What do you think the list represents? What does the order of items in the list represent? Hint: if a class A is declared as class A(B, C), and both B and C have a method named foo, then invoking foo on an instance of A will use the version on B rather than C. Why?

    • If you think you have a good hypothesis for this, can you write a class that is impossible to instantiate, because it would have no legal __mro__ field?

  6. Challenge: Generators: Python has a feature called generators, which allow a function to behave kind of like an Iterator in Java. For example, say we want to iterate over the list of positive integers. Then we might write a function like the following.

    def pos_ints(): n = 0 while True: n += 1 yield n

    Python 3 renamed next to __next__.

    The yield keyword says “return this value as the next value in the sequence, but resume execution at this point in the function when you iterate again.” Any function that contains a “yield” keyword automatically returns a generator object whenever it is called. That object has a next method that works just like the next method on an iterator in Java. In this case, calling pos_ints creates a generator that can be used like so:

    gen = pos_ints() print(gen.next()) # prints 1 print(gen.next()) # prints 2 print(gen.next()) # prints 3

    Create some examples of generators for yourself. Try writing a generator that uses multiple function calls, including where the “yield” happens in one of those inner function calls. What happens? Can you refactor your code to do what you expect?