Learning Python 3 with the Linkbot/Dealing with the imperfect

...or how to handle errors edit

closing files with with edit

We use the "with" statement to open and close files.[1][2]

with open("in_test.txt", "rt") as in_file:
    with open("out_test.txt", "wt") as out_file:
        text = in_file.read()
        data = parse(text)
        results = encode(data)
        out_file.write(results)
    print( "All done." )

If some sort of error happens anywhere in this code (one of the files is inaccessible, the parse() function chokes on corrupt data, etc.) the "with" statements guarantee that all the files will eventually be properly closed. Closing a file just means that the file is "cleaned up" and "released" by our program so that it can be used in another program.


 

To do:
Is the "closing files with with" section too much detail for a non-programmers tutorial? If so, move it to some other Python Wikibook (Subject:Python programming language)


catching errors with try edit

So you now have the perfect program, it runs flawlessly, except for one detail, it will crash on invalid user input. Have no fear, for Python has a special control structure for you. It's called try and it tries to do something. Here is an example of a program with a problem:

print("Type Control C or -1 to exit")
number = 1
while number != -1:
   number = int(input("Enter a number: "))
   print("You entered:", number)

Notice how when you enter @#& it outputs something like:

Traceback (most recent call last):
 File "try_less.py", line 4, in <module>
   number = int(input("Enter a number: "))
ValueError: invalid literal for int() with base 10: '\\@#&'

As you can see the int() function is unhappy with the number @#& (as well it should be). The last line shows what the problem is; Python found a ValueError. How can our program deal with this? What we do is first: put the place where errors may occur in a try block, and second: tell Python how we want ValueErrors handled. The following program does this:

print("Type Control C or -1 to exit")
number = 1
while number != -1:
    try:
        number = int(input("Enter a number: "))
        print("You entered:", number)
    except ValueError:
        print("That was not a number.")

Now when we run the new program and give it @#& it tells us "That was not a number." and continues with what it was doing before.

When your program keeps having some error that you know how to handle, put code in a try block, and put the way to handle the error in the except block.

Generating Errors: Controlling a Linkbot's Speed edit

We've seen in previous examples that we can write a function that makes a wheeled robot travel a certain distance. We can also control the rotational velocity of the motors with the setJointSpeed() function. The setJointSpeed() function expects a rotational speed with units of degrees/sec, but it would be nice if we could have a function where we could set the robot speed using inches/sec. The math equation to convert   inches/sec to   degrees/sec is

 

where   is the wheel radius. Lets expand our example from the Learning Python 3 with the Linkbot/Defining Functions section:

import barobo
import math # So that we can use math.pi
dongle = barobo.Dongle()
dongle.connect() 
myLinkbot = dongle.getLinkbot('abcd') # Change abcd to your Linkbot's serial ID

def driveDistance(linkbot, distance):
    r = 3.5 / 2 # If you have a wheel that's not 3.5 inches in diameter, change "3.5" to the diameter of your wheel
    degrees = (360) / (2 * math.pi * r) * distance
    linkbot.move(degrees, 0, -degrees)

def setSpeed(linkbot, speed):
    r = 3.5 / 2
    omega = (speed/r) * (180/math.pi)
    linkbot.setJointSpeed(1, omega)
    linkbot.setJointSpeed(3, omega)

setSpeed(myLinkbot, 2.5)     # Sets the speed to 2.5 inches/sec
driveDistance(myLinkbot, 10) # Drives the Linkbot 10 inches forward
driveDistance(myLinkbot, -5) # Drives the Linkbot 5 inches backward

This example is all well and good. We define a new function setSpeed() that sets the speed of a Linkbot wheeled vehicle and we use it to set the speed to 2.5 inches per second.

What if the programmer tries to set the speed to 1,000 inches/second? Or 1,000,000 inches/second? Although it would be cool to see a Linkbot compete with a Formula One race car, there are physical limitations that prevent the Linkbot's motors from moving more than 200 degrees/second. If the speed is too high, we should set an error the user can see and possibly deal with. This is called "raising an exception". The code to raise an exception looks like this:

def setSpeed(linkbot, speed):
    r = 3.5 / 2
    omega = (speed/r) * (180/math.pi)
    if omega > 200:
        raise Exception('The speed is too high!')
    linkbot.setJointSpeed(1, omega)
    linkbot.setJointSpeed(3, omega)

When an exception is raised, the function immediately returns with the exception. These raised exceptions can be caught by try/except blocks. If the exception occurred outside of a try/except block, the entire program will quit and display the error message of the exception. In the setSpeed() function, this means that if the raise is executed, the two setJointSpeed() statements will be skipped.

When I run the new program and I try to set the speed to 1000 inches a second, I get this output:

Traceback (most recent call last):
  File "./linkbot_speed.py", line 20, in <module>
    setSpeed(myLinkbot, 1000)     # Sets the speed to 1000 inches/sec
  File "./linkbot_speed.py", line 16, in setSpeed
    raise Exception('The speed is too high!')
Exception: The speed is too high!

Now you can use try/catch blocks to deal with possible errors. Lets try writing a program that tries to set the speed to 10 inches per second again, except every time it encounters an exception, in reduces the requested speed by 1 inch/second and tries again.

import barobo                                                                    
import math # So that we can use math.pi                                         
dongle = barobo.Dongle()                                                         
dongle.connect()                                                                 
myLinkbot = dongle.getLinkbot('ABCD') # Change ABCD to your Linkbot's serial ID        
                                                                                 
def driveDistance(linkbot, distance):                                            
    r = 3.5 / 2 # If you have a wheel that's not 3.5 inches in diameter, change "3.5" to the diameter of your wheel
    degrees = (360) / (2 * math.pi * r) * distance                               
    linkbot.move(degrees, 0, -degrees)                                           
                                                                                 
def setSpeed(linkbot, speed):                                                    
    r = 3.5 / 2                                                                  
    omega = (speed/r) * (180/math.pi)                                            
    if omega > 200:                                                              
        raise Exception('The speed is too high!')                                
    linkbot.setJointSpeed(1, omega)                                              
    linkbot.setJointSpeed(3, omega)                                              
                                                                                 
requestedSpeed = 10  # 1                                                             
while True:  # 2                                                                     
    try:                                                                         
        print('Trying to set speed to: ' + str(requestedSpeed) + 'inches/sec')
        setSpeed(myLinkbot, requestedSpeed)  # 3 
        print('Success!') 
        break  # 4
    except:                                                                      
        print('Failed.')                                                       
        requestedSpeed -= 1  # 5                                                   

# 6                                                                                 
driveDistance(myLinkbot, 10) # Drives the Linkbot 10 inches forward              
driveDistance(myLinkbot, -5) # Drives the Linkbot 5 inches backward

The output is

Trying to set speed to: 10inches/sec
Failed.
Trying to set speed to: 9inches/sec
Failed.
Trying to set speed to: 8inches/sec
Failed.
Trying to set speed to: 7inches/sec
Failed.
Trying to set speed to: 6inches/sec
Success!

Lets step through this program together to make sure we fully understand what is happening.

  • # 1 : When we first get to this line, we create a new variable called requestedSpeed and set its value to "10".
  • # 2 : Enter an infinite loop
  • # 3 : Try to set the speed. requestedSpeed is currently 10, which is too high. The setSpeed() function raises an exception. Since we are in a try/except block, we immediately go to the except block since an exception was thrown. Proceed to # 5
  • # 5 : Decrease requestedSpeed by one. requestedSpeed is now 9. This is the end of our while loop, which means that Python goes back to the beginning of the loop.
  • # 3 : We end up at # 3 again, except requestedSpeed is now 9. Still too high, exception is thrown.
  • # 5 : Again, we decrease requestedSpeed to 8.
  • # 3 : Still too high...
  • # 5 : Reduce to 7...
  • # 3 : Still too high...
  • # 5 : Reduce to 6.
  • # 3 : Now it succeeds. Since it succeeded, no exception was raised. Continue to # 4
  • # 4 : This break statement pops us out of the loop. Proceed to # 6 and the rest of the program.

Exercises edit

Update at least the phone numbers program (in section Dictionaries) so it doesn't crash if a user doesn't enter any data at the menu.

Learning Python 3 with the Linkbot
 ← File IO Dealing with the imperfect Recursion → 
  1. "The 'with' statement"
  2. 'The Python "with" Statement by Example'