Программирование на Python 3. Руководство издательство СимволПлюс
Скачать 3.74 Mb.
|
Примеры Мы завершили обзор встроенных типов коллекций в языке Python, а также двух типов, реализованных в стандартной библиотеке (collec tions.namedtuple и collections.defaultdict). В языке Python имеется так же тип коллекций collections.deque, двухсторонней очереди, и многие другие типы коллекций, реализованные сторонними разработчиками и доступные в каталоге пакетов Python Package Index, pypi.python.org/ pypi . А сейчас мы рассмотрим пару немного более длинных примеров, 176 Глава 3. Типы коллекций в которых используется многое из того, о чем рассказывалось в этой и в предыдущей главах. Первая программа насчитывает примерно семьдесят строк и имеет от ношение к обработке текстовой информации. Вторая программа содер жит примерно девяносто строк и предназначена для выполнения мате матических вычислений. Обе программы используют словари, спи ски, именованные кортежи и множества и обе широко используют ме тод str.format(), который был описан в предыдущей главе. generate_usernames.py Представьте, что мы выполняем настройку новой компьютерной сис темы и нам необходимо сгенерировать имена пользователей для всех служащих нашей компании. У нас имеется простой текстовый файл (в кодировке UTF8), где каждая строка представляет собой запись из полей, разделенных двоеточиями. Каждая запись соответствует одно му сотруднику компании и содержит следующие поля: уникальный идентификатор служащего, имя, отчество (это поле может быть пус тым), фамилию и название отдела. Ниже в качестве примера приво дятся несколько строк из файла data/users.txt: 1601:Albert:Lukas:Montgomery:Legal 3702:Albert:Lukas:Montgomery:Sales 4730:Nadelle::Landale:Warehousing Программа должна читать данные из файла, который указан в ко мандной строке, извлекать отдельные поля из каждой строки (записи) и возвращать подходящие имена пользователей. Каждое имя пользо вателя должно быть уникальным и должно создаваться на основе име ни сотрудника. Результаты должны выводиться на консоль в тексто вом виде, отсортированными в алфавитном порядке по фамилии и име ни, например: Name ID Username Landale, Nadelle................ (4730) nlandale Montgomery, Albert L............ (1601) almontgo Montgomery, Albert L............ (3702) almontgo1 Каждая запись имеет точно пять полей, и хотя можно обращаться к ним по числовым индексам, тем не менее мы будем использовать ос мысленные имена, чтобы сделать программный код более понятным: ID, FORENAME, MIDDLENAME, SURNAME, DEPARTMENT = range(5) В языке Python общепринято использовать только символы верхнего регистра для идентификаторов, которые будут играть роль констант. Нам также необходимо создать тип именованного кортежа, где будут храниться данные о текущем пользователе: Примеры 177 User = collections.namedtuple("User", "username forename middlename surname id") Позднее, когда мы будем рассматривать оставшуюся часть программы, мы увидим, как используется именованный кортеж User и константы. Основная логика программы сосредоточена в функции 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:]: for line in open(filename, encoding="utf8"): 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], 178 Глава 3. Типы коллекций 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], то в случае отсутствия отчества будет возбуждено исключение Index Error . Но при использовании операции извлечения среза мы получаем либо первый символ отчества, либо пустую строку. Затем мы переводим все символы полученного имени пользователя в нижний регистр и ограничиваем его длину восемью символами. Если имя пользователя уже занято (то есть оно уже присутствует в множе Примеры 179 стве 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.for mat() используется довольно оригинальным образом. Для вывода ему определяется строка "", то есть пустая строка; в результате при выводе пустой строки мы полу чаем строку из дефисов заданной ширины поля вывода. Затем мы используем цикл for ... in для вывода информации о каж дом пользователе, извлекая ключи из отсортированного словаря. Для удобства мы создаем переменную user, чтобы не вводить каждый раз users[key] в оставшейся части функции. В цикле сначала вызывается метод str.format(), чтобы записать в переменную name фамилию со трудника, имя и необязательный первый символ отчества. Обращение к элементам в именованном кортеже user производится по их именам. Собрав строку с именем пользователя, мы выводим информацию о пользователе, ограничивая ширину каждого столбца (имя сотрудни ка, идентификатор и имя пользователя) желаемыми значениями. Полный программный код программы (который несколько отличается от того, что мы только что рассмотрели, несколькими начальными строками с комментариями и инструкциями импорта) находится в фай ле generate_usernames.py. Структура программы – чтение данных из Метод str. format() , стр. 100 180 Глава 3. Типы коллекций файла, обработка каждой записи, вывод результата – одна из наиболее часто встречающихся, и она повторяется в следующем примере. statistics.py Предположим, что у нас имеется пакет файлов с данными, содержа щих числовые результаты некоторой обработки, выполненной нами, и нам необходимо вычислить некоторые основные статистические ха рактеристики, которые дадут нам возможность составить общую кар тину о полученных данных. В каждом файле находится обычный текст (в кодировке ASCII), с одним или более числами в каждой строке (разделенными пробельными символами). Ниже приводится пример информации, которую нам необходимо по лучить: count = 183 mean = 130.56 median = 43.00 mode = [5.00, 7.00, 50.00] std. dev. = 235.01 Здесь видно, что было прочитано 183 числа, из которых наиболее час то встречаются числа 5, 7 и 50, и со стандартным отклонением по вы борке 235.01. Сами статистические характеристики хранятся в именованном корте же 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. Для нахожде ния модальных («наиболее часто встречающихся») значений нам необ Примеры 181 ходимо знать, сколько раз встречается каждое число, поэтому мы соз даем словарь со значениями по умолчанию, используя функцию int() в качестве фабричной функции, где будут накапливаться счетчики. Затем выполняется обход списка файлов и производится чтение дан ных из них. В качестве дополнительных аргументов мы передаем функ ции read_data() список и словарь со значениями по умолчанию, чтобы она могла обновлять их. После чтения всех данных мы исходим из предположения, что некоторые числа были благополучно прочитаны, и вызываем функцию calculate_statistics(). Она возвращает имено ванный кортеж типа Statistics, который затем используется для вы вода результатов. def read_data(filename, numbers, frequencies): for lino, line in enumerate(open(filename, encoding="ascii"), start=1): for x in line.split(): try: number = float(x) numbers.append(number) frequencies[number] += 1 except ValueError as err: print("{0}:{1}: skipping {2}: {3}".format( filename, lino, x, err)) Каждая строка разбивается по пробельным символам, после чего про изводится попытка преобразовать каждый элемент в число типа float. Если преобразование удалось, следовательно, это либо целое число, либо число с плавающей точкой в десятичной или в экспоненциальной форме. Полученное число добавляется в список numbers и выполняется обновление словаря frequencies со значениями по умолчанию. (Если бы здесь использовался обычный словарь dict, программный код, вы полняющий обновление словаря, мог бы выглядеть так: frequenci es[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) Эта функция используется для сбора всех статистических характери стик воедино. Поскольку среднее значение вычисляется очень просто, мы делаем это прямо здесь. Вычислением других статистических ха рактеристик занимаются отдельные функции, и в заключение данная 182 Глава 3. Типы коллекций функция возвращает экземпляр именованного кортежа Statistics, со держащий четыре вычисленные статистические характеристики. def calculate_mode(frequencies, maximum_modes): highest_frequency = max(frequencies.values()) mode = [number for number, frequency in frequencies.items() if math.fabs(frequency highest_frequency) <= sys.float_info.epsilon] 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 Медиана («среднее значение») – это значение, находящееся в середине упорядоченной выборки чисел, за исключением случая, когда в вы борке присутствует четное число чисел, – тогда значение медианы оп ределяется как среднее арифметическое значение двух чисел, находя щихся в середине. Функция вычисления медианы сначала выполняет сортировку чисел по возрастанию. Затем посредством целочисленного деления опреде ляется позиция середины выборки, откуда извлекается число и сохра няется как значение медианы. Если выборка содержит четное число Примеры 183 значений, то значение медианы определяется как среднее арифмети ческое двух чисел, находящихся в середине. 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) Стандартное отклонение – это мера дисперсии; оно определяет, как сильно отклоняются значения в выборке от среднего значения. Вычис ление стандартного отклонения в этой функции выполняется по фор муле , где x – очередное число, – 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 = {1.mean:{fmt}} median = {1.median:{fmt}} {2}\ std. dev. = {1.std_dev:{fmt}}""".format( count, statistics, modeline, fmt=real)) Большая часть этой функции связана с форматировани ем списка модальных значений в строку modeline. Если модальные значения отсутствуют, то строка modeline во обще не выводится. Если модальное значение единствен ное, список модальных значений содержит единствен ный элемент (mode[0]), который и выводится с той же строкой форматирования, что используется при выводе других статистических значений. Если имеется несколь ко модальных значений, они выводятся как список, в ко тором каждое значение форматируется отдельно. Дела ется это с помощью генератора списков, который позво Σ x x + ( ) 2 n –1 = Метод |