In Python, you may use if
and else
inside an expression;
this is called a conditional expression.
For example, consider this code:
if x > 100: print('x is big') else: print('x is not so big')
We may rewrite it using a conditional expression:
print('x is ' + ('big' if x > 100 else 'not so big'))
As another example, here is a function that takes two integers and returns whichever has the greater magnitude (i.e. absolute value):
def greater(i, j): if abs(i) >= abs(j): return i else: return j
We can rewrite it using a conditional expression:
def greater(i, j): return i if abs(i) >= abs(j) else j
A conditional expression always has the form
expression
if
condition else
expression
The
keyword else
must
always be present (unlike in an ordinary if
statement).
(If you already know C or another C-like language, you may notice this feature's resemblance to the ternary operator (? :) in those languages.)
The open() function opens a file. It takes two parameters: a filename plus an optional mode string, which can contain one or more of these characters:
'r'
– open for
reading (default)
'w'
– open for
writing. If the file already exists, its existing contents will be
lost!
'a'
– append
to an existing file, or create a new file if the file does not exist
already
't'
– text
mode (default)
'b'
– binary
mode
If you don't specify a mode string, the file will be opened for reading in text mode.
When you open a file in text mode, Python will perform universal newline translation, i.e. translate any sort of newlines to '\n'. We will only use text mode in this course.
open() returns a file object. When reading,
the file object is iterable: you can loop over it with for
to receive a sequence of lines, just like with sys.stdin. When a file
is opened for writing, you may
write lines to the file by calling the write() method on the file
object once for each output line.
When we are finished reading or writing data, you should call close() to close the file.
Here's a program that opens two files called 'input.txt' and 'output.txt'. It reads lines from the input file and writes them to the output file, duplicating every line as it writes:
input = open('input.txt', 'r') # open for reading ('r' is default mode) output = open('output.txt', 'w') # open for writing for line in input: output.write(line) # write line to output output.write(line) # write it again input.close() output.close()
You can read about more methods for input and output in our Python quick library reference.
with
statementWhenever
you open a file, you'll want to close it, even if your program exits
unexpectedly with an error. Python has a special statement that makes
this easy. The with
statement assigns a file
object (or other resource) to a variable, then runs a block of code.
When the block of code exits for any reason, the object is
automatically closed, just as if you had called close() on the
object.
Let's
rewrite the previous program using 'with'
:
with open('input.txt', 'r') as input: # open for reading ('r' is default mode) with open('output.txt', 'w') as output: # open for writing for line in input: output.write(line) # write line to output output.write(line) # write it again
It's good practice to use 'with' whenever you open a file, to ensure that the file will be closed even if the program exits with an error.
In fact with
also works with other types of objects that need to be closed, such
as network sockets. We may see some of these later in this course.
When we run a program from the command line, we may specify command-line arguments. For example:
$ python prog.py hello one two three
In a Python program, sys.argv holds a list of all command-line arguments. Actually the first element in the list is the name of the program itself, and subsequent elements hold the arguments. For example, suppose that prog.py holds this program:
import sys print(sys.argv)
Let's run it:
$ python3 prog.py hello one two three ['prog.py', 'hello', 'one', 'two', 'three']
As an example, let's write a program that imitates the Unix utility 'wc' ("word count"), which prints out the number of lines, words, and characters in a file:
import sys if len(sys.argv) != 2: print(f'usage: {sys.argv[0]} <filename>') exit() filename = sys.argv[1] chars = words = lines = 0 with open(filename) as f: for line in f: chars += len(line) words += len(line.split()) lines += 1 print(f'{lines} lines, {words} words, {chars} chars')
Notice that the program prints out a usage message if we attempt to invoke it without arguments. This is a good practice:
$ python wc.py usage: wc.py <filename>
Let's use the program to count the lines, words and characters in its own source code:
$ python wc.py wc.py 16 lines, 43 words, 324 chars
This matches the counts printed by the Unix wc utility itself:
$ wc wc.py 16 43 324 wc.py
Command-line programs will often accept arguments with names beginning with a hyphen, such as -d or ‑e. Typically these denote options that affect a program's behavior.
As another example, let's implement a simple variant of the Unix utility 'grep', which searches for a string in a given file, and prints out all lines containing the string. Our program will accept two options:
-i
: search
case-insensitively, i.e. "horse" will match "HoRsE"
-v
: invert
matches, i.e. print out lines that do not contain the given
string
Here's the program:
import sys def usage(): print(f'usage: {sys.argv[0]} [-i] [-v] <string> <filename>') exit() insensitive = invert = False i = 1 while i < len(sys.argv): arg = sys.argv[i] if arg.startswith('-'): if arg == '-i': insensitive = True elif arg == '-v': invert = True else: usage() else: break # end of options i += 1 if len(sys.argv) - i < 2: usage() find = sys.argv[i] if insensitive: find = find.lower() filename = sys.argv[i + 1] with open(filename) as f: for line in f: line = line.rstrip() if insensitive: line = line.lower() match = find in line if (match and not invert) or (not match and invert): print(line)
Let's use it to find all lines containing "sys" in the program's own source code:
$ python grep.py -i sys grep.py import sys print(f'usage: {sys.argv[0]} [-i] [-v] <string> <filename>') while i < len(sys.argv): arg = sys.argv[i] if len(sys.argv) - i < 2: find = sys.argv[i] filename = sys.argv[i + 1]
A set in Python is a useful built-in data structure that represents a mutable set of values. The function set() returns an empty set:
>>> set() set()
Alternatively, you may construct a set of values by listing them within braces:
>>> s = {3, 5, 10}
By the way, do not attempt to create an empty set by writing '{}'
.
That is an empty dictionary, not a set! (We will discuss dictionaries
below.)
We may add a value to a set using the add() method. If the value is already present, the call does nothing:
>>> s.add(4) >>> s {10, 3, 4, 5} >>> s.add(4) >>> s {10, 3, 4, 5}
The remove() method removes a value from a set. If it's not present, Python will report an error:
>>> s.remove(5) >>> s {10, 3, 4} >>> s.remove(5) Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 5
The 'in' operator tests whether a value is present in a set:
>>> s = { 'red', 'green', 'yellow', 'turquoise' } >>> 'yellow' in s True >>> 'purple' in s False
All of these operations are efficient: add(), remove(), and 'in' will run in O(1) on average. For this reason, a set is sometimes a better choice of data structure than a list, especially in situations when we want to be able to quickly test whether an element is present.
However, be aware that sets are unordered. In other words, the values in a set are not in any particular order, and you cannot access values by index:
>>> s = { 'red', 'green', 'yellow', 'turquoise' } >>> s[2] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'set' object is not subscriptable
Python will print a set's values in an arbitrary order, unrelated to the order in which values were added:
>>> s = set() >>> s.add(5) >>> s.add(2) >>> s.add(10) >>> s {2, 10, 5}
Sets are iterable, meaning that you can use a 'for' statement to loop over a set's values (in some arbitrary order):
>>> s = { 'red', 'green', 'yellow', 'turquoise' } >>> for c in s: ... print(c) ... green turquoise red yellow
Note that every value in a set must be immutable. So you cannot create a set of lists, for example:
>>> s = { [1, 2, 3], [6, 5, 8], [10, 11, 12] } Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list'
However, a set of tuples will work fine, since tuples are immutable:
>>> s = { (1, 2, 3), (6, 5, 8), (10, 11, 12) } >>> s {(6, 5, 8), (1, 2, 3), (10, 11, 12)} >>>
Python provides handy operators for performing Boolean operations on sets. The '&', '|', and '-' operators compute the intersection, union, or difference of two sets:
>>> s = { 5, 8, 2, 10, 3, 6, 12 } >>> t = { 8, 77, 3, 14, 12 } >>> s & t {8, 3, 12} >>> s | t {2, 3, 5, 6, 8, 10, 12, 77, 14} >>> s - t {2, 10, 5, 6}
The set operators 's <= t' and 's >= t' test whether s is a subset or superset of t, respectively:
>>> s = { 2, 4, 6 } >>> t = { 1, 2, 3, 4, 5, 6, 8 } >>> s <= t True >>> t <= s False
For more information about built-in operators and functions that work on sets, see our quick library reference.
A dictionary is a built-in Python data type that represents a mutable map from keys to values. Dictionaries are useful in many situations.
Here's a dictionary that maps the names of some animals in English to their Czech equivalents:
>>> d = {'cat' : 'kočka', 'dog' : 'pes', 'guinea pig' : 'morče'}
The keys in this dictionary are the strings 'cat'
,
'dog'
and 'guinea
pig'
. The corresponding values are 'kočka'
,
'pes'
and 'morče'
.
Notice that each key and its value are separated by a colon (':') in
the declaration above.
We may look up the value for any key:
>>> d['dog'] 'pes' >>> d['guinea pig'] 'morče'
We can use an assignment to add a new key/value pair to the dictionary:
>>> d['cow'] = 'kráva' >>> d {'cat': 'kočka', 'dog': 'pes', 'guinea pig': 'morče', 'cow': 'kráva'}
We may use the same syntax to replace the value at any key:
>>> d['cat'] = 'gato' >>> d {'cat': 'gato', 'dog': 'pes', 'guinea pig': 'morče', 'cow': 'kráva'}
The del
statement will delete a key and
its value from a dictionary:
>>> del d['dog'] >>> d {'cat': 'gato', 'guinea pig': 'morče', 'cow': 'kráva'}
The in
operator tests whether a key is
present in a dictionary:
>>> 'guinea pig' in d True >>> 'horse' in d False
All of these operations are efficient. Looking up a key's value, replacing a key's value, deleting a key, and testing for a key will all run in O(1) on average.
Notice that all keys in a dictionary are unique. Values, however, may be repeated. Also note that dictionaries are indexed by key, but not by value. In other words, you can quickly look up the value for any key, but if you want to know whether a particular value is present in the dictionary, you must scan the entire dictionary, which takes O(N).
Python provides methods that let us conveniently access the keys and values in a dictionary. First, d.keys() returns an iterable object containing all keys in the dictionary. You can iterate over this object using 'for', or convert it to a list:
>>> d = {'cat' : 'kočka', 'dog' : 'pes', 'guinea pig' : 'morče'} >>> for k in d.keys(): print(k) cat dog guinea pig >>> list(d.keys()) ['cat', 'dog', 'guinea pig']
Similarly, d.values() lists all values in a dictionary. Another helpful method is d.items(), which is an iterable of key‑value pairs:
>>> list(d.items()) [('cat', 'kočka'), ('dog', 'pes'), ('guinea pig', 'morče')]
Recall that in a 'for' statement we may use pattern matching to assign to multiple variables on each loop iteration:
>>> for x, y in [(1, 2), (3, 4), (5, 6)]: print(x + y) 3 7 11
And so we may conveniently use pattern matching to loop over the keys and values of a dictionary simultaneously:
>>> d {'cat': 'kočka', 'dog': 'pes', 'guinea pig': 'morče'} >>> for key, value in d.items(): print(f'key = {key}, value = {value}') key = cat, value = kočka key = dog, value = pes key = guinea pig, value = morče
Note that dictionary keys, like values in a set, must be immutable. So you can't create a dictionary whose keys are lists:
>>> d = { [1, 2, 3]: 7, [5, 3, 2]: 8 } Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list'
However, tuples will work fine as dictionary keys:
>>> d = { (1, 2, 3): 7, (5, 3, 2): 8 }
See our quick library reference for more information about built-in operators and functions that work on dictionaries.
As an example of the practical utility of dictionaries, suppose that we'd like to determine which words in a file appear the most times. For example, here is the entire text of the classic novel War and Peace. Let's write a program to read the text and print out its most common words. We'll only consider words with at least 5 letters, since shorter common words (e.g. 'the', 'a') are not usually so interesting.
We'll assume that the text is in a file 'war_and_peace'. We'll read the file and construct a dictionary that maps each word to its count. After that, we'll extract a list of pairs (count, word) from the dictionary. Then we'll sort the list. Python orders tuples lexicographically, so that will sort by count since the count appears first in each pair.
Here is the program:
f = open('war_and_peace') count = {} for line in f: words = line.strip().split() for w in words: if len(w) >= 5: if w in count: count[w] += 1 else: count[w] = 1 # Extract a list of pairs. pairs = [] for word, n in count.items(): pairs.append( (n, word) ) pairs.sort(reverse = True) for n, word in pairs[:10]: print(f'{word}: {n}')
Let's run it:
$ py words.py which: 1924 Prince: 1499 their: 1407 would: 1333 Pierre: 1260 could: 1090 about: 908 there: 895 CHAPTER: 730 Andrew: 700
A recursive function calls itself. Recursion is a powerful technique that can help us solve many problems.
When we solve a problem recursively, we express its solution in terms of smaller instances of the same problem. The idea of recursion is fundamental in mathematics as well as in computer science.
As a first example, suppose we we'd like to write a function that computes n! for any positive integer n. Recall that
n! = n · (n – 1) · … · 3 · 2 · 1
We may write this function iteratively, i.e. using a loop:
def factorial_iter(n): p = 1 for i in range(1, n + 1): p *= i return p
Alternatively, we can rewrite this function using recursion. Now it calls itself:
def factorial(n): if n == 0: return 1 # 0! is 1 return n * factorial(n - 1)
Whenever we write a recursive function, there is a base case and a recursive case.
The base case is an instance that we can solve immediately. In the function above, the base case is when n == 0. A recursive function must always have a base case – otherwise it would loop forever since it would always call itself!
In the recursive case, a function calls itself, passing it a smaller instance of the given problem. Then it uses the return value from the recursive call to construct a value that it itself can return.
In the recursive case in this example, we
recursively call factorial(n -
1).
We then multiply its return value by n, and
return that value. That works because
n! = n · (n – 1)! whenever n ≥ 1
(In fact, we may treat this equation as a recursive definition of the factorial function, mathematically speaking.)
How does this function work, exactly? Suppose that we call factorial(3). It will call factorial(2), which calls factorial(1), which calls factorial(0). The call stack in memory now looks like this:
factorial(3) → factorial(2) → factorial(1) → factorial(0)
Now
factorial(0) returns 1 to its caller factorial(1).
factorial(1) takes this value 1, multiplies it by n = 1 and returns 1 to factorial(2).
factorial(2) takes this value 1, multiplies it by n = 2 and returns 2 to factorial(3).
factorial(3) takes this value 2, multiplies it by n = 3 and returns 6.
We see that given any n, the function returns the sum 1 * 2 * 3 * … * n = n!.
We have seen that we can write the factorial function either iteratively (i.e. using loops) or recursively. In theory, any function can be written either iteratively or recursively. We will see that for some problems a recursive solution is easy and an iterative solution would be quite difficult. Conversely, some problems are easier to solve iteratively. Python lets us write functions either way.
Here is some general advice for writing a recursive function. First look for a base case where the function can return immediately. (In some problems there will actually be more than one base case.) Now you need to write the recursive case, where the function calls itself. At this point you may wish to pretend that the function "already works". Write the recursive call and believe that it will return a correct solution to a subproblem, i.e. a smaller instance of the problem. Now you must somehow transform that subproblem solution into a solution to the entire problem, and return it. This is really the key step: understanding the recursive structure of the problem, i.e. how a solution can be derived from a subproblem solution.
Here's another example. Consider this implementation of Euclid's algorithm that we saw in Introduction to Algorithms:
# gcd, iteratively def gcd(a, b): while b > 0: a, b = b, a % b return a
We can rewrite this function using recursion:
# gcd, recursively def gcd(a, b): if b == 0: return a return gcd(b, a % b)
Broadly speaking, we will see that "easy" recursive
functions such as gcd
call themselves
only once, and it would be straightforward to write them either
iteratively or recursively. Soon
(in our algorithms class) we will see recursive functions that
call themselves two or more times. Those functions will let us solve
more difficult tasks that we could not easily solve iteratively.
Here is another example of a recursive function:
def hi(x): if x == 0: print('hi') return print('start', x) hi(x - 1) print('done', x)
If we call hi(3), the output will be
start 3 start 2 start 1 hi done 1 done 2 done 3
Be sure you understand why the lines beginning with 'done' are printed. hi(3) calls hi(2), which calls hi(1), which calls hi(0). At the moment that hi(0) runs, all of these function invocations are active and are present in memory on the call stack:
hi(3) → hi(2) → hi(1) → hi(0)
Each function invocation has its own value of the parameter x. (If this procedure had local variables, each invocation would have a separate set of variable values as well.)
When hi(0) returns, it does not exit from this entire set of calls. It returns to its caller, i.e. hi(1). hi(1) now resumes execution and writes 'done 1'. Then it returns to hi(2), which writes 'done 2', and so on.