Сейчас рассмотрим пару примеров, в которых используется многое из того, о чем рассказывалось в этой и в предыдущей главах.
Первая программа имеет отношение к обработке текстовой
информации. Вторая программа содержит примерно девяносто строк и
предназначена для выполнения математических вычислений. Обе программы
используют словари, списки, именованные кортежи и множества и обе
широко используют метод str.format()
, который был описан в предыдущей
главе.
Представьте, что мы выполняем настройку новой компьютерной системы и нам необходимо сгенерировать имена пользователей для всех служащих нашей компании. У нас имеется простой текстовый файл (в кодировке UTF-8), где каждая строка представляет собой запись из полей, разделенных двоеточиями. Каждая запись соответствует одному сотруднику компании и содержит следующие поля: уникальный идентификатор служащего, имя, отчество (это поле может быть пустым), фамилию и название отдела.
Ниже в качестве примера приводятся несколько строк из файла users.txt :
1080:Priscillia:Forbes:Shepard:Cleaning Services
4382:Devan::Fielder:Public Relations
6285:Grey::Collyer:Public Relations
6201:Kierah::Battaile:Catering
Программа должна читать данные из файла, который указан в командной строке, извлекать отдельные поля из каждой строки (записи) и возвращать подходящие имена пользователей. Каждое имя пользователя должно быть уникальным и должно создаваться на основе имени сотрудника. Результаты должны выводиться на консоль в текстовом виде, отсортированными в алфавитном порядке по фамилии и имени, например:
Terminal> ./generate_usernames.py users.txt
Name ID Username
-------------------------------- ------ ---------
Battaile, Kierah................ (6201) kbattail
Blakey, Kelci-Louise............ (0641) kblakey
Blenkinsop, Keirien............. (4541) kblenkin
Clifford, Rebbekkah............. (6263) rcliffor
Каждая запись имеет точно пять полей, и хотя можно обращаться к ним по числовым индексам, тем не менее мы будем использовать осмысленные имена, чтобы сделать программный код более понятным:
ID, FORENAME, MIDDLENAME, SURNAME, DEPARTMENT = range(5)
В языке Python общепринято использовать только символы верхнего регистра для идентификаторов, которые будут играть роль констант.
Нам также необходимо создать тип именованного кортежа, где будут храниться данные о текущем пользователе:
User = collections.namedtuple("User",
"username forename middlename surname id")
Основная логика программы сосредоточена в функции main()
:
def main():
if len(sys.argv) == 1 or sys.argv[1] in {"-h", "--help"}:
print("usage: {0} file1 [file2 [... fileN]]".format(
sys.argv[0]))
sys.exit()
usernames = set()
users = {}
for filename in sys.argv[1:]:
with open(filename, encoding="utf8") as file:
for line in file:
line = line.rstrip()
if line:
user = process_line(line, usernames)
users[(user.surname.lower(), user.forename.lower(),
user.id)] = user
print_users(users)
Если пользователь не ввел в командной строке имя какого-нибудь
файла или ввел параметр -h
или --help
, то программа просто выводит
текст сообщения с инструкцией о порядке использования и завершает
работу.
Из каждой прочитанной строки удаляются любые завершающие пробельные
символы (такие как \n
), и обработка строки продолжается, только если
она не пустая. Это означает, что если в данных содержатся пустые
строки, они будут просто проигнорированы.
Все сгенерированные имена пользователей сохраняются в множестве
usernames
, чтобы гарантировать отсутствие повторяющихся имен
пользователей. Сами данные сохраняются в словаре users
. Информация о
каждом пользователе сохраняется в виде элемента словаря, ключом
которого является кортеж, содержащий фамилию сотрудника, его имя и
идентификатор, а значением – именованный кортеж типа
User
. Использование кортежа, содержащего фамилию сотрудника, его имя
и идентификатор, в качестве ключа обеспечивает возможность вызывать
функцию sorted()
для словаря и получать итерируемый объект, в
котором элементы будут упорядочены в требуемом нам порядке (то есть
фамилия, имя, идентификатор), избежав необходимости создавать функцию,
которую пришлось бы передавать в качестве аргумента key
.
def process_line(line, usernames):
fields = line.split(":")
username = generate_username(fields, usernames)
user = User(username, fields[FORENAME], fields[MIDDLENAME],
fields[SURNAME], fields[ID])
return user
Поскольку все записи имеют очень простой формат, и мы уже удалили
из строки завершающие пробельные символы, извлечь отдельные поля можно
простой разбивкой строки по двоеточиям. Мы передаем список полей и
множество usernames
в функцию generate_username()
и затем
создаем экземпляр именованного кортежа User, который возвращается
вызывающей программе (функции main()
), которая в свою очередь
вставляет информацию о пользователе в словарь users
, готовый для
вывода на экран.
Если бы мы не создали соответствующие константы для хранения индексов, мы могли бы использовать числовые индексы, как показано ниже:
user = User(username, fields[1], fields[2], fields[3], fields[0])
Хотя такой программный код занимает меньше места, тем не менее это не самое лучшее решение. Во-первых, человеку, который будет сопровождать такой программный код, непонятно, какое поле какую информацию содержит, а, во-вторых, такой программный код чувствителен к изменениям в формате файла с данными – если изменится порядок или число полей в записи, этот программный код окажется неработоспособен. При использовании констант в случае изменения структуры записи нам достаточно будет изменить только значения констант, и программа сохранит свою работоспособность.
def generate_username(fields, usernames):
username = ((fields[FORENAME][0] + fields[MIDDLENAME][:1] +
fields[SURNAME]).replace("-", "").replace("'", ""))
username = original_name = username[:8].lower()
count = 1
while username in usernames:
username = "{0}{1}".format(original_name, count)
count += 1
usernames.add(username)
return username
При первой попытке имя пользователя создается путем конкатенации
первого символа имени, первого символа отчества и фамилии целиком,
после чего из полученной строки удаляются дефисы и апострофы.
Выражение, извлекающее первый символ отчества, таит в себе одну
хитрость. Если просто использовать обращение fields[MIDDLENAME][0]
,
то в случае отсутствия отчества будет возбуждено исключение
IndexError
. Но при использовании операции извлечения среза мы
получаем либо первый символ отчества, либо пустую строку.
Затем мы переводим все символы полученного имени пользователя
в нижний регистр и ограничиваем его длину восемью символами. Если
имя пользователя уже занято (то есть оно уже присутствует в множестве
usernames
), предпринимается попытка добавить в конец имени
пользователя символ "1"
, если это имя пользователя тоже занято, тогда
предпринимается попытка добавить символ "2"
и т. д., пока не будет
получено незанятое имя пользователя. После этого имя пользователя
добавляется в множество usernames
и возвращается вызывающей
программе.
def print_users(users):
namewidth = 32
usernamewidth = 9
print("{0:<{nw}} {1:^6} {2:{uw}}".format(
"Name", "ID", "Username", nw=namewidth, uw=usernamewidth))
print("{0:-<{nw}} {0:-<6} {0:-<{uw}}".format(
"", nw=namewidth, uw=usernamewidth))
for key in sorted(users):
user = users[key]
initial = ""
if user.middlename:
initial = " " + user.middlename[0]
name = "{0.surname}, {0.forename}{1}".format(user, initial)
print("{0:.<{nw}} ({1.id:4}) {1.username:{uw}}".format(
name, user, nw=namewidth, uw=usernamewidth))
После обработки всех записей вызывается функция print_users()
,
которой в качестве параметра передается словарь users
.
Первая инструкция print()
выводит заголовки столбцов. Вторая
инструкция print()
выводит дефисы под каждым из заголовков. В этой
второй инструкции метод str.format()
используется довольно
оригинальным образом. Для вывода ему определяется строка ""
, то есть
пустая строка; в результате при выводе пустой строки мы получаем
строку из дефисов заданной ширины поля вывода.
Затем мы используем цикл for ... in
для вывода информации о каждом
пользователе, извлекая ключи из отсортированного словаря. Для удобства
мы создаем переменную user, чтобы не вводить каждый раз users[key]
в
оставшейся части функции. В цикле сначала вызывается метод
str.format()
, чтобы записать в переменную name фамилию сотрудника, имя
и необязательный первый символ отчества. Обращение к элементам в
именованном кортеже user производится по их именам. Собрав строку с
именем пользователя, мы выводим информацию о пользователе, ограничивая
ширину каждого столбца (имя сотрудника, идентификатор и имя
пользователя) желаемыми значениями.
Полный программный код программы находится в файле generate_usernames.py
Предположим, что у нас имеется пакет файлов с данными, содержащих числовые результаты некоторой обработки, выполненной нами, и нам необходимо вычислить некоторые основные статистические характеристики, которые дадут нам возможность составить общую картину о полученных данных. В каждом файле находится обычный текст (в кодировке ASCII), с одним или более числами в каждой строке (разделенными пробельными символами).
Ниже приводится пример информации, которую нам необходимо получить:
Terminal> python statistics.py statistics.dat
count = 129
mean = 170.49
median = 51.00
mode = 50.00
std. dev. = 269.50
Здесь видно, что прочитано \( 129 \) чисел, из которых наиболее часто встречается число \( 50 \) со стандартным отклонением по выборке \( 269.50 \).
Сами статистические ххарактеристики хранятся в именованном кортеже
Statistics
:
Statistics = collections.namedtuple("Statistics",
"mean mode median std_dev")
Функция main()
может служить схематическим отображением структуры
программы:
def main():
if len(sys.argv) == 1 or sys.argv[1] in {"-h", "--help"}:
print("usage: {0} file1 [file2 [... fileN]]".format(
sys.argv[0]))
sys.exit()
numbers = []
frequencies = collections.defaultdict(int)
for filename in sys.argv[1:]:
read_data(filename, numbers, frequencies)
if numbers:
statistics = calculate_statistics(numbers, frequencies)
print_results(len(numbers), statistics)
else:
print("no numbers found")
Все числа из всех файлов сохраняются в списке numbers
. Для
нахождения модальных («наиболее часто встречающихся») значений нам
необходимо знать, сколько раз встречается каждое число, поэтому мы
создаем словарь со значениями по умолчанию, используя функцию int()
в качестве фабричной функции, где будут накапливаться счетчики.
Затем выполняется обход списка файлов и производится чтение данных из
них. В качестве дополнительных аргументов мы передаем функции
read_data()
список и словарь со значениями по умолчанию, чтобы она
могла обновлять их. После чтения всех данных мы исходим из
предположения, что некоторые числа были благополучно прочитаны, и
вызываем функцию calculate_statistics()
. Она возвращает именованный
кортеж типа Statistics
, который затем используется для вывода
результатов.
def read_data(filename, numbers, frequencies):
with open(filename, encoding="ascii") as file:
for lino, line in enumerate(file, start=1):
for x in line.split():
try:
number = float(x)
numbers.append(number)
frequencies[number] += 1
except ValueError as err:
print("{filename}:{lino}: skipping {x}: {err}".format(
**locals()))
Каждая строка разбивается по пробельным символам, после чего
производится попытка преобразовать каждый элемент в число типа
float
. Если преобразование удалось, следовательно, это либо целое
число, либо число с плавающей точкой в десятичной или в
экспоненциальной форме. Полученное число добавляется в список numbers
и выполняется обновление словаря frequencies
со значениями по
умолчанию. (Если бы здесь использовался обычный словарь dict
,
программный код, выполняющий обновление словаря, мог бы выглядеть так:
frequencies[number] = frequencies.get(number, 0) + 1
.) Если
преобразование потерпело неудачу, выводится номер строки (счет строк в
текстовых файлах по традиции начинается с 1), текст, который программа
пыталась преобразовать в число, и сообщение об ошибке, соответствующее
исключению ValueError
.
def calculate_statistics(numbers, frequencies):
mean = sum(numbers) / len(numbers)
mode = calculate_mode(frequencies, 3)
median = calculate_median(numbers)
std_dev = calculate_std_dev(numbers, mean)
return Statistics(mean, mode, median, std_dev)
Эта функция используется для сбора всех статистических характеристик
воедино. Поскольку среднее значение вычисляется очень просто, мы
делаем это прямо здесь. Вычислением других статистических
характеристик занимаются отдельные функции, и в заключение данная
функция возвращает экземпляр именованного кортежа Statistics
,
содержащий четыре вычисленные статистические характеристики.
def calculate_mode(frequencies, maximum_modes):
highest_frequency = max(frequencies.values())
mode = [number for number, frequency in frequencies.items()
if frequency == highest_frequency]
if not (1 <= len(mode) <= maximum_modes):
mode = None
else:
mode.sort()
return mode
В выборке может существовать сразу несколько значений, встречающихся
наиболее часто, поэтому, помимо словаря frequencies
, функции
передается максимально допустимое число модальных значений. (Эта
функция вызывается из calculate_statistics()
, и при вызове задается
максимальное число модальных значений, равное трем.)
Функция max()
используется для поиска наибольшего значения в словаре
frequencies
. Затем с помощью генератора списков создается список из
значений, которые равны наивысшему значению. Поскольку числа могут
быть с плавающей точкой, мы сравниваем абсолютное значение разницы
(используя функцию math.fabs()
, поскольку она лучше подходит для
случаев сравнения малых величин, близких к порогу точности
представления числовых значений в компьютере, чем abs()
) с
наименьшим значением, которое может быть представлено компьютером.
Если число модальных значений равно 0 или больше максимального,
то в качестве модального значения возвращается None
; в противном
случае возвращается сортированный список модальных значений.
def calculate_median(numbers):
numbers = sorted(numbers)
middle = len(numbers) // 2
median = numbers[middle]
if len(numbers) % 2 == 0:
median = (median + numbers[middle - 1]) / 2
return median
Медиана («среднее значение») – это значение, находящееся в середине упорядоченной выборки чисел, за исключением случая, когда в выборке присутствует четное число чисел, – тогда значение медианы определяется как среднее арифметическое значение двух чисел, находящихся в середине.
Функция вычисления медианы сначала выполняет сортировку чисел по возрастанию. Затем посредством целочисленного деления определяется позиция середины выборки, откуда извлекается число и сохраняется как значение медианы. Если выборка содержит четное число значений, то значение медианы определяется как среднее арифметическое двух чисел, находящихся в середине.
def calculate_std_dev(numbers, mean):
total = 0
for number in numbers:
total += ((number - mean) ** 2)
variance = total / (len(numbers) - 1)
return math.sqrt(variance)
Стандартное отклонение – это мера дисперсии; оно определяет, как сильно отклоняются значения в выборке от среднего значения. Вычисление стандартного отклонения в этой функции выполняется по формуле $$ \frac{\sqrt{\sum(x + \bar{x})^2}}{n-1}, $$ где \( x \) — очередное число, \( \bar{x} \) — среднее значение, а \( n \) — количество чисел.
def print_results(count, statistics):
real = "9.2f"
if statistics.mode is None:
modeline = ""
elif len(statistics.mode) == 1:
modeline = "mode = {0:{fmt}}\n".format(statistics.mode[0], fmt=real)
else:
modeline = ("mode = [" + ", ".join(["{0:.2f}".format(m)
for m in statistics.mode]) + "]\n")
print("""\
count = {0:6}
mean = {mean:{fmt}}
median = {median:{fmt}}
{1}\
std. dev. = {std_dev:{fmt}}""".format(count, modeline, fmt=real, **statistics._asdict()))
Большая часть этой функции связана с форматированием списка модальных
значений в строку modeline
. Если модальные значения отсутствуют, то
строка modeline
вообще не выводится. Если модальное значение
единственное, список модальных значений содержит единственный элемент
(mode[0]
), который и выводится с той же строкой форматирования, что
используется при выводе других статистических значений. Если имеется
несколько модальных значений, они выводятся как список, в котором
каждое значение форматируется отдельно. Делается это с помощью
генератора списков, который позволяет получить список строк с
модальными значениями; строки затем объединяются в единую строку, где
отделяются друг от друга запятой с пробелом (", "
). Последняя
инструкция print()
в самом конце получилась очень простой благодаря
использованию именованного кортежа. Он позволяет обращаться к
статистическим значениям в объекте statistics
, используя не числовые
индексы, а их имена, а благодаря строкам в тройных кавычках мы смогли
отформатировать выводимый текст наглядным способом.
В этой функции имеется одна особенность, о которой следует упомянуть
отдельно. Строка с модальными значениями выводится с помощью элемента
строки формата {2}
, за которым следует символ обратного
слеша. Символ обратного слеша экранирует символ перевода строки,
поэтому если строка с модальными значениями пустая, то пустая строка
выводиться не будет. Именно по этой причине мы вынуждены были добавить
символ \n
в конец строки modeline
, если она не пустая.