Модуль 7 · Урок 7.1
Законченная программа: анализ файла
Договорённость, полный код, разбор новых деталей и проверка программы на разных файлах.
Сумму и наибольшее из 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())Тонкости:неверная кодировка файла
Если файл записан не в 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"])type(error).__name__ — имя типа исключения. Пустой список даёт отчёт с None, а не ошибку — так записано в договорённости.
Развитие программы
Программу развивают через новую договорённость, а не случайными правками. Каждое изменение сначала записывают пунктом договорённости, затем меняют одну функцию и проверяют старые и новые случаи.
- подсчёт чётных чисел — новая строка в
summarize; - заголовок с количеством чисел — правка
read_numbersи новая ошибка формата; - огромный файл — один проход без списка, как в уроке о скорости и памяти;
- отчёт в JSON — новый вывод в
main.
Список здесь оставлен нарочно: так summarize принимает готовые данные и её легко проверить. Три из этих улучшений — задачи практики урока.
Итог
Сквозная задача стала программой: договорённость, три функции и проверка на разных файлах. Вот что понадобится дальше.
- ДоговорённостьВход, выход и спорные случаи — до кода; каждый пункт — будущая проверка.
- Три функции
read_numbers— формат,summarize— вычисление,main— запуск и вывод. raise … fromПонятное сообщение с номером строки, исходная ошибка — причиной;as errorдаёт ей имя.- Запуск
sys.argv— аргументы,sys.stderr— ошибки, код завершения 0 — успех. - ПроверкаОбычный, пустой, одна строка, нет файла, испорченная строка.
Проверьте себя
Ответь сначала сам, потом открой ответ.
Что выведет программа для пустого файла?
ОтветСкрыть ответ
«Количество: 0», «Сумма: 0» и «Файл пуст: минимум, максимум и среднее не определены»; код завершения 0.
Что выведет программа для файла из одной строки -5?
ОтветСкрыть ответ
Количество 1, сумма −5, минимум и максимум −5, среднее −5.000.
Какое сообщение получит пользователь, если первая строка файла — abc? В какой поток оно попадёт?
ОтветСкрыть ответ
«Не удалось прочитать данные: Строка 1: ожидалось целое число» — в поток ошибок sys.stderr, код завершения 1.
Что содержит sys.argv при запуске python analyze_numbers.py data.txt?
ОтветСкрыть ответ
['analyze_numbers.py', 'data.txt'].
Зачем summarize принимает список, а не путь к файлу?
ОтветСкрыть ответ
Так вычисление проверяется на готовых списках и работает с любым источником данных — файлом, вводом, списком в коде.
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Допиши программу: чётные числа в отчёте
Скачай numbers.txt. Добавь в summarize программы урока ключ "even" — количество чётных чисел — и напечатай его в main строкой Чётных: …. Запусти программу для numbers.txt и запиши число из этой строки.
Введите число.
Подсказка
Чётность проверяют x % 2 == 0; для −2 это тоже верно.
Разбор
Например, в summarize:
"even": sum(1 for x in numbers if x % 2 == 0),Чётные в файле — −2 и 10.
Ответ: 2
Напиши вариант без списка
Скачай numbers.txt. Напиши вариант программы без списка: за один проход по файлу обновляй количество, сумму, минимум и максимум, затем напечатай их одним print. Запусти и запиши, что напечатала программа.
Запишите четыре числа через пробел: количество, сумму, минимум, максимум.
Подсказка
Четыре переменные; минимум и максимум начинаются с None, как best в уроке о файлах.
Разбор
Например:
count = 0total = 0smallest = Nonelargest = Nonewith open("numbers.txt", encoding="utf-8") as file: for line in file: number = int(line) count += 1 total += number if smallest is None or number < smallest: smallest = number if largest is None or number > largest: largest = numberprint(count, total, smallest, largest)Память не растёт с размером файла: хранятся четыре числа.
Ответ: 4 20 -2 10
Измени договорённость: заголовок с количеством
Скачай header.txt: первая строка — количество чисел, дальше сами числа. Измени read_numbers: прочитай заголовок отдельно, а если строк данных не столько, сколько в нём сказано, вызови ValueError. Запусти программу для header.txt и запиши сумму из отчёта.
Введите число.
Подсказка
Заголовок читают int(file.readline()) до цикла; после цикла сравни его с len(numbers).
Разбор
Например, в начале read_numbers:
expected = int(file.readline())и после цикла:
if len(numbers) != expected: raise ValueError("Количество не совпадает с заголовком")Заголовок 3 в сумму не попадает: 4 − 1 + 6 = 9.
Ответ: 9