Перейти до змісту

Л15. pathlib, модулі й фінальний конвеєр Python

Коротко про лекцію

Вийдемо за межі одного фрагмента коду. Навчимося працювати зі шляхами й текстовими файлами через pathlib, розділяти програму на модулі та збирати відтворюваний конвеєр.

У Л10 ми побудували C++ конвеєр read → parse → validate → process → write. У Python цей поділ відповідальностей лишається корисним. Змінюються інструменти для роботи з файлами та організацією коду.

Код і дані можуть жити у різних файлах. Окремі функції теж можна винести у власні модулі. Тому потрібно трасувати код усередині кожного файла та переходи даних між файлами.

Практична мета. Після лекції ви повинні читати невелику багатофайлову Python-програму та пояснювати шлях даних від файла до результату.

1. Модуль дає імена з іншого файла або бібліотеки

Модуль — Python-файл або бібліотечний компонент, який групує пов’язані імена.

Термін: модуль

Модуль дає пов’язаним іменам окремий простір у програмі. Імпорт робить потрібні імена доступними в іншому файлі.

Модуль — це одиниця Python-коду, яку можна підключити через import. Стандартна бібліотека вже містить багато готових модулів.

Для роботи зі шляхами використаємо модуль pathlib. Він надає тип Path.

Запис

from pathlib import Path

означає: взяти ім’я Path із модуля pathlib. Після цього воно доступне в поточному модулі.

Path("note.txt") створює об’єкт шляху. Створення об’єкта ще не читає файл.

Термін: Path

Path представляє шлях у файловій системі як об’єкт. Читання, запис і перевірка існування починаються лише після виклику відповідного методу.

2. Path поєднує шлях і файлові операції

Під час виклику можна явно записати ім’я параметра перед значенням. Запис encoding="utf-8" є іменованим аргументом. Він явно вказує кодування тексту.

Як читати іменований аргумент

У encoding="utf-8" ім’я encoding показує, якому параметру належить значення. Це зручно для необов’язкових параметрів і довших викликів.

Метод write_text(text, encoding="utf-8") записує текст у файл. Метод read_text(encoding="utf-8") читає весь текст.

Метод splitlines() ділить текст на рядки та повертає список без символів кінця рядка.

from pathlib import Path

path = Path("note.txt")
path.write_text("альфа\nбета\n", encoding="utf-8")
text = path.read_text(encoding="utf-8")
print(text.splitlines())

Об’єкт path представляє шлях. Запис і читання відбуваються лише після відповідного виклику методу.

flowchart TD
    A["Path"] --> B["write_text"]
    B --> C["текстовий файл"]
    C --> D["read_text"]
    D --> E["str"]
    E --> F["splitlines"]
    F --> G["список рядків"]

У браузерному Python файл належить файловому середовищу робочого зошита. Не покладайтеся на неописану постійну доступність після перезапуску середовища.

3. Існування файла є окремою умовою

Метод exists() повертає True, якщо шлях існує у поточній файловій системі.

from pathlib import Path

path = Path("missing.txt")
print(path.exists())

Перевірка корисна для деяких рішень інтерфейсу. Проте між перевіркою та читанням файл теоретично може змінитися.

Тому фактична операція читання все одно потребує політики помилок. Її розберемо нижче.

4. Контекстний менеджер завершує роботу з відкритим ресурсом

Path.read_text() і Path.write_text() зручні, коли файл читають або записують цілком. У реальному коді також часто потрібне поступове читання через файловий об’єкт.

Функція open(...) повертає файловий об’єкт. Конструкція with утворює контекстний менеджер: після виходу з блоку файл закривається незалежно від звичайного завершення або винятку.

Термін: контекстний менеджер

Контекстний менеджер задає початок і гарантоване завершення роботи з ресурсом. Для файла with open(...) as input_file: гарантує закриття файлового об’єкта після блоку.

from pathlib import Path

path = Path("notes.txt")
path.write_text("альфа\nбета\n", encoding="utf-8")

with open(path, "r", encoding="utf-8") as input_file:
    first_line = input_file.readline().strip()

print(first_line)

Після завершення блоку with файловий об’єкт більше не повинен використовуватися для читання. Сам шлях path при цьому залишається звичайним об’єктом Path.

5. Текстовий рядок треба розібрати й перевірити

Метод split() без аргументів ділить рядок за пробільними символами. Наприклад, рядок ручка 3 12.5 дає три текстові частини.

Їх можна розпакувати:

name, quantity_text, price_text = line.split()

Функція int(...) перетворює текст у ціле число. Функція float(...) перетворює текст у число з дробовою частиною.

from pathlib import Path

path = Path("sales.txt")
path.write_text("ручка 3 12.5\nкнига 2 40\n", encoding="utf-8")
for line in path.read_text(encoding="utf-8").splitlines():
    name, quantity_text, price_text = line.split()
    print(name, int(quantity_text), float(price_text))

Тут кожен рядок проходить три кроки: поділ, перетворення типів, використання. У реальній програмі перевірка значень буде окремим етапом.

flowchart TD
    A["рядок файла"] --> B["split"]
    B --> C["текстові поля"]
    C --> D["int / float"]
    D --> E["типізовані значення"]
    E --> F["валідація"]
    F --> G["обробка"]

6. Власний .py файл може бути модулем

Власний .py файл може бути модулем, якщо Python може знайти його під час імпорту. Функції модуля утворюють окрему логічну одиницю.

Нехай report_tools.py містить:

def total_sales(rows):
    total = 0.0
    for quantity, price in rows:
        total += quantity * price
    return total

Тоді інший файл може імпортувати потрібне ім’я:

from report_tools import total_sales

rows = [(3, 12.5), (2, 40.0)]
print(total_sales(rows))

Тут інтерактивний запуск не додаємо, бо приклад залежить від двох файлів. Канонічні файли зберігаються разом у пакеті прикладів.

flowchart TD
    A["ex04_report_module.py"] --> B["import total_sales"]
    B --> C["report_tools.py"]
    C --> D["функція total_sales"]
    D --> E["результат повертається коду виклику"]

Межа модуля допомагає бачити залежності. Вона також дозволяє тестувати функції окремо від інтерфейсу програми.

7. Назва імпорту повинна пояснювати роль

Винесення функції у модуль має сенс, коли модуль отримує зрозумілу відповідальність. Наприклад, нормалізацію тексту можна відокремити від файлового коду.

text_tools.py:

def normalize(text):
    return text.strip().lower()

Код використання:

from text_tools import normalize

print(normalize("  PyThOn "))

Імпорт показує залежність прямо у верхній частині файла. Читач бачить, звідки походить функція normalize.

8. Файлова помилка потребує конкретної політики

Файл може бути відсутнім під час фактичного читання. У такому випадку read_text може підняти FileNotFoundError.

Після try/except можна додати блок else. Він виконується, якщо код у try завершився без винятку.

from pathlib import Path

path = Path("input.txt")
try:
    text = path.read_text(encoding="utf-8")
except FileNotFoundError:
    print("вхідний файл відсутній")
else:
    print(len(text))

except FileNotFoundError описує очікувану файлову проблему. Інші винятки не приховуються цим обробником.

Блок else містить роботу, яка потребує успішного читання. Так нормальний шлях не змішується з реакцією на відсутній файл.

9. parse і validate мають різні контракти

Розбір відповідає на питання «чи можна отримати поля потрібної форми?». Валідація відповідає на питання «чи допустимі отримані значення для задачі?».

Наприклад, рядок книга -2 40 успішно перетворюється на число -2. Проте від’ємна кількість може порушувати правило предметної області.

Тому корисно мати дві функції:

parse_sale(line) → структура або ValueError
valid_sale(sale) → True / False

Цей поділ уже знайомий із Л10. Python змінює синтаксис, а межа відповідальності лишається.

Перенесення моделі

parse відповідає за форму зовнішнього запису, а validate — за правила задачі. Цей поділ не залежить від мови програмування.

10. Фінальний конвеєр складається з кількох файлів

Побудуємо модуль sales_tools.py. Він відповідатиме за розбір і перевірку одного запису.

def parse_sale(line):
    parts = line.split()
    if len(parts) != 3:
        raise ValueError("очікуються три поля")
    name, quantity_text, price_text = parts
    return name, int(quantity_text), float(price_text)

def valid_sale(sale):
    name, quantity, price = sale
    return name != "" and quantity > 0 and price >= 0.0

Слово continue має знайомий зміст: завершити поточну ітерацію циклу та перейти до наступної. Воно доречне після підрахунку відхиленого рядка.

Оператор not заперечує логічне значення. Тому if not valid_sale(sale): читаємо як «якщо запис не є допустимим».

Основний файл координує етапи:

from pathlib import Path
from sales_tools import parse_sale, valid_sale

path = Path("sales.txt")
path.write_text("ручка 3 12.5\nпомилка\nкнига 2 40\n", encoding="utf-8")

total = 0.0
rejected = 0
# Один помилковий рядок відхиляємо локально, щоб решта файла продовжила оброблятися.
for line in path.read_text(encoding="utf-8").splitlines():
    try:
        sale = parse_sale(line)
    except ValueError:
        rejected += 1
        continue
    if not valid_sale(sale):
        rejected += 1
        continue

    name, quantity, price = sale
    # До підсумку потрапляють лише записи, що успішно розібрані та пройшли валідацію.
    total += quantity * price

print("Сума:", total)
print("Відхилено:", rejected)

Цей приклад також багатофайловий, тому він показаний статично. Для виконання потрібні ex07_final_pipeline.py і sales_tools.py в одному середовищі.

flowchart TD
    A["sales.txt"] --> B["read_text + splitlines"]
    B --> C["parse_sale"]
    C --> D{"ValueError?"}
    D -- так --> R["rejected += 1"]
    D -- ні --> V["valid_sale"]
    V -- false --> R
    V -- true --> P["додати до total"]
    R --> N["наступний рядок"]
    P --> N

11. Відтворюваність важливіша за випадковий успіх

Багатофайлова програма має явні залежності. Потрібні файли коду повинні лежати там, де імпорт може їх знайти. Вхідні дані теж мають мати визначене місце.

Перевірка готової програми включає чистий запуск. Створіть потрібні вхідні дані, запустіть головний файл і перевірте результат. Потім змініть один граничний випадок та повторіть перевірку.

У зошиті той самий принцип набуває форми Restart → Run All. У багатофайловій програмі він означає запуск із чітко визначеного набору файлів та даних.

12. Підсумкова модель курсу

До цієї лекції ви пройшли повний малий цикл розробки:

задача
→ модель даних
→ алгоритм
→ код
→ трасування
→ тести
→ обробка помилок
→ зовнішні дані
→ модулі
→ відтворюваний запуск

Мова може змінювати синтаксис. Цикл міркування лишається переносним. Цю послідовність дій варто переносити у наступні дисципліни.

13. Інтерактивна самоперевірка лекції

Самоперевірка вимагає трасувати файловий конвеєр, імпорти, розбір і політику помилок. Частина питань просить знайти межу, на якій повинна виконуватися перевірка.