Chapter 4#
ALE 4.1: This and that#
Python allows you to assign multiple values to multiple names in one assignment-statement. Here are the semantics:
The lefthand side of the assignment operator must be a comma-separated list of names.
The righthand side of the assignment operator can be
a comma-separated values, or
a value that adheres to the sequence abstraction.
The first value on the left is given the first name on the right; the second on the left is given the second on the right; and so forth. For the operation to complete without an error, the comma-separated lists on both sides of the assignment operator must have the same number of elements. Similarly, the length of the sequence value on the right must be the same as the number of comma-separated names on the left.
This is not an operation you’d want to do all the time, since it can quickly become confusing to lump different things together in a single assignment statement. There are, however, two cases where this functionality is very helpful:
1# CASE 1: Unpacking a sequence into its component pieces
2# so that you can operate on each separately
3answer = '16.83 million books are in the Harvard library'
4
5num, magnitude, units = answer.split()[0:3]
6
7print("num =", num)
8print("magnitude =", magnitude)
9print("units =", units)
1# CASE 2: Swapping two values
2x = 0
3y = 1
4
5x, y = y, x
6
7print(f'x = {x}, y = {y}')
Step 1. Write an equivalent sequence of statements to implement the swapping of two values without using multiple assignment.
1# Swapping two values without multiple assignment
2x = 0
3y = 1
4
5# YOUR CODE HERE
6
7print(f'x = {x}, y = {y}')
Step 2. Look at the code in CASE 2 and the code that you wrote. Which is easier to understand?
ALE 4.2: Tuples, a box for everything#
A tuple is yet another kind of Python sequence. They may look like a Python list
, but they are immutable.
Be careful. Tuples are always printed with surrounding parentheses, but you don’t need parentheses to create a tuple. A comma is sufficient.
1t1 = 'is blue', 'Who changed the sky?!?'
2print(t1)
3
4t2 = 'is blue', 1
5print(t2)
6
7t3 = 0, 1
8print(t3)
9
10t4 = (0, 1)
11print(t4)
12
13print(t3 == t4)
Step 1. Run the code in the code block above. Notice the different ways you can make a tuple.
Step 2. And here is some stuff not to do. Execute each code statement and talk with your partner(s) about what’s wrong.
# an error
x, y = 0, 1, 2
# not an error, but hard to understand
x, y = 0, (1, 2)
Step 3. We’ve been looking at tuples with just two values in them, but a tuple can also contain a single value or many values.
1a_big_tuple = 'a', 2, 'c', 4
2print(a_big_tuple)
3
4a_big_tuple = 'a', 2, 'c', 4,
5print(a_big_tuple)
6
7a_big_tuple = ('a', 2, 'c', 4,)
8print(a_big_tuple)
Notice that you can add a comma after the last element in a tuple, and the Python interpreter happily throws it away. However, you need this to be able to define a tuple with a single element:
1a_tuple = 0,
2print(a_tuple)
3
4not_a_tuple = (0)
5print(not_a_tuple)
6
7print(a_tuple == not_a_tuple)
Before you execute the following code block, answer for yourself if the expression that is the parameter to print
will evaluate True
or False
, given the definitions of the variables from above.
Now run the code block to check your work.
print(a_tuple[0] == not_a_tuple)
Tip
We’ve played around with the syntax of tuples, and now I’ll mention one reason why they are useful. One of their most convenient uses is to return multiple values from a function. Remember that a function returns only a single object. A tuple is a convenient way to box up every result you have computed in a function and return them in a single object, which the caller can pull apart using multiple assignment!
ALE 4.3: But it must be true!#
When you’re first coding a solution to a problem, you will find yourself making assumptions. You could document them in a comment, but it is often more helpful to include an assert-statement in your code.
The assert-statement takes a condition, which is what you expect to be True
every time you hit this point in your code. As long as the condition evaluates True
, execution proceeds as if the assert-statement wasn’t there (i.e., your assumption holds). However, if the assert condition evaluates to False
, the statement throws an AssertionError
, which tells you that something about your thinking was wrong.
Step 1. To experience an assertion failure, run the following silly example.
1the_sky = 'is red'
2assert(the_sky == 'is blue')
3print('It must be because execution got here!')
Step 2. Be careful with your parentheses in this statement. assert
is not a command, but a kind of Python statement. We can rewrite the code block above in the follow ways:
1# Adds a space after `assert`
2the_sky = 'is red'
3assert (the_sky == 'is blue')
4print('It must be because execution got here!')
1# Removes the parentheses on the assert condition
2the_sky = 'is red'
3assert the_sky == 'is blue'
4print('It must be because execution got here!')
Run the two code blocks to prove to yourself that they’re all equivalent.
Step 3. An assert-statement also takes an optional second argument, which is the string you’d like printed when the assertion fails. Make sure you see the difference between the first assert-statement, which doesn’t do what we want, and the two that follow, which do.
1# The INCORRECT way to include an assert message
2the_sky = 'is red'
3assert(the_sky == 'is blue', 'Who changed the sky?!?')
4print('It must be because execution got here!')
1# The CORRECT way to include an assert message
2the_sky = 'is red'
3assert the_sky == 'is blue', 'Who changed the sky?!?'
4print('It must be because execution got here!')
1# The CORRECT way to use parentheses around the assert condition
2the_sky = 'is red'
3assert (the_sky == 'is blue'), 'Who changed the sky?!?'
4print('It must be because execution got here!')
Step 4. Where you put the closing parenthesis makes all the difference. Keep in mind that parentheses mean many different things in Python depending upon the context, as we just saw with tuples!
1# Parentheses surrounding a comma-separated list of objects
2# AFTER A FUNCTION NAME mean: "Here are the arguments."
3print(the_sky == 'is blue', 'Who changed the sky?!?')
4
5# Parentheses surrounding a comma-separated list of objects
6# IN SOME OTHER CONTEXTS mean: "Make a tuple."
7a_tuple = ('is blue', 'Who changed the sky?!?')
8print(a_tuple)
Can you explain what was wrong in this INCORRECT use of assert?
assert(the_sky == 'is blue', 'Who changed the sky?!?')
ALE 4.4: A Mashup#
Let’s combine what we learned in the ALEs 4.1-4.3 with the basics of web programming from the chapter to create what’s called a mashup.
The qweb8.py
script at the end of the chapter queries a single web resource. Had we programmed it to query multiple resources, we would written what’s called a mashup: an application that grabs content from multiple web services to power its own new service. We can build an amazing range of applications by taking advantage of the information available to us around the Internet. Look at what the researchers at IBM accomplished with Watson, a software program running on 100 servers that was able to quickly sift through 200 million pages of information. In 2011, IBM Watson took on the best-ever human contestants in the TV quiz show Jeopardy, and it beat them.
Today, we talk to our cell phones and home appliances running software agents (e.g., Apple’s Siri, Amazon’s Alexa, Google’s Assistant, and Microsoft’s Cortana), which do much the same work: You ask them a question. They analyze and parse the wave form that is your question, trying to understand what you want to know. Then they query many different resources around the Internet and evaluate what they get back to determine if the information is pertinent to the question they think you asked. Finally, they take the best ranked response and present it to you.
While these software agents are quite sophisticated, we can build a script that does mashup work. In particular, let’s say we want to know: does Harvard Library contain more books than Wikipedia has articles?
The script mashup32.py
(listed below) breaks this question into its two pieces and asks each to Wolfram Alpha, a site on the Internet with the goal of accepting free-form questions and returning relevant and clearly presented answers. We can think of it as a voice assistant without the voice interface.
1### chap04/mashup32.py -- not executable without a Wolfram Alpha dev key
2import requests
3import xmltodict
4
5def walpha(query, appid):
6 print(f'Asking Wolfram|Alpha "{query}" ...')
7 response = requests.get(
8 'http://api.wolframalpha.com/v2/query',
9 params={'input': query,
10 'appid': appid,
11 'podtitle': 'Result'
12 }
13 )
14 assert(response.status_code == 200)
15
16 jlike_response = xmltodict.parse(response.text)
17 answer = jlike_response["queryresult"]["pod"]["subpod"]["plaintext"]
18 print(f'... answered "{answer}"')
19 return answer
20
21def main():
22 # Compare wikipedia's and Harvard library's knowledge base.
23
24 # Grab Mike's Wolfram Alpha develop key
25 with open('walpha-id.txt') as f:
26 appid = f.readline().strip()
27
28 h_query = 'how many books are in the harvard library'
29 h_answer = walpha(h_query, appid)
30 h_num, h_magnitude, h_units = h_answer.split()[0:3]
31
32 w_query = 'how big is wikipedia'
33 w_answer = walpha(w_query, appid)
34 w_num, w_magnitude, w_units = w_answer.split()[0:3]
35
36 assert(h_magnitude == w_magnitude)
37 h_num, w_num = float(h_num), float(w_num)
38 if h_num > w_num:
39 print(f'Harvard Library has more {h_units} ({h_num} {h_magnitude})',
40 f'than Wikipedia has {w_units} ({w_num} {h_magnitude}).')
41 elif h_num < w_num:
42 print(f'Wikipedia has more {w_units} ({w_num} {w_magnitude})',
43 f'than Harvard Library has {h_units} ({h_num} {h_magnitude}).')
44 else:
45 print("They're the same size ({h_units} and {w_units})!")
46
47if __name__ == '__main__':
48 main()
Step 1. See if you can understand the general logic of this script. It does contain two programming language features we haven’t studied yet:
It accepts and manipulates floating-point numbers, which are simply numbers with a decimal point and possibly a fractional part. We’ll cover this data type in Chapter 7.
The Wolfram Alpha API also doesn’t allow me to choose the format I’d like for the results; it only returns results in XML. No problem! I just looked up and used a library that converted this XML into a JSON-like dictionary.
Step 2. Would you like to run mashup32.py
? If you try, you’ll find that you don’t have a file called walpha-id.txt
. This file exists in my personal world — I have a Wolfram Alpha developer appid
. Without an appid
, the Wolfram Alpha API won’t accept your query. I’d say most sites with APIs require you to register as a developer so that they can identify bad actors (e.g., someone who tries to overload their servers).
If you want to build your own mashup scripts, just realize that you’ll probably have to register and receive an appid
on the sites you use. Since this appid
is specific to you, you shouldn’t share it with others or store it in public code repositories like GitHub. You’ll notice that I grab mine from a local file on my machine so that I can share with you my script, but not my appid
. You too should do something like this when you write your scripts.
Tip
Don’t ever write your private information as a literal in the scripts you develop or store it in GitHub.
Step 3 (optional). What might you try to do now that you have some skill in web programming? For example, mashup32.py
uses the size of the English version of Wikipedia. You may consider this unfair because Harvard Library’s book count includes books written in many different languages. You could sign up for your own Wolfram Alpha appid
and change mashup32.py
to ask your own version of the question. Perhaps a software agent of this type might make an exciting project for you.
ALE 4.5: JSON makes my eyes ache#
The qweb7.py
script illustrated how we can pull apart a response from Harvard’s LibraryCloud API and determine if the Harvard Library system has a copy of The Cat in the Hat by Dr. Seuss. But this script printed only the titles of each resource in the response.
I’ve copied qweb7.py
below, except that it asks for the top 4 results. See if you can extend the code in the following ways. Each will require you to read through the dumped JSON response and figure out the sequence of indexing operations you need to do to access the information you need to print. Be careful as some JSON fields may be a Python dictionary in one item and a Python list in another, as our code illustrated with the 'titleInfo'
field.
Print the author’s name.
Print the type of resource (i.e., the item is a text or a still image).
If the resource is a text and the item includes an abstract, print it. The abstract for The Cat in the Hat is “Two children sitting at home on a rainy day are visited by the Cat in the Hat who shows them some tricks and games.”
1### chap04/qweb7.py
2import requests
3import json
4
5def main():
6 print('Searching HOLLIS for "The Cat in the Hat"')
7
8 # Concatenate the first 3 components of a URL for HTTP
9 protocol = 'https'
10 hostname = 'api.lib.harvard.edu'
11 path = '/v2/items.json'
12 url = protocol + '://' + hostname + path
13
14 # Describe the query string as a Python dictionary
15 query = {'q': 'The Cat in the Hat',
16 'limit': 4
17 }
18
19 # Add a field to the request header saying what we accept
20 accept = {'Accept': 'application/json'}
21
22 response = requests.get(url, params=query, headers=accept)
23
24 # Read the response body in JSON format and print it
25 j = response.json()
26 print("response.json() =", json.dumps(j, indent=4))
27
28 print()
29
30 if j['pagination']['numFound'] == 0:
31 print('Zero results')
32 else:
33 # Process each returned response
34 for i, item in enumerate(j['items']['mods']):
35 # Print title info
36 ti = item['titleInfo']
37 if type(ti) == list:
38 # Lots of title info; just print the first
39 ti = ti[0]
40 print(f"Title #{i}: ", end='')
41 if 'nonSort' in ti:
42 print(ti['nonSort'], end='')
43 print(ti['title'])
44
45if __name__ == '__main__':
46 main()
[Version 20240111]