Book Image

IPython Notebook Essentials

By : Luiz Felipe Martins
Book Image

IPython Notebook Essentials

By: Luiz Felipe Martins

Overview of this book

Table of Contents (15 chapters)
IPython Notebook Essentials
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Control structures


Control structures allow changes to the flow of the execution of code. There are two types of structures that are of interest to us: branching and looping.

Branching allows the execution of different code depending on the result of a test. The following example shows an improved version of code to solve quadratic equations. An if-then-else structure is used to handle the cases of real and imaginary solutions, as follows:

a, b, c = 2., -4., 5.
discr = b ** 2 - 4 * a * c
if discr >= 0:
    sqroot = discr ** 0.5
    x1 = 0.5 * (-b + sqroot)
    x2 = 0.5 * (-b - sqroot)
else:
    sqroot = (-discr) ** 0.5
    x1 = 0.5 * (-b + sqroot * 1j)
    x2 = 0.5 * (-b - sqroot * 1j)
print x1, x2

The preceding code starts by computing the discriminant of the quadratic. Then, an if-then-else statement is used to decide if the roots are real or imaginary, according to the sign of the discriminant. Note the indentation of the code. Indentation is used in Python to define the boundaries of blocks of statements. The general form of the if-then-else structure is as follows:

if <condition>:
     <statement block T>
else:
     <statement block F>

First, the condition <condition> is evaluated. If it is True, the statement <statement block T> is executed. Otherwise, <statement block F> is executed. The else: clause can be omitted.

The most common looping structure in Python is the for statement. Here is an example:

numbers = [2, 4, 3, 6, 2, 1, 5, 10]
for n in numbers:
    r = n % 2
    if r == 0:
        print 'The integer {:d} is even'.format(n)
    else:
        print 'The integer {:d} is odd'.format(n)

We start by defining a list of integers. The for statement makes the variable n assume each value in the list numbers in succession and execute the indented block for each value. Note that there is an if-then-else structure inside the for loop. Also, the print statements are doubly-indented.

A for loop is frequently used to perform simple searches. A common scenario is the need to step out of the loop when a certain condition is met. The following code finds the first perfect square in a range of integers:

for n in range(30, 90):
    if int(n ** 0.5) ** 2 == n:
        print n
        break

For each value of n in the given range, we take the square root of n, take the integer part, and then calculate its square. If the result is equal to n, then we go into the if block, print n, and then break out of the loop.

What if there are no perfect squares in the range? Change the preceding function, range(30, 60), to range(125, 140). When the command line is run, nothing is printed, since there are no perfect squares between 125 and 140. Now, change the command line to the following:

for n in range(125, 140):
    if int(n ** 0.5) ** 2 == n:
        print n
        break
else:
    print 'There are no perfect squares in the range'

The else clause is only executed if the execution does not break out of the loop, in which case the message is printed.

Another frequent situation is when some values in the iteration must be skipped. In the following example, we print the square roots of a sequence of random numbers between -1 and 1, but only if the numbers are positive:

import random
numbers = [-1 + 2 * rand() for _ in range(20)]
for n in numbers:
    if n < 0:
        continue
    print 'The square root of {:8.6} is {:8.6}'.format(n, n ** 0.5)

When Python meets the continue statement in a loop, it skips the rest of the execution block and continues with the next value of the control variable.

Another control structure that is frequently used is the while loop. This structure executes a block of commands as long as a condition is true. For example, suppose we want to compute the running sum of a list of randomly generated values, but only until the sum is above a certain value. This can be done with the following code:

import random
bound = 10.
acc = 0.
n = 0
while acc < bound:
    v = random.random()
    acc += v
    print 'v={:5.4}, acc={:6.4}'.format(v, acc)

Another common situation that occurs more often than one might expect requires a pattern known as the forever loop. This happens when the condition to be checked is not available at the beginning of the loop. The following code, for example, implements the famous 3n+1 game:

n = 7
while True:
    if n % 2 == 0:
        n /= 2
    else:
        n = 3 * n + 1
    print n
    if n == 1:
        break

The game starts with an arbitrary integer, 7 in this case. Then, in each iteration, we test whether n is even. If it is, we divide it by 2; otherwise, multiply it by 3 and add 1. Then, we check whether we reached 1. If yes, we break from the loop. Since we don't know if we have to break until the end of the loop, we use a forever loop as follows:

while True:
   <statements>
   if <condition>:
        break
   <possibly more statements>

Some programmers avoid this construct, since it may easily lead to infinite loops if one is careless. However, it turns out to be very handy in certain situations. By the way, it is an open problem if the loop in the 3n+1 problem stops for all initial values! Readers may have some fun trying the initial value n=27.

Functions, objects, and methods

We now come to the constructs that really make Python so flexible and powerful, its object-oriented features. We have already seen some examples of object-oriented code in the previous sections (the object-oriented paradigm is so integral to Python that is hardly possible to write any code without using it), but now we will have a more specific treatment of these features.

Functions

We have already seen many examples of functions being used. For example, the len() function is used to compute the length of a list:

lst = range(1000)
print len(lst)

The most basic syntax for calling a function is as follows:

function_name(arg1, arg2, …, argn)

In this case, arg1, arg2, …, argn are called positional arguments, since they are matched according to the position in which they appear. As an example, let's consider the built-in function, pow(). This function takes up to three arguments:

pow(b, n, m)

In this form, the preceding function uses an optimized algorithm to compute b raised to the power n modulo m. (If you are wondering, this is an important operation in public key cryptography, for example.) The arguments b, n, and m are associated by their position. For example, to compute 12 raised to the tenth power modulo 15, we use the following command:

pow(12, 10, 15)

Python also supports sequences of arguments of arbitrary size. For example, the max() function computes the maximum of an arbitrary sequence of values:

max(2,6,8,-3,3,4)

The preceding command returns the value 8.

A third way to pass arguments to a function is to use keyword arguments. This turns out to be very useful, since it is in general difficult to remember the exact order of positional arguments. (I would prefer not to write a function with more than three or four positional arguments, for example.)

For example, the built-in int() function can be used to convert a string to an integer. The optional keyword argument, base, lets us specify the base for conversion. For example, the following command line assigns to n, an integer given in base 2:

n = int('10111010100001', base=2)
print n

Keyword arguments always have a default value. In our example, if the base is not specified, it is assumed to be 10.

We often need to write our own functions. This is done with the keyword, def. As an example, let's consider writing code to implement the well-known bisection method to solve equations numerically. A possible solution is as follows:

def bisection(f, a, b, tol=1e-5, itermax=1000):
    fa = f(a)
    fb = f(b)
    if fa * fb > 0:
        raise ValueError('f(a) and f(b) must have opposite signs')
    niter = 0
    while abs(a-b) > tol and niter < itermax:
        m = 0.5 * (a + b)
        fm = f(m)
        if fm * fa < 0:
            b, fb = m, fm
        else:
            a, fa = m, fm
    return min(a, b), max(a, b)

The preceding function takes three important and necessary arguments:

  • The f function accepts a float value as input and returns a float value as output

  • The floating-point values, a and b, which specify an interval that contains a zero of the function

The other two arguments are optional. The argument tol specifies the desired tolerance in the result and itermax specifies the maximum number of iterations. To use the bisection() function, we must first define the function f. We will take the opportunity to display another way to define a function in Python, as follows:

from math import cos, pi
f = lambda x: cos(x) - x

We are now ready to call the function with the following command:

bisection(f, 0, pi/2)

The preceding function returns the following output:

(0.7390851262506977, 0.7390911183631504)

Note that we designed the function to return an interval containing the zero. The length of the interval is less than tol, unless the maximum number of iterations is reached. If we want a smaller tolerance, we could use the following function:

bisection(f, 0, pi/2, tol=1E-10)

Now, suppose that we are concerned with the time the computation is taking. We can limit the maximum number as follows:

bisection(f, 0, pi/2, itermax=10, tol=1E-20)

Note that the order in which the keyword arguments are given is irrelevant and the desired tolerance is not reached in the preceding example.

Objects and methods

Objects are the most general data abstraction in Python. Actually, in Python, everything is an object from the point of view of the programmer.

An object is nothing more than a collection of structured data, together with an interface to operate on this data. Objects are defined using the class construct, but our goal here is not to show how to define classes. Although designing a new class is an advanced topic, using existing classes is pretty straightforward.

As an example, let's explore the built-in type str. Let's start by defining a str object we can play with as follows:

message = 'Mathematics is the queen of science'

To start, let's convert the message to uppercase as follows:

message.upper()

We say that the preceding statement calls the upper() method of the message object. A method is simply a function that is associated to an object. The following are a few other methods of the str objects:

  • To find the first occurrence of a substring (returns -1 if the string is not found), use the following command line:

    message.find('queen')
    
  • To split the string in words, use the following command line:

    words = message.split()
    print words
    
  • To count the number of occurrences of s substring, use the following command line

    message.count('e')
    
  • To replace a substring by something else, use the following command line:

    message.replace('Mathematics', 'Mme. Curie')
    

Note

Note that the preceding methods do not change the original string object, but return new modified strings. Strings are immutable. For mutable objects, methods are free to change the data in the object.