Программирование и научные вычисления на языке Python/§11
Вступление
правитьФайлы, как вы, конечно, знаете, используются для хранения информации. Кроме того, что они ее просто хранят, в них ее можно записывать и извлекать, сохранять уже измененной и так далее. Представим, мы записали данные измерений в файле data1.txt. Давайте поставим нашей начальной целью дать прочитать из файла значения измерений, найти для них среднее и вывести на экран.
Перед тем как прочитать файл, нам нужно, чтобы он просто был. Давайте создадим его и запишем в нем наши простые измерения, например:
21.8
18.1
19
23
26
17.8
Чтение файла строка за строкой
правитьЧтение файла мало чем отличается от чтения книги. Например, чтобы прочитать книгу, мы должны по крайней мере, открыть ее. То же и с файлом — нам нужно создать file object, который мы обозначим переменной infile:
infile = open('data1.txt', 'r')
Первый аргумент понятен — мы называем имя файла, который мы должны открыть (open). Второй аргумент, строка 'r', говорит, что мы хотим открыть файл для чтения (reading). Далее мы увидим, что он может быть открыт и для записи, тогда вторым аргументом будет 'w' (writing). Как и прочитанную книгу, файл желательно закрыть:
infile.close()
Но закрывать еще рано. Читать файл мы можем разными способами. Простейший из них — построчное чтение с помощью цикла for:
for line in infile:
# do something with line
То есть синтаксис такой же, как при чтении списка, только вместо списка — файл, а вместо элементов — строки. Вместо того, чтобы читать поочередно каждую строку, можно загрузить их все в список строк с помощью функции readlines в список lines. Ниже представлено три способа, дающих один и то же список lines:
lines = infile.readlines()
lines = []
for line in infile:
lines.append(line)
lines = [line for line in infile]
Итак, мы загрузили файл строка за строкой в список lines. Далее мы хотим посчитать среднее значение полученных нами измерений. При этом помним, что загрузили мы именно строки, то есть у нас список объектов string. А работать нам нужно с float-числами. Мы уже не раз с этим сталкивались и знаем что делать:
mean = 0
for line in lines:
number = float(line)
mean = mean + number
mean = mean/len(lines)
Суммирование чисел — часто встречающаяся операция, поэтому в Python есть и встроенная функция sum, умеющая складывать элементы списка. И здесь мы можем воспользоваться генерацией списка, решающей задачу существенно короче:
mean = sum([float(line) for line in lines])/len(lines)
Другой вариант — мы можем не создавать список строк, а сразу же записывать числа и получить полное решение нашей задачи в пять строчек:
infile = open('data1.txt', 'r')
numbers = [float(line) for line in infile.readlines()]
infile.close()
mean = sum(numbers)/len(numbers)
print mean
На этом примере видно, насколько неоднозначно программирование и как можно придумать несколько решений для одной проблемы. Опытный программист прикинет для сравнения несколько решений, и выберет наиболее быстрое, компактное и легкое для понимания решение.
Чтение файла целиком
правитьИтак, метод readline читает файл построчно: при каждом новом вызове читается следующая строка. У файлов имеется и другой метод чтения, залпом — read, читающий файл целиком, возвращая его в виде одной строки:
>>> infile = open('data1.txt', 'r')
>>> filestr = infile.read()
>>> filestr
'21.8\n18.1\n19\n23\n26\n17.8\n'
>>> print filestr
21.8
18.1
19
23
26
17.8
В этом интерактивном примере легко можно отметить разницу между filestr и print filestr. Как видно, это не список строк, а одна строка вместе с символами перехода на новую строку \n. А строки, как мы узнаем позже, обладают довольно большой функциональностью. Ясно, что чтобы съесть эту строку, нам нужен нож. В качестве него выступает метод строк split, который разрезает их на слова:
>>> words = filestr.split()
>>> words
['21.8', '18.1', '19', '23', '26', '17.8']
>>> numbers = [float(w) for w in words]
>>> mean = sum(numbers)/len(numbers)
>>> print mean
20.95
Более компактное элегантное решение:
infile = open('data1.txt', 'r')
numbers = [float(w) for w in infile.read().split()]
mean = sum(numbers)/len(numbers)
Чтение смешанного файла
правитьЕстественно, наш первый пример был очень прост и лишь вводил в суть дела. Многие данные представляются не только в форме чисел, но и слов. Как быть тут? Например, у нас есть файл, рассказывающий нам о средних атмосферных осадках, выпадающий каждый месяц в Риме, и записано это в виде файла rainfall.dat:
Average rainfall (in mm) in Rome: 1188 months between 1782 and 1970 Jan 81.2 Feb 63.2 Mar 70.3 Apr 55.7 May 53.0 Jun 36.4 Jul 17.5 Aug 27.5 Sep 60.9 Oct 117.7 Nov 111.0 Dec 97.9 Year 792.9
И вот мы ставим уже себе более интересную цель: по этому файлу построить график. Помня про метод split, мы можем построчно прочитать файл и разбить каждую строку на два элемента. Далее выбираем каждый второй элемент и составляем список чисел, соответствующий месяцам:
def extract_data(filename):
infile = open(filename, 'r')
infile.readline() # пропускаем первую строчку
numbers = []
for line in infile:
words = line.split()
number = float(words[1])
numbers.append(number)
infile.close()
return numbers
values = extract_data('rainfall.dat')
import matplotlib.pyplot as plt
month_indices = range(1, 13)
plt.plot(month_indices, values[:-1], 'o--')
plt.show()
Заметьте, что первую строку мы пропускаем, поскольку для решения самой задачи она нам не нужна и цикл for записывает список, начиная со второй строки. Но, например, если у нас имеется много схожих файлов, оформленных в одинаковом стиле, можно использовать эту первую комментирующую строку как аргумент для названия графиков, передавая ее функциям plt.savefig() или plt.title().
Также мы видим, что в dat-файле в последней строке имеется значение суммарных средних осадков, что нам не нужно, так как мы строим график для каждого месяца. Поэтому мы используем срез списка values[:-1], исключая таким образом последний элемент.
Цикл for при желании можно успешно заменить генерацией списков:
def extract_data(filename):
infile = open(filename, 'r')
infile.readline()
numbers = [float(line.split()[1]) for line in infile]
infile.close()
return numbers
Запись в файл
правитьПисать в файл тоже несложно. Основная функция для этого действия по форме не отличается от чтения — outfile.write(s), где outfile — имя файла, а s — передаваемая строка. При этом для разделения строк не стоит забывать про символ переноса, если его там не имеется:
outfile.write(s + '\n')
Простая запись может осуществляться двумя способами, которые определяются по второму аргументу, когда мы открываем файл:
# записать в новый файл или перезаписать старый
outfile = open(filename, 'w')
# добавить в конец существующего файла
outfile = open(filename, 'a')
Запись таблицы
правитьРассмотрим запись в файл на примере вот такой таблицы, оформленной в виде вложенного списка:
[[ 0.75, 0.29619813, -0.29619813, -0.75 ], [ 0.29619813, 0.11697778, -0.11697778, -0.29619813], [-0.29619813, -0.11697778, 0.11697778, 0.29619813], [-0.75, -0.29619813, 0.29619813, 0.75 ]]
Итак, вспомним вложенные списки, и увидим, что первый индекс будет отвечать за строку, а второй - за столбец. В конце каждой строки мы должны вставить символ перехода на новую. И тогда код будет выглядеть так:
data = [[ 0.75, 0.29619813, -0.29619813, -0.75 ],
[ 0.29619813, 0.11697778, -0.11697778, -0.29619813],
[-0.29619813, -0.11697778, 0.11697778, 0.29619813],
[-0.75, -0.29619813, 0.29619813, 0.75 ]]
outfile = open('tmp_table.dat', 'w')
for row in data:
for column in row:
outfile.write('%14.8f' % column)
outfile.write('\n')
outfile.close()
И теперь наши данные даже отформатированы и выстроены в ровную таблицу dat-файла:
0.75000000 0.29619813 -0.29619813 -0.75000000 0.29619813 0.11697778 -0.11697778 -0.29619813 -0.29619813 -0.11697778 0.11697778 0.29619813 -0.75000000 -0.29619813 0.29619813 0.75000000
Представим, что мы хотим, чтобы это была еще и таблица с названиями, пусть самая простая:
column 1 column 2 column 3 column 4 row 1 0.75000000 0.29619813 -0.29619813 -0.75000000 row 2 0.29619813 0.11697778 -0.11697778 -0.29619813 row 3 -0.29619813 -0.11697778 0.11697778 0.29619813 row 4 -0.75000000 -0.29619813 0.29619813 0.75000000
Для этого мы должны добавить немного форматирования к предыдущей программе:
data = [[ 0.75, 0.29619813, -0.29619813, -0.75 ],
[ 0.29619813, 0.11697778, -0.11697778, -0.29619813],
[-0.29619813, -0.11697778, 0.11697778, 0.29619813],
[-0.75, -0.29619813, 0.29619813, 0.75 ]]
outfile = open('tmp_table.dat', 'w')
ncolumns = len(data[0])
outfile.write(' ')
for i in range(1, ncolumns+1):
outfile.write('%10s ' % ('column %2d' % i))
outfile.write('\n')
row_counter = 1
for row in data:
outfile.write('row %2d' % row_counter)
for column in row:
outfile.write('%14.8f' % column)
outfile.write('\n')
row_counter += 1
outfile.close()
Итак, разберемся. Первая инструкция write делает необходимый отступ в первой строке. Вторая, вставленная в цикл по числу столбцов, выводит десятиместные строки (%10s) в одну линию. В этой строке, как и нужно, все время выводится column и меняется номер. Заметьте, что мы могли бы, конечно, использовать конструкцию вроде 'column' + str(i), но тогда длина строки будет зависеть от числа i. Поэтому лучшее уже пораньше научиться форматировать, поскольку в форматировании можно заранее выделить место.
Далее, после первой строки шапки с наименованиями столбцов, переходим на следующую. Здесь у нас уже пойдут однообразные строчки, все начинающиеся с названия строки с номеров, а внутри них после этого располагаются данные из таблицы и переходы на следующую строчку. При этом мы используем тот же цикл, что и раньше, который проходит именно по таблице, но добавляем к нему текст — названия строк.
Возможен и другой способ создания этого цикла, довольно ясный — использовать индексацию вложенных списков:
for i in range(len(data)):
outfile.write('row %2d' % (i+1))
for j in range(len(data[i])):
outfile.write('%14.8f' % data[i][j])
outfile.write('\n')
outfile.close()
Стандартные Input/Output
правитьМы уже общались с пользователем и получали от него данные с помощью функции raw_input(). Input! Информацию мы выводили на экран с помощью инструкции print. Output! Многие языки программирования обладают такими средствами. Это стандартные вход и выход и для управления ими в Python есть файловые объекты sys.stdin и sys.stdout. Это file objects, но их не требуется открывать и закрывать. Это уже не «книжка», а поток. Запись
s = raw_input('Give s:')
#эквивалентна такой записи:
print 'Give s: ',
s = sys.stdin.readline()
Запятая здесь нужна, поскольку инструкция print по умолчанию после вывода строки переходит на следующую. Также аналогичны:
# эта запись
s = eval(raw_input('Give s:'))
# и эта
print 'Give s: ',
s = eval(sys.stdin.readline())
Так же не отличишь и output:
# коротко
print s
# чуть длиннее
sys.stdout.write(s)
Чем же все эти сравнения могут оказаться полезны и как этим пользоваться покажет следующий пример. Представим, у вас есть функция, читающая файл и пишущая из него данные (например, попутно оформляя, пересчитывая и форматируя) в другой файловый объект. Пусть у нее такая форма:
def x2f(infile, outfile, f):
for line in infile:
x = float(line)
y = f(x)
outfile.write('%g\n' % y)
И эта функция работает с любыми типами файлов. С sys.stdin в качестве infile и/или sys.stdout вместо outfile мы можем использовать эту функцию повсеместно, не нуждаясь в каком-то дополнительном коде вроде print или raw_input() и работать одинаково с разными типами файлов.
Кроме стандартных Input/Output, имеются и Standard Errors. Обозначаются они как файловый объект sys.stderr и совмещают в себе вывод и возбуждение исключения:
if x < 0:
sys.stderr.write('Illegal value of x'); sys.exit(1)
Чтение и запись табличных файлов
правитьНаверняка, вы встречались с такими табличными процессорами как Microsoft Excel или OpenOffice. Может быть, кто-то из вас и большой их любитель. Такие программы представляют данные в виде таблицы чисел и текста и широко используются для расчетов и построения графиков. Поэтому было бы неплохо иметь инструмент для работы с файлами такого типа, тем более что сам Python и дополнительные библиотеки позволяют производить практически любые известные типы вычислений, что обычно не так ожидаемо от табличных процессоров.
Данные в виде таблиц также называют CSV (comma separated values) — буквально «значения, разделенные запятыми». Это текстовый формат, предназначенный для представления табличных данных. Каждая строка файла — это одна строка таблицы. Значения отдельных колонок разделяются разделительным символом — запятой (,) , точка с запятой (;) или другим символом. CSV могут быть легко прочитаны и обработаны Python.
Файл может быть записан в CSV формате весьма просто и ясно:
,"year 1","year 2","year 3" "person 1",651000,651000,651000 "person 2",1100500,950100,340000 "person 3",740000,780000,800000
Чтение CSV-файлов
правитьНаша цель записать Python код, загружающий данные из CSV-таблицы. Технически таблица это вложенный список. Мы можем прочитать CSV-файл строка за строкой, используя стандартный модуль csv. Рецепт прост:
infile = open('budget.csv', 'r')
import csv
table = []
for row in csv.reader(infile):
table.append(row)
infile.close()
Переменная row — список значений, прочитанный с помощью csv-модуля. Как у нас часто бывало со списками, три строчки несложно превратить в одну. При этом естественным образом получается нужный вложенный список:
table = [row for row in csv.reader(infile)]
Таблицу в наиболее удобоваримом виде легко распечатать с помощью модуля pretty print:
import pprint
pprint.pprint(table)
и увидеть во что она превратилась в Python:
[['', 'year 1', 'year 2', 'year 3'],
['person 1', '651000', '651000', '651000'],
['person 2', '1100500', '950100', '340000'],
['person 3', '740000', '780000', '800000']]
Заметим важную особенность csv модуля — он возвращает все в виде строк. Для трансформации того текста, что представлен числами мы предпринимаем следующий цикл:
for r in range(1,len(table)):
for c in range(1, len(table[0])):
table[r][c] = float(table[r][c])
Теперь инструкция pprint.pprint(table) выведет требуемое:
[['', 'year 1', 'year 2', 'year 3'],
['person 1', 651000.0, 651000.0, 651000.0],
['person 2', 1100500.0, 950100.0, 340000.0],
['person 3', 740000.0, 780000.0, 800000.0]]
Обработка данных
правитьТеперь получив данные, с которыми может работать Python, произведем с ними какие-то действия для последующей записи в файл. Например, добавим строку, рассчитывающую сумму за каждый год, то есть суммирующую числа в столбцах:
row = [0.0]*len(table[0])
row[0] = 'sum'
for c in range(1, len(row)):
s = 0
for r in range(1, len(table)):
s += table[r][c]
row[c] = s
Теперь наша таблица стала такой:
[['', 'year 1', 'year 2', 'year 3'],
['person 1', 651000.0, 651000.0, 651000.0],
['person 2', 1100500.0, 950100.0, 340000.0],
['person 3', 740000.0, 780000.0, 800000.0]
['sum', 2491500.0, 2381100.0, 1791000.0]
Запись в CSV-файл
правитьНаша последняя задача — загрузить обработанные данные обратно в файл. С этим успешно справится код, также использующий модуль csv, но уже для записи:
outfile = open('budget.csv', 'w')
writer = csv.writer(outfile)
for row in table:
writer.writerow(row)
outfile.close()
Наша программа целиком будет выглядеть следующим образом:
infile = open('budget.csv', 'r')
import csv
table = []
for row in csv.reader(infile):
table.append(row)
infile.close()
import pprint
pprint.pprint(table)
# трансформируем числа из string в float
# (оставляя в покое первую строку и первый столбец)
for r in range(1,len(table)):
for c in range(1, len(table[0])):
table[r][c] = float(table[r][c])
pprint.pprint(table)
# добавляем строку с суммой
row = [0.0]*len(table[0])
row[0] = 'sum'
for c in range(1, len(row)):
s = 0
for r in range(1, len(table)):
s += table[r][c]
row[c] = s
table.append(row)
pprint.pprint(table)
outfile = open('budget2.csv', 'w')
writer = csv.writer(outfile)
for row in table:
writer.writerow(row)
outfile.close()