Lab 11: Exceptions, Iterators/Generators, and SQL
Due at 11:59:59 pm on Tuesday, 12/10/2019.
Starter Files
Download lab11.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.
Submission
By the end of this lab, you should have submitted the lab with python3 ok --submit
. You may submit more than once before the deadline; only the final submission will be graded. Check that you have successfully submitted your code on okpy.org. See this article for more instructions on okpy and submitting assignments.
Exceptions
Exceptions allow us to try a chunk of code, and then catch any errors that might come up. If we do catch an exception, we can run an alternative set of instructions. This construct is very useful in many situations.
try:
<try suite>
except Exception as e:
<except suite>
else:
<else suite>
finally:
<finally suite>
Notice that we can catch the exception as e
. This binds the name e
to
the exception object. This can be helpful when we want to give extra
information on what happened. For example, we can print(e)
inside the
except clause.
Also, we have an optional else case. The else suite is executed if the
try
suite finishes without any exceptions.
We also have an optional finally clause, which is always executed, whether or not an exception is thrown. We generally don't need to use the else and finally controls in this class.
When we write exception statements, we generally don't just use the
class Exception as above. Rather, we figure out the specific type of
exception that we want to handle, such as TypeError
or
ZeroDivisionError
. To figure out which type of exception you are trying
to handle, you can type purposely wrong things into the interpreter
(such as 'hi' + 5 or 1 / 0) and see what kind of exception Python spits
out.
Iterators
Remember the for
loop? (We really hope so.) The object the for
loop
iterates over must be an iterable! An iterable is a way of representing
explicit sequences (like lists or strings) as well as implicit sequences (like
the natural numbers 1, 2, 3, ...
).
for elem in iterable:
# do something
for
loops only work with iterables. This means the object you want to use a
for
loop on must implement the iterable interface. To implement the
iterable interface, an object must define an __iter__
method that returns an
object that implements the iterator interface. To implement the iterator
interface, an object must define a __next__
method to compute and return the
next element in the sequence. If the iterator exhausts the sequence, it raises
StopIteration
to send a signal to indicate that it reaches the end.
An iterable object can create an arbitrary amount of iterator objects. In addition, the iterators are independent of each other; in other words they can have a different position in the sequence.
Here is a table summarizing the required methods of the iterable and iterator interfaces/protocols. Python also has more documentation about iterator types.
Iterable | Iterator |
---|---|
__iter__ : return a new iterator |
__iter__ : must return itself |
__next__ : return the next element,
or raise StopIteration |
In Python, an iterator must also be an iterable. In other words, it must have a
__iter__
method that returns itself (with the current state unaltered).
Analogy: an iterable is like a book (one can flip through the
pages) and an iterator is a bookmark (saves the position and can locate
the next page). Calling __iter__
on a book gives you a new bookmark
independent of other bookmarks, but calling __iter__
on a bookmark
gives you the bookmark itself, without changing its position at all.
Here is an example of a class definition for an object that implements the iterator interface:
class AnIterator:
def __init__(self):
self.current = 0
def __next__(self):
if self.current > 5:
raise StopIteration
self.current += 1
return self.current
def __iter__(self):
return self
Let's go ahead and try out our new toy.
>>> for num in AnIterator():
... print(num)
1
2
3
4
5
6
This is somewhat equivalent to running:
t = AnIterator()
t = iter(t) # iter(t) is the same as t.__iter__()
try:
while True:
# next(t) is the same as t.__next__()
print(next(t))
except StopIteration as e:
pass
Question 1
Implement an iterator class called ScaleIterator
that scales elements in an
iterable s
by a number k
.
class ScaleIterator:
"""An iterator the scales elements of the iterable s by a number k.
>>> s = ScaleIterator([1, 5, 2], 5)
>>> list(s)
[5, 25, 10]
>>> m = ScaleIterator(naturals(), 2)
>>> [next(m) for _ in range(5)]
[2, 4, 6, 8, 10]
"""
def __init__(self, s, k):
"*** YOUR CODE HERE ***"
self.s = iter(s)
self.k = k
def __iter__(self):
return self
def __next__(self):
"*** YOUR CODE HERE ***"
return next(self.s) * self.k
Use OK to test your code:
python3 ok -q ScaleIterator
Generators
A generator function returns a special type of iterator called
a generator object. Such functions can be written using a yield
statement:
def <generator_fn_name>():
<somevariable> = <something>
while <predicate>:
yield <something>
<increment somevariable>
Calling a generator function (a function with a yield statement in it) makes it return a generator object rather than executing the body of the function.
The reason we say a generator object is a special type of iterator is that it has all the properties of an iterator, meaning that:
- Calling the
__iter__
method makes a generator object return itself without modifying its current state. - Calling the
__next__
method makes a generator object compute and return the next object in its sequence. If the sequence is exhausted,StopIteration
is raised. - Typically, a generator should not restart unless it's defined that way. But
calling the generator function returns a brand new generator object (like
calling
__iter__
on an iterable object).
However, they do have some fundamental differences:
- An iterator is a class with
__next__
and__iter__
explicitly defined, but a generator can be written as a mere function with ayield
in it. __iter__
in an iterator usesreturn
, but a generator usesyield
.A generator "remembers" its state for the next
__next__
call. Therefore, the first__next__
call works like this:- Enter the function, run until the line with
yield
. - Return the value in the
yield
statement, but remember the state of the function for future__next__
calls.
And subsequent
__next__
calls work like this:- Re-enter the function, start at the line after
yield
, and run until the nextyield
statement. - Return the value in the
yield
statement, but remember the state of the function for future__next__
calls.
- Enter the function, run until the line with
Use OK to test your knowledge with the following What would Python print questions:
python3 ok -q generators -u --local
def generator():
print("Starting here")
i = 0
while i < 6:
print("Before yield")
yield i
print("After yield")
i += 1
>>> g = generator()
>>> g # what type of object is this?
______<generator object ...>
>>> g == iter(g) # equivalent of g.__iter__()
______True
>>> next(g) # equivalent of g.__next__()
______Starting here
Before yield
0
>>> next(g)
______After yield
Before yield
1
>>> next(g)
______After yield
Before yield
2
Trace through the code and make sure you know where and why each statement is printed.
You might have noticed from the Iterators section that IteratorB
, which didn't
define a __next__
method, failed to run in the for
loop. However, this is
not always the case.
class IterGen:
def __init__(self):
self.start = 5
def __iter__(self):
while self.start < 10:
self.start += 1
yield self.start
for i in IterGen():
print(i)
Why does this iterable work without defining a __next__
method?
The for
loop only expects the object returned by __iter__
to have a
__next__
method. The __iter__
method is a generator function because of the
yield
statement in the body. Therefore, when __iter__
is called, it returns
a generator object, which you can call __next__
on.
Question 2: Scale
Implement the generator function scale(s, k)
, which yields elements of the
given iterable s
, scaled by k
.
def scale(s, k):
"""Yield elements of the iterable s scaled by a number k.
>>> s = scale([1, 5, 2], 5)
>>> type(s)
<class 'generator'>
>>> list(s)
[5, 25, 10]
>>> m = scale(naturals(), 2)
>>> [next(m) for _ in range(5)]
[2, 4, 6, 8, 10]
"""
"*** YOUR CODE HERE ***"
for elem in s:
yield elem * k
Use OK to test your code:
python3 ok -q scale
SQL
Setup
For this lab, you're going to move away from Datahub and onto your own machines. You can complete this lab by either installing SQLite onto your computer or use one of the computers in lab using your EECS login information. To find your EECS login information, you can visit the EECS instructional account site.
The simplest way to start using SQLite is to download a precompiled binary from the SQLite website. The latest version of SQLite at the time of writing is 3.23.0, but you can check for additional updates on the website.
Windows
- Visit the download page linked above and navigate to the section Precompiled Binaries for Windows. Click on the link sqlite-shell-win32-x86-*.zip to download the binary.
- Unzip the file. There should be a
sqlite3.exe
file in the directory after extraction. Navigate to the folder containing the
sqlite3.exe
file and check that the version is at least 3.8.3:$ cd path/to/sqlite $ ./sqlite3 --version 3.9.2 2015-11-02 18:31:15 bda77dda9697c463c3d0704014d51627fceee328
Mac OS X Yosemite (10.10) or El Capitan (10.11)
SQLite comes pre-installed. Check that you have a version that's greater than 3.8.3:
$ sqlite3
SQLite version 3.8.5
Mac OS X Mavericks (10.9) or older
SQLite comes pre-installed, but it is the wrong version.
- Visit the download page linked above and navigate to the section Precompiled Binaries for Mac OS X (x86). Click on the link sqlite-shell-osx-x86-*.zip to download the binary.
- Unzip the file. There should be a
sqlite3
file in the directory after extraction. Navigate to the folder containing the
sqlite3
file and check that the version is at least 3.8.3:$ cd path/to/sqlite $ ./sqlite3 --version 3.9.2 2015-11-02 18:31:15 bda77dda9697c463c3d0704014d51627fceee328
Ubuntu
The easiest way to use SQLite on Ubuntu is to install it straight from the native repositories (the version will be slightly behind the most recent release):
$ sudo apt-get install sqlite3
$ sqlite3 --version
3.8.6 2014-08-15 11:46:33 9491ba7d738528f168657adb43a198238abde19e
Usage
Note: If you downloaded a precompiled binary above, make sure that the
sqlite3.exe
file is in the same directory as your.sql
file. (Extract and move it out from the zip file you downloaded.)
After writing your code in the .sql
file, you can test and verify
your output on the terminal with one of the two following
commands.
1.) Runs your code and then exits SQLite immediately afterwards.
sqlite3 < lab13.sql
2.) Runs your code and keeps SQLite open for further commands, which
is similar to running Python code with the interactive -i
flag.
You can type .help
to see some of the commands you can run.
sqlite3 --init lab13.sql
To exit out of SQLite after using the second command, you can hit
Ctrl-D
type .exit
or .quit
.
SQL Basics
Creating Tables
You can create SQL tables either from scratch or from existing tables.
The following statement creates a table by specifying column names and values
without referencing another table. Each SELECT
clause specifies the values
for one row, and UNION
is used to join rows together. The AS
clauses give a
name to each column; it need not be repeated in subsequent rows after the
first.
CREATE TABLE [table_name] AS
SELECT [val1] AS [column1], [val2] AS [column2], ... UNION
SELECT [val3] , [val4] , ... UNION
SELECT [val5] , [val6] , ...;
Let's say we want to make the following table called big_game
which records
the scores for the Big Game each year. This table has three columns:
berkeley
, stanford
, and year
.
We could do so with the following CREATE TABLE
statement:
CREATE TABLE big_game AS
SELECT 30 AS berkeley, 7 AS stanford, 2002 AS year UNION
SELECT 28, 16, 2003 UNION
SELECT 17, 38, 2014;
Selecting From Tables
More commonly, we will create new tables by selecting specific columns that we
want from existing tables by using a SELECT
statement as follows:
SELECT [columns] FROM [tables] WHERE [condition] ORDER BY [columns] LIMIT [limit];
Let's break down this statement:
SELECT [columns]
tells SQL that we want to include the given columns in our output table;[columns]
is a comma-separated list of column names, and*
can be used to select all columnsFROM [table]
tells SQL that the columns we want to select are from the given tableWHERE [condition]
filters the output table by only including rows whose values satisfy the given[condition]
, a boolean expressionORDER BY [columns]
orders the rows in the output table by the given comma-separated list of columnsLIMIT [limit]
limits the number of rows in the output table by the integer[limit]
Note: We capitalize SQL keywords purely because of style convention. It makes queries much easier to read, though they will still work if you don't capitalize keywords.
Here are some examples:
Select all of Berkeley's scores from the big_game
table, but only include
scores from years past 2002:
sqlite> SELECT berkeley FROM big_game WHERE year > 2002;
28
17
Select the scores for both schools in years that Berkeley won:
sqlite> SELECT berkeley, stanford FROM big_game WHERE berkeley > stanford;
30|7
28|16
Select the years that Stanford scored more than 15 points:
sqlite> SELECT year FROM big_game WHERE stanford > 15;
2003
2014
SQL operators
Expressions in the SELECT
, WHERE
, and ORDER BY
clauses can contain
one or more of the following operators:
- comparison operators:
=
,>
,<
,<=
,>=
,<>
or!=
("not equal") - boolean operators:
AND
,OR
- arithmetic operators:
+
,-
,*
,/
- concatenation operator:
||
Here are some examples:
Output the ratio of Berkeley's score to Stanford's score each year:
sqlite> select berkeley * 1.0 / stanford from big_game;
0.447368421052632
1.75
4.28571428571429
Output the sum of scores in years where both teams scored over 10 points:
sqlite> select berkeley + stanford from big_game where berkeley > 10 and stanford > 10;
55
44
Output a table with a single column and single row containing the value "hello world":
sqlite> SELECT "hello" || " " || "world";
hello world
Data
In each question below, you will define a new table based on the following tables.
create table parents as
select "abraham" as parent, "barack" as child union
select "abraham" , "clinton" union
select "delano" , "herbert" union
select "fillmore" , "abraham" union
select "fillmore" , "delano" union
select "fillmore" , "grover" union
select "eisenhower" , "fillmore";
create table dogs as
select "abraham" as name, "long" as fur, 26 as height union
select "barack" , "short" , 52 union
select "clinton" , "long" , 47 union
select "delano" , "long" , 46 union
select "eisenhower" , "short" , 35 union
select "fillmore" , "curly" , 32 union
select "grover" , "short" , 28 union
select "herbert" , "curly" , 31;
create table sizes as
select "toy" as size, 24 as min, 28 as max union
select "mini", 28, 35 union
select "medium", 35, 45 union
select "standard", 45, 60;
Your tables should still perform correctly even if the values in these tables change. For example, if you are asked to list all dogs with a name that starts with h, you should write:
select name from dogs where "h" <= name and name < "i";
Instead of assuming that the dogs
table has only the data above and writing
select "herbert";
The former query would still be correct if the name grover
were changed to
hoover
or a row was added with the name harry
.
In the following questions, update lab11.sql
with your
solution.
Question 3
Some people just love little dogs. Create ashort_dogs
table with
three columns, name
, fur
, and size
, containing the dogs of
height < 40 cm.
-- All short dogs
create table short_dogs as
-- REPLACE THIS LINE
select name, fur, height as size from dogs
where height < 40;
-- Example:
select name from short_dogs;
-- Expected output:
-- abraham
-- eisenhower
-- fillmore
-- grover
-- herbert
Test your solution with OK:
python3 ok -q short
Question 4
The Fédération Cynologique Internationale classifies a standard poodle as over 45 cm and up to 60 cm. Thesizes
table describes this and other such
classifications, where a dog must be over the min
and less than or equal to
the max
in height to qualify as a size
.
Create a size_of_dogs
table with two columns, one for each dog's name
and
another for its size
.
-- The size of each dog
create table size_of_dogs as
-- REPLACE THIS LINE
select name, size from dogs, sizes
where height > min and height <= max;
-- Example:
select name from size_of_dogs where size="toy" or size="mini";
-- Expected output:
-- abraham
-- eisenhower
-- fillmore
-- grover
-- herbert
Test your solution with OK:
python3 ok -q small
Question 5
Create a tableby_height
that has a column of the names of all dogs that have
a parent
, ordered by the height of the parent from tallest parent to shortest
parent.
-- All dogs with parents ordered by decreasing height of their parent
create table by_height as
-- REPLACE THIS LINE
select child from parents, dogs where name = parent order by -height;
For example, fillmore
has a parent (eisenhower
) with height 35, and so
should appear before grover
who has a parent (fillmore
) with height 32.
The names of dogs with parents of the same height should appear together in any
order. For example, barack
and clinton
should both appear at the end, but
either one can come before the other.
-- Example:
select * from by_height;
-- Expected output:
-- herbert
-- fillmore
-- abraham
-- delano
-- grover
-- barack
-- clinton
Test your solution with OK:
python3 ok -q parent_height
Question 6
Create a table tallest
that includes the height
and name
of every dog
that shares the 10's digit of its height
with at least one other dog and has
the highest 1's digit of all dogs that have the same 10's digit.
-- Height and name of every dog that shares height 10's digit
-- with at least one other dog and has the highest 1's digit of all dogs
-- that have the same 10's digit
create table tallest as
-- REPLACE THIS LINE
select max(height), name from dogs group by height/10 having count(*) > 1;
-- Example:
select * from tallest;
-- Expected output:
-- 28|grover
-- 35|eisenhower
-- 47|clinton
Explanation: grover
is the tallest 20-something dog. eisenhower
is the
tallest 30-something dog. clinton
is the tallest 40-something dog. barack
is not included because there are no other 50-something dogs.
Test your solution with OK:
python3 ok -q tallest