Learning Python 3 with the Linkbot/File IO

From LinkbotLabs
Jump to: navigation, search

File I/O

Here is a simple example of file I/O (input/output):

# Write a file
with open("test.txt", "wt") as out_file:
    out_file.write("This Text is going to out file\nLook at it and see!")
 
# Read a file
with open("test.txt", "rt") as in_file:
    text = in_file.read()
 
print(text)

The output and the contents of the file test.txt are:

This Text is going to out file
Look at it and see!

Notice that it wrote a file called test.txt in the directory that you ran the program from. The \n in the string tells Python to put a newline where it is.

A overview of file I/O is:

  • Get a file object with the open function
  • Read or write to the file object (depending on how it was opened)
  • If you did not use with to open the file, you'd have to close it manually

The first step is to get a file object. The way to do this is to use the open function. The format is file_object = open(filename, mode) where file_object is the variable to put the file object, filename is a string with the filename, and mode is "rt" to read a file as text or "wt" to write a file as text (and a few others we will skip here). Next the file objects functions can be called. The two most common functions are read and write. The write function adds a string to the end of the file. The read function reads the next thing in the file and returns it as a string. If no argument is given it will return the whole file (as done in the example).

Now here is a new version of the phone numbers program that we made earlier:

def print_numbers(numbers):
    print("Telephone Numbers:")
    for k, v in numbers.items():
        print("Name:", k, "\tNumber:", v)
    print()
 
def add_number(numbers, name, number):
    numbers[name] = number
 
def lookup_number(numbers, name):
    if name in numbers:
        return "The number is " + numbers[name]
    else:
        return name + " was not found"
 
def remove_number(numbers, name):
    if name in numbers:
        del numbers[name]
    else:
        print(name," was not found")
 
def load_numbers(numbers, filename):
    in_file = open(filename, "rt")
    while True:
        in_line = in_file.readline()
        if not in_line:
            break
        in_line = in_line[:-1]
        name, number = in_line.split(",")
        numbers[name] = number
    in_file.close()
 
def save_numbers(numbers, filename):
    out_file = open(filename, "wt")
    for k, v in numbers.items():
        out_file.write(k + "," + v + "\n")
    out_file.close()
 
def print_menu():
    print('1. Print Phone Numbers')
    print('2. Add a Phone Number')
    print('3. Remove a Phone Number')
    print('4. Lookup a Phone Number')
    print('5. Load numbers')
    print('6. Save numbers')
    print('7. Quit')
    print()
 
phone_list = {}
menu_choice = 0
print_menu()
while True:
    menu_choice = int(input("Type in a number (1-7): "))
    if menu_choice == 1:
        print_numbers(phone_list)
    elif menu_choice == 2:
        print("Add Name and Number")
        name = input("Name: ")
        phone = input("Number: ")
        add_number(phone_list, name, phone)
    elif menu_choice == 3:
        print("Remove Name and Number")
        name = input("Name: ")
        remove_number(phone_list, name)
    elif menu_choice == 4:
        print("Lookup Number")
        name = input("Name: ")
        print(lookup_number(phone_list, name))
    elif menu_choice == 5:
        filename = input("Filename to load: ")
        load_numbers(phone_list, filename)
    elif menu_choice == 6:
        filename = input("Filename to save: ")
        save_numbers(phone_list, filename)
    elif menu_choice == 7:
        break
    else:
        print_menu()
 
print("Goodbye")

Notice that it now includes saving and loading files. Here is some output of my running it twice:

1. Print Phone Numbers
2. Add a Phone Number
3. Remove a Phone Number
4. Lookup a Phone Number
5. Load numbers
6. Save numbers
7. Quit

Type in a number (1-7): 2
Add Name and Number
Name: Jill
Number: 1234
Type in a number (1-7): 2
Add Name and Number
Name: Fred
Number: 4321
Type in a number (1-7): 1
Telephone Numbers:
Name: Jill     Number: 1234
Name: Fred     Number: 4321

Type in a number (1-7): 6
Filename to save: numbers.txt
Type in a number (1-7): 7
Goodbye
1. Print Phone Numbers
2. Add a Phone Number
3. Remove a Phone Number
4. Lookup a Phone Number
5. Load numbers
6. Save numbers
7. Quit

Type in a number (1-7): 5
Filename to load: numbers.txt
Type in a number (1-7): 1
Telephone Numbers:
Name: Jill     Number: 1234
Name: Fred     Number: 4321

Type in a number (1-7): 7
Goodbye

The new portions of this program are:

def load_numbers(numbers, filename):
    in_file = open(filename, "rt")
    while True:
        in_line = in_file.readline()
        if not in_line:
            break
        in_line = in_line[:-1]
        name, number = in_line.split(",")
        numbers[name] = number
    in_file.close()
 
def save_numbers(numbers, filename):
    out_file = open(filename, "wt")
    for k, v in numbers.values():
        out_file.write(k + "," + v + "\n")
    out_file.close()

First we will look at the save portion of the program. First it creates a file object with the command open(filename, "wt"). Next it goes through and creates a line for each of the phone numbers with the command out_file.write(x + "," + numbers[x] + "\n"). This writes out a line that contains the name, a comma, the number and follows it by a newline.

The loading portion is a little more complicated. It starts by getting a file object. Then it uses a while True: loop to keep looping until a break statement is encountered. Next it gets a line with the line in_line = in_file.readline(). The readline function will return a empty string when the end of the file is reached. The if statement checks for this and breaks out of the while loop when that happens. Of course if the readline function did not return the newline at the end of the line there would be no way to tell if an empty string was an empty line or the end of the file so the newline is left in what readline returns. Hence we have to get rid of the newline. The line in_line = in_line[:-1] does this for us by dropping the last character. Next the line name, number = in_line.split(",") splits the line at the comma into a name and a number. This is then added to the numbers dictionary.

Advanced use of .txt files

You might be saying to yourself, "Well I know how to read and write to a textfile, but what if I want to print the file without opening out another program?"

There are a few different ways to accomplish this. The easiest way does open another program, but everything is taken care of in the Python code, and doesn't require the user to specify a file to be printed. This method involves invoking the subprocess of another program.

Remember the file we wrote output to in the above program? Let's use that file. Keep in mind, in order to prevent some errors, this program uses concepts from the Next chapter. Please feel free to revisit this example after the next chapter.

import subprocess
def main():
    try:
        print("This small program invokes the print function in the Notepad application")
        #Lets print the file we created in the program above
        subprocess.call(['notepad','/p','numbers.txt'])
    except WindowsError:
        print("The called subprocess does not exist, or cannot be called.")
 
main()

The subprocess.call takes three arguments. The first argument in the context of this example, should be the name of the program which you would like to invoke the printing subprocess from. The second argument should be the specific subprocess within that program. For simplicity, just understand that in this program, '/p' is the subprocess used to access your printer through the specified application. The last argument should be the name of the file you want to send to the printing subprocess. In this case, it is the same file used earlier in this chapter.

Storing and loading music saved in a text file

This section will demonstrate how we can store simple melodies in a file. We can then write a program that reads the file and plays the melody on a Linkbot.

A little music theory

To fully understand the sample program, we will need a little knowledge about music theory. If you don't care about music theory and you just want to get the Linkbot to play music, you can skip to the next section.

What is music? Music is a series of notes played in a certain order. Sometimes, more than one note is played together. Each note is played for a certain duration. Notes themselves are vibrations in the air that we can hear. Each note has a certain frequency of vibration; when the frequency changes, the perceived pitch of the note changes too.

Each note has a name. If you know it's name, you can find it on a piano keyboard, and vice versa. You may have heard the term "Middle-C", or the phrase "Do-Re-Mi". These are both ways to refer to notes. If you are familiar with a piano keyboard, you will know that the "C" note appears more than once on the keyboard. When you play the C notes, you'll notice that they don't sound the same, but they are all called "C". It turns out that there are 12 tones that repeat over and over again. Starting at C, they are:

  • C
  • C# (Db)
  • D
  • D# (Eb)
  • E
  • F
  • F# (Gb)
  • G
  • G# (Ab)
  • A
  • A# (Bb)
  • B

The "#" symbol is pronounced "sharp", so C# is pronounced "See-Sharp". The "sharp" indicates that the tone is one step higher than the normal note. For instance, "A#" is one step higher than "A". You might also be familiar with another symbol that looks like a lowercase b. That symbol is pronounced "flat", so "Bb" is pronounced "B-flat". It has the opposite meaning of sharp, indicating that the note is one step lower than the un-flatted note. This means that the note "G#" is actually the same note as "Ab". For now, we will only use sharps to avoid confusion. On a piano keyboard, all of the sharp/flat notes are the black keys and the rest of the notes are the white keys.

On a piano keyboard, these notes repeat over and over again, so we must devise a way to specifically refer which A or B or C we are referring to. To do this, we introduce the concept of an octave number. The lowest note a piano can play is called "A0", where 0 is the octave number. Going from left to right on a piano keyboard, the octave number increases every time you encounter the C note. Thus, the first several white keys on a piano keyboard from left to right are:

  • A0
  • B0
  • C1
  • D1
  • E1
  • F1
  • G1
  • A1
  • B1
  • C2
  • etc...

Now, we have a way to specify exactly what key on a keyboard we are referring to using a string such as "F#5".

We also have an equation where we can calculate the frequency of a note depending on how many steps away it is from our "A0" note. The equation is:

For instance, G0 is -2 steps away from A0, and B0 is 2 steps away from A0. For notes in the same octave, we can simply count the number of black and white keys to find the distance away from A, but how about different octaves? For instance, how many keys away is A0 from B4?

First, lets consider how many notes away A4 is from A0. When we count the keys, each octave is 12 notes. Since A4 is 4 octave away from 0 (4-0 = 4) , A4 is 4*12=48 keys away from A0.

How about A0 and B4? A0 is 2 keys away from B0, and B0 is 4*12 keys away from B4, so in total, A0 is 2+4*12 = 50 keys away from B4. Now, we can write an equation to figure out exactly how many keys away any note is from A0. Lets use the variable for the note name we want, and for the octave of the note. Then,

Reading and playing a melody from a file

First, let us write a function that takes a string describing a note such as "A4" and gives us a frequency for that note. Here is our strategy:

  • We receive a string describing the note. The first character is the letter name of the note. We calculate the offset of that letter name from the "A" note. For instance, "B" would have an offset of +2 and "E" would have an offset of -5. We find these offsets simply by counting the keys on a keyboard.
  • The next character in the string might be a "#" sharp or a "b" flat. If it is sharp, increase the offset by one. If it is flat, decrease it by one. If it is neither, don't do anything.
  • The final character is the octave number. We multiply this number by 12 and add it to our offset.
  • The result is the number of keys away the note is away from A0. We can now use the equation to find the frequency based on the key offset from A0.

Next, lets talk about our file format. Since the Linkbot can only play one note at a time, the file should specify single notes for the Linkbot to play. Also, the file should specify how many seconds to play each note. We choose to have our file such that each line contains a note name such as "C4" or "Bb3", along with a note duration in seconds.

Here is a file that we have created for you:

fur_elise.txt

E5  0.125                                                                        
D#5 0.125                                                                        
E5  0.125                                                                        
D#5 0.125                                                                        
E5  0.125                                                                        
B4  0.125                                                                        
D5  0.125                                                                        
C5  0.125                                                                        
A4  0.375                                                                        
C4  0.125                                                                        
E4  0.125                                                                        
A4  0.125                                                                        
B4  0.375                                                                        
G#3 0.125                                                                        
G#4 0.125                                                                        
B4  0.125                                                                        
C5  0.375                                                                        
E4  0.125                                                                        
E5  0.125                                                                        
D#5 0.125                                                                        
E5  0.125                                                                        
D#5 0.125                                                                        
E5  0.125                                                                        
B4  0.125                                                                        
D5  0.125                                                                        
C5  0.125                                                                        
A4  0.375                                                                        
C4  0.125                                                                        
E4  0.125                                                                        
A4  0.125                                                                        
B4  0.375                                                                        
E4  0.125                                                                        
C5  0.125                                                                        
B4  0.125                                                                        
A4  0.375

Go ahead and copy-paste the text into a file called "fur_elise.txt". Lets write our function and program that will read this file and play a tune.

# File: 01_play_music_file.py
 
import time                                                                      
import linkbot                                                                   
 
myLinkbot = linkbot.Linkbot('ABCD') # Replace 'ABCD' with your Linkbot's ID                                                 
 
def noteToFreq(note):                                                            
    # First, we need to figure out where the note is relative to 'A'.            
    # For instance, 'B' is 2 half-steps above A, 'C' is 9 half-steps             
    # below 'A'. Lets create a dictionary called "note-offset" and store         
    # all of our offsets from A in the dictionary.                               
    noteOffsets = {                                                              
        'C' : -9,                                                                
        'D' : -7,                                                                
        'E' : -5,                                                                
        'F' : -4,                                                                
        'G' : -2,                                                                
        'A' : 0,                                                                 
        'B' : 2 }                                                                
    # Find our offset                                                            
    offset = noteOffsets[ note[0].upper() ]  # 1                                      
    # See if there is a sharp or flat                                            
    if note[1] == '#':                                                           
        offset += 1                                                              
    elif note[1] == 'b':                                                         
        offset -= 1                                                              
    # Calculate the offset based on the octave                                   
    octave = int(note[-1])  # 2                                                       
    offset += (octave)*12                                                        
 
    # Calculate the note frequency                                                                                              
    freq = 2**((offset)/12)*27.5                                                 
    return freq                                                                                                  
 
musicFile = open('fur_elise.txt', 'r')                                                                           
for line in musicFile:  # 3                                                                                           
    data = line.split()  # 4                                                                                         
    myLinkbot.setBuzzerFrequency(noteToFreq(data[0]))                                                            
    time.sleep( float(data[1]) )                                                                                 
    myLinkbot.setBuzzerFrequency(0)
  1. note[0].upper() takes the first character and uppercases it. We want to uppercase anything that comes in just in case the user used a lowercase note name, like "e4". Since we wrote our dictionary with uppercase note names, we need to make sure the incoming note names are upper-case too so that it can be matched with the ones in our dictionary.
  2. The "-1" index means the last item of a list. Since all of the items in our list are strings, we need to convert it to an integer with int.
  3. This loop goes through every single line in the music file, line by line. Each time, it stores the text of the line in the line variable.
  4. The split() function splits lines of text based on whitespace. For instance, "Hello there".split() becomes the list ["Hello", "there"] . Since each line in our text file has 2 "words", the first part (the note name) is stored into our variable data[0], and the second part (the note duration) is stored into data[1].

Exercises

Now modify the grades program from section Dictionaries so that is uses file I/O to keep a record of the students.

Solution

Now modify the grades program from section Dictionaries so that is uses file I/O to keep a record of the students.

assignments = ['hw ch 1', 'hw ch 2', 'quiz   ', 'hw ch 3', 'test']
students = { }
 
def load_grades(gradesfile):
    inputfile = open(gradesfile, "r")
    grades = [ ]
    while True:
        student_and_grade = inputfile.readline()
        student_and_grade = student_and_grade[:-1]
        if not student_and_grade:
            break
        else:
            studentname, studentgrades = student_and_grade.split(",")
            studentgrades = studentgrades.split(" ")
            students[studentname] = studentgrades
    inputfile.close()
    print("Grades loaded.")
 
def save_grades(gradesfile):
    outputfile = open(gradesfile, "w")
    for k, v in students.values():
        outputfile.write(k + ",")
        for x in v:
            outputfile.write(x + " ")
        outputfile.write("\n")
    outputfile.close()
    print("Grades saved.")
 
def print_menu():
    print("1. Add student")
    print("2. Remove student")
    print("3. Load grades")
    print("4. Record grade")
    print("5. Print grades")
    print("6. Save grades")
    print("7. Print Menu")
    print("9. Quit")
 
def print_all_grades():
    if students:
        keys = sorted(students.keys())
        print('\t', end=' ')
        for x in assignments:
            print(x, '\t', end=' ')
        print()
        for x in keys:
            print(x, '\t', end=' ')
            grades = students[x]
            print_grades(grades)
    else:
        print("There are no grades to print.")
 
def print_grades(grades):
    for x in grades:
        print(x, '\t', end=' ')
    print()
 
print_menu()
menu_choice = 0
while menu_choice != 9:
    print()
    menu_choice = int(input("Menu Choice: "))
    if menu_choice == 1:
        name = input("Student to add: ")
        students[name] = [0] * len(assignments)
    elif menu_choice == 2:
        name = input("Student to remove: ")
        if name in students:
            del students[name]
        else:
            print("Student:", name, "not found")
    elif menu_choice == 3:
        gradesfile = input("Load grades from which file? ")
        load_grades(gradesfile)
    elif menu_choice == 4:
        print("Record Grade")
        name = input("Student: ")
        if name in students:
            grades = students[name]
            print("Type in the number of the grade to record")
            print("Type a 0 (zero) to exit")
            for i,x in enumerate(assignments):
                print(i + 1, x, '\t', end=' ')
            print()
            print_grades(grades)
            which = 1234
            while which != -1:
                which = int(input("Change which Grade: "))
                which -= 1
                if 0 <= which < len(grades):
                    grade = input("Grade: ") # Change from float(input()) to input() to avoid an error when saving
                    grades[which] = grade
                elif which != -1:
                    print("Invalid Grade Number")
        else:
            print("Student not found")
    elif menu_choice == 5:
        print_all_grades()
    elif menu_choice == 6:
        gradesfile = input("Save grades to which file? ")
        save_grades(gradesfile)
    elif menu_choice != 9:
        print_menu()


Write a program that reads a text file containing motor angles, such as

90 45 20
180 22 -180
5 32 0

Each triplet of numbers represents three motor angles. Write a program that moves the Linkbot's joints to those positions with the moveTo() function for each line in the data file.

Solution
# File: 02_moveto.py
 
import linkbot
 
robotID = input("Please enter a Linkbot's ID: ")
robot = linkbot.Linkbot(robotID)
 
robot.setJointSpeeds(90, 90, 90)
with open('movedata.txt', 'rt') as datafile:
    for line in datafile:
        data = line.split()
        if len(data) is not 3:
            continue
        robot.moveTo(float(data[0]), float(data[1]), float(data[2]))


Learning Python 3 with the Linkbot
 ← Revenge of the Strings File IO Dealing with the imperfect →