К содержанию

Модуль 7 · Урок 7.1

Законченная программа: анализ файла

Договорённость, полный код, разбор новых деталей и проверка программы на разных файлах.

3 задачи

Сумму и наибольшее из 5, −2, 10, 7 мы считали весь курс: тремя if, циклом, списком, функцией, по файлу. Последний шаг — программа, которую можно отдать другому человеку: она читает любой файл с числами, печатает отчёт, а на испорченном файле объясняет, что не так. Законченная программа начинается с договорённости о входе и выходе и собирается из знакомых частей: чтение, вычисление, вывод и обработка ожидаемых ошибок.

Читать она будет знакомый numbers.txt:

5-2107

В отчёте — количество, сумма, минимум, максимум и среднее. Так же устроено решение задачи экзамена с файлом: прочитать, посчитать, вывести.

Договорённость

Сначала договариваются, что программа принимает и что делает в спорных случаях. Код пишут потом — по этим пунктам:

  • вход — текстовый файл, по одному целому числу на строке, без заголовка; числа могут быть отрицательными;
  • путь к файлу передают при запуске; если его не передали, читается numbers.txt в рабочей папке;
  • выход — количество, сумма, минимум, максимум и среднее с тремя знаками после точки;
  • пустой файл разрешён: минимум, максимум и среднее для него не определены, и программа так и пишет;
  • строка, которая не число, — ошибка формата: программа называет её номер и не считает ничего.

Каждый пункт потом станет проверочным случаем: обычный файл, пустой, без файла, с испорченной строкой.

Чтение файла

Программа делится на три функции, и данные идут через них по очереди. read_numbers отвечает за формат файла, summarize — за вычисление, main — за запуск и вывод, как в уроке о частях программы:

Каждая функция получает готовое от предыдущей. Если файл не прочитан, данные до summarize не доходят: сообщение уходит в поток ошибок, программа завершается с кодом 1.

Первая функция читает файл и превращает строки в список чисел:

def read_numbers(path):    numbers = []    with open(path, encoding="utf-8") as file:        for line_number, line in enumerate(file, start=1):            try:                number = int(line)            except ValueError as error:                raise ValueError(                    f"Строка {line_number}: ожидалось целое число"                ) from error            numbers.append(number)    return numbers

Цикл по файлу и enumerate(…, start=1) — из уроков о файлах и кортежах: line_number считает строки с единицы, как их видит человек. Новое здесь — две записи в обработке ошибки.

except ValueError as error даёт пойманному исключению имя error, чтобы с ним можно было работать дальше. raise … from error создаёт новое исключение с понятным текстом — номером строки — и сохраняет исходное как причину. Вместо invalid literal for int() with base 10: 'abc\n' пользователь увидит «Строка 2: ожидалось целое число».

Вычисление и запуск

summarize считает отчёт по готовому списку, main связывает части и печатает. Отчёт — словарь из урока о словарях: у каждого числа своё имя.

def summarize(numbers):    if not numbers:        return {            "count": 0,            "total": 0,            "minimum": None,            "maximum": None,            "average": None,        }    total = sum(numbers)    return {        "count": len(numbers),        "total": total,        "minimum": min(numbers),        "maximum": max(numbers),        "average": total / len(numbers),    }

Для пустого списка min и max упали бы с ошибкой, поэтому функция заранее возвращает None там, где ответа нет, — так записано в договорённости. Функция не знает о файле: её можно проверить на любом списке.

def main():    if len(sys.argv) > 2:        print(            "Использование: python analyze_numbers.py [путь]",            file=sys.stderr,        )        return 2    if len(sys.argv) == 2:        path = Path(sys.argv[1])    else:        path = Path("numbers.txt")    try:        numbers = read_numbers(path)    except (OSError, ValueError) as error:        print(            f"Не удалось прочитать данные: {error}",            file=sys.stderr,        )        return 1     report = summarize(numbers)    print("Количество:", report["count"])    print("Сумма:", report["total"])    if report["count"] == 0:        print(            "Файл пуст: минимум, максимум "            "и среднее не определены"        )    else:        print("Минимум:", report["minimum"])        print("Максимум:", report["maximum"])        print(f"Среднее: {report['average']:.3f}")    return 0  if __name__ == "__main__":    raise SystemExit(main())

В main три новые детали запуска из терминала.

  • sys.argv — список строк запуска: первый элемент — имя программы, дальше — то, что написали после него. Для python analyze_numbers.py data.txt это ['analyze_numbers.py', 'data.txt'].
  • file=sys.stderr печатает сообщение в поток ошибок — отдельный от обычного вывода, как красный текст ошибки в первом уроке.
  • return 0, 1 или 2 — код завершения: raise SystemExit(main()) передаёт его системе. 0 значит «успех», другие числа — «что-то не так»; это читают программы, которые запускают нашу.

OSError в except (OSError, ValueError) — общее имя ошибок доступа к файлу, среди них FileNotFoundError. Ошибку внутри summarize мы не перехватываем: такая ошибка — ошибка автора, её надо увидеть, а не спрятать.

Вся программа целиком — сохрани её в analyze_numbers.py рядом с numbers.txt и запусти:

import sysfrom pathlib import Path  def read_numbers(path):    numbers = []    with open(path, encoding="utf-8") as file:        for line_number, line in enumerate(file, start=1):            try:                number = int(line)            except ValueError as error:                raise ValueError(                    f"Строка {line_number}: ожидалось целое число"                ) from error            numbers.append(number)    return numbers  def summarize(numbers):    if not numbers:        return {            "count": 0,            "total": 0,            "minimum": None,            "maximum": None,            "average": None,        }    total = sum(numbers)    return {        "count": len(numbers),        "total": total,        "minimum": min(numbers),        "maximum": max(numbers),        "average": total / len(numbers),    }  def main():    if len(sys.argv) > 2:        print(            "Использование: python analyze_numbers.py [путь]",            file=sys.stderr,        )        return 2    if len(sys.argv) == 2:        path = Path(sys.argv[1])    else:        path = Path("numbers.txt")    try:        numbers = read_numbers(path)    except (OSError, ValueError) as error:        print(            f"Не удалось прочитать данные: {error}",            file=sys.stderr,        )        return 1     report = summarize(numbers)    print("Количество:", report["count"])    print("Сумма:", report["total"])    if report["count"] == 0:        print(            "Файл пуст: минимум, максимум "            "и среднее не определены"        )    else:        print("Минимум:", report["minimum"])        print("Максимум:", report["maximum"])        print(f"Среднее: {report['average']:.3f}")    return 0  if __name__ == "__main__":    raise SystemExit(main())
Количество: 4 Сумма: 20 Минимум: -2 Максимум: 10 Среднее: 5.000
Тонкости:неверная кодировка файла

Если файл записан не в UTF-8, чтение вызовет UnicodeDecodeError. Это разновидность ValueError, поэтому main обработает и её.

Проверка программы

Программу проверяют не только на обычном файле, но и на пустом, отсутствующем и испорченном. Запуск из терминала с путём к файлу:

python analyze_numbers.py numbers.txt

Повтори его для пустого файла, для файла из одной строки -5, для несуществующего missing.txt и для bad.txt со строками 5, abc, 7. Для испорченного файла программа должна назвать номер строки, а не пропустить её молча.

Функции можно проверить и без терминала: импортировать их в другой файл, как в уроке о частях программы. При импорте main не запускается — его защищает if __name__ == "__main__".

from analyze_numbers import read_numbers, summarize for name in ["bad.txt", "missing.txt"]:    try:        read_numbers(name)    except (OSError, ValueError) as error:        print(type(error).__name__, error)print(summarize([]))print(summarize([-5])["average"])
ValueError Строка 2: ожидалось целое число FileNotFoundError [Errno 2] No such file or directory: 'missing.txt' {'count': 0, 'total': 0, 'minimum': None, 'maximum': None, 'average': None} -5.0

type(error).__name__ — имя типа исключения. Пустой список даёт отчёт с None, а не ошибку — так записано в договорённости.

Развитие программы

Программу развивают через новую договорённость, а не случайными правками. Каждое изменение сначала записывают пунктом договорённости, затем меняют одну функцию и проверяют старые и новые случаи.

  • подсчёт чётных чисел — новая строка в summarize;
  • заголовок с количеством чисел — правка read_numbers и новая ошибка формата;
  • огромный файл — один проход без списка, как в уроке о скорости и памяти;
  • отчёт в JSON — новый вывод в main.

Список здесь оставлен нарочно: так summarize принимает готовые данные и её легко проверить. Три из этих улучшений — задачи практики урока.

Итог

Сквозная задача стала программой: договорённость, три функции и проверка на разных файлах. Вот что понадобится дальше.

  • ДоговорённостьВход, выход и спорные случаи — до кода; каждый пункт — будущая проверка.
  • Три функцииread_numbers — формат, summarize — вычисление, main — запуск и вывод.
  • raise … fromПонятное сообщение с номером строки, исходная ошибка — причиной; as error даёт ей имя.
  • Запускsys.argv — аргументы, sys.stderr — ошибки, код завершения 0 — успех.
  • ПроверкаОбычный, пустой, одна строка, нет файла, испорченная строка.

Проверьте себя

Ответь сначала сам, потом открой ответ.

Вопрос 1

Что выведет программа для пустого файла?

ОтветСкрыть ответ
Ответ

«Количество: 0», «Сумма: 0» и «Файл пуст: минимум, максимум и среднее не определены»; код завершения 0.

Вопрос 2

Что выведет программа для файла из одной строки -5?

ОтветСкрыть ответ
Ответ

Количество 1, сумма −5, минимум и максимум −5, среднее −5.000.

Вопрос 3

Какое сообщение получит пользователь, если первая строка файла — abc? В какой поток оно попадёт?

ОтветСкрыть ответ
Ответ

«Не удалось прочитать данные: Строка 1: ожидалось целое число» — в поток ошибок sys.stderr, код завершения 1.

Вопрос 4

Что содержит sys.argv при запуске python analyze_numbers.py data.txt?

ОтветСкрыть ответ
Ответ

['analyze_numbers.py', 'data.txt'].

Вопрос 5

Зачем summarize принимает список, а не путь к файлу?

ОтветСкрыть ответ
Ответ

Так вычисление проверяется на готовых списках и работает с любым источником данных — файлом, вводом, списком в коде.

Практика

Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.

Допиши программу: чётные числа в отчёте

Скачай numbers.txt. Добавь в summarize программы урока ключ "even" — количество чётных чисел — и напечатай его в main строкой Чётных: …. Запусти программу для numbers.txt и запиши число из этой строки.

numbers.txtЧисла 5, −2, 10, 7 по одному на строке — файл из урокаТекстовый файл · 10 Б

Введите число.

Напиши вариант без списка

Скачай numbers.txt. Напиши вариант программы без списка: за один проход по файлу обновляй количество, сумму, минимум и максимум, затем напечатай их одним print. Запусти и запиши, что напечатала программа.

numbers.txtЧисла 5, −2, 10, 7 по одному на строке — файл из урокаТекстовый файл · 10 Б

Запишите четыре числа через пробел: количество, сумму, минимум, максимум.

Измени договорённость: заголовок с количеством

Скачай header.txt: первая строка — количество чисел, дальше сами числа. Измени read_numbers: прочитай заголовок отдельно, а если строк данных не столько, сколько в нём сказано, вызови ValueError. Запусти программу для header.txt и запиши сумму из отчёта.

header.txtФайл из урока: заголовок 3 и три числаТекстовый файл · 9 Б

Введите число.