Финальный проект: создание приложения
Комплексное применение ООП, структур данных, работы с файлами и исключениями для создания реального продукта.
Добро пожаловать в финальный проект курса!
Поздравляем вас с достижением этого важного этапа. Вы прошли долгий путь от изучения базовых типов данных до понимания сложных концепций объектно-ориентированного и функционального программирования. На уровне Intermediate вы перестаете просто копировать чужой код и начинаете понимать, как и почему он работает. Ваша цель теперь — не просто заставить программу работать, а написать эффективный, модульный и поддерживаемый код. Мы окончательно отойдем от написания линейных скриптов (так называемого лапша-кода) к структурированному программированию.
В этом уроке мы будем использовать методологию Project-Based Learning (Проектное обучение). Проектное обучение — это метод, при котором вы осваиваете новые знания и навыки не через сухие лекции, а выполняя настоящий, значимый и сложный проект. Вы работаете над реальной проблемой, что требует от вас поиска информации, планирования, анализа и применения паттернов проектирования. Это не просто задание в конце темы, а основной способ закрепления всего пройденного материала.
Наш проект — Система управления библиотекой (Advanced Library Management System). Это консольное приложение (CLI), которое позволит управлять книжным фондом, регистрировать пользователей, выдавать и возвращать книги, а также вести журнал событий (логирование) и сохранять данные в файл JSON. Создание такого приложения потребует от вас применения следующих навыков: создание классов и объектов, работа с магическими методами, инкапсуляция с использованием декоратора @property, обработка исключений (try/except/finally), работа с контекстными менеджерами (with open), а также использование продвинутых коллекций и генераторов списков (list comprehensions).
Прежде чем писать сложный код, давайте вспомним три важнейших принципа из философии The Zen of Python, которые должны стать вашим руководством при проектировании архитектуры приложения. Во-первых, красивое лучше, чем уродливое. Ваш код должен быть читаемым и структурированным. Во-вторых, явное лучше, чем неявное. Не используйте скрытые механизмы или неочевидные побочные эффекты в функциях. В-третьих, простое лучше, чем сложное. Если задачу можно решить простым циклом, не нужно городить сложную иерархию классов. В нашем проекте мы будем придерживаться этих принципов, разбивая сложную задачу на множество маленьких, независимых модулей (компонентов).
Каждый этап разработки будет сопровождаться блоками Active Recall (Активного припоминания). Это означает, что после изучения части архитектуры мы будем проверять ваши знания с помощью тестов и карточек без подсказок. Такой подход запускает эффект тестирования, который перемещает информацию в долговременное хранилище памяти. Готовы? Тогда давайте приступим к проектированию архитектуры нашего первого серьезного программного продукта!
Задание
Этап 1: Проектирование архитектуры и постановка задач. Ознакомьтесь с требованиями к проекту.
- Определить сущности (Модели данных): Книга, Пользователь, Библиотека.
- Определить механизмы хранения данных: JSON файлы для постоянного хранения.
- Определить функционал логирования: кастомная функция с использованием *args и **kwargs.
- Спроектировать обработку ошибок: создание пользовательских классов исключений.
- Спроектировать интерфейс пользователя: текстовое меню (CLI).
Глубокое погружение: Проектирование сущности Book (Книга)
Любое объектно-ориентированное приложение начинается с проектирования моделей данных. Модель данных — это класс, который описывает состояние и поведение сущности из реального мира. В нашей библиотеке основной сущностью является Книга. Давайте детально разберем, какие атрибуты и методы нам понадобятся, и как мы можем применить принципы инкапсуляции для защиты состояния объекта.
Создание класса Book потребует атрибутов: title (название), author (автор), isbn (уникальный идентификатор книги) и приватного атрибута _is_borrowed (статус выдачи, по умолчанию False). Почему мы используем нижнее подчеркивание для статуса? Это конвенция Python, указывающая на то, что атрибут предназначен для внутреннего использования (protected/private). Напрямую менять статус книги извне (например, book._is_borrowed = True) — это антипаттерн, который нарушает инкапсуляцию и может привести к неконсистентному состоянию данных. Вместо этого мы реализуем методы borrow_book() и return_book(), которые будут контролировать бизнес-логику выдачи и возврата.
Кроме того, мы используем декоратор @property для создания метода status(), который будет возвращать человекочитаемое состояние книги (Доступна или Выдана), не позволяя при этом напрямую изменять внутренний флаг. Декоратор @property превращает вызов метода в доступ к атрибуту, что делает интерфейс класса чище и интуитивно понятнее. Это классический пример применения принципа явное лучше, чем неявное.
Также мы должны реализовать магические методы __str__ и __repr__. Метод __str__ предназначен для пользователя — он должен возвращать красивую строку, например: '1984' автор George Orwell. А метод __repr__ предназначен для разработчика и отладки — он должен возвращать строку, по которой можно воссоздать объект: Book('1984', 'George Orwell'). Это критически важное различие на уровне Intermediate. Многие джуниоры забывают про __repr__, из-за чего при выводе списка объектов в консоль они видят нечитаемые адреса в памяти вида <__main__.Book object at 0x7f8b9c...>.
При реализации методов выдачи мы также заложим фундамент для обработки ошибок. Если пользователь пытается взять книгу, которая уже выдана, метод не должен просто возвращать False. Он должен сообщать об этом явно. В дальнейшем мы перепишем этот механизм на выброс пользовательских исключений (Custom Exceptions), но на первом этапе ограничимся возвратом булевых значений и выводом сообщений. Помните: правильное проектирование базовых классов — это 80% успеха всего проекта.
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self._is_borrowed = False
def borrow_book(self):
if self._is_borrowed:
print(f"Книга '{self.title}' уже выдана.")
return False
self._is_borrowed = True
print(f"Вы взяли книгу '{self.title}'.")
return True
def return_book(self):
if not self._is_borrowed:
print(f"Книга '{self.title}' не была выдана.")
return False
self._is_borrowed = False
print(f"Книга '{self.title}' возвращена.")
return True
@property
def status(self):
return "Выдана" if self._is_borrowed else "Доступна"
def __str__(self):
return f"{self.title} by {self.author} ({self.status})"
def __repr__(self):
return f"Book('{self.title}', '{self.author}', '{self.isbn}')"
Какая основная цель использования декоратора @property в классе Book?
Флеш-карточки
В чем разница между магическими методами __str__ и __repr__?
Нажмите, чтобы увидеть ответ
__str__ возвращает читаемое представление для пользователя, а __repr__ - техническое представление для разработчика, в идеале позволяющее воссоздать объект.
Нажмите, чтобы вернуться
Что означает одинарное подчеркивание перед именем атрибута (например, _is_borrowed)?
Нажмите, чтобы увидеть ответ
Это конвенция (соглашение) Python, указывающая, что атрибут предназначен для внутреннего использования в классе и не должен изменяться напрямую извне.
Нажмите, чтобы вернуться
Как называется принцип сокрытия внутреннего состояния объекта и предоставления доступа через публичные методы?
Нажмите, чтобы увидеть ответ
Инкапсуляция (Encapsulation).
Нажмите, чтобы вернуться
Анализ типичных ошибок: Mutable Default Arguments
В процессе создания системы управления библиотекой нам потребуется класс User (Пользователь). У пользователя должно быть имя, идентификатор и список книг, которые он взял. На этом этапе многие Junior-разработчики совершают одну из самых классических и коварных ошибок в Python — использование изменяемых объектов (mutable objects) в качестве значений по умолчанию для аргументов функции или метода __init__. Эта ошибка настолько распространена, что заслуживает отдельного глубокого разбора.
Представьте ситуацию (симуляция Code Review). Джуниор написал класс User со следующим конструктором: def __init__(self, name, borrowed_books=[]). Логика кажется очевидной: если при создании пользователя мы не передаем список книг, он должен создаваться пустым. Но когда Джуниор создает двух разных пользователей, скажем, Алису и Боба, и Алиса берет книгу, эта же книга мистическим образом появляется в списке у Боба! Почему это происходит?
В Python аргументы по умолчанию оцениваются и создаются только один раз — в момент определения функции или класса (когда интерпретатор читает файл), а не при каждом вызове. Если вы используете изменяемый объект (список, словарь, множество) как значение по умолчанию, этот один и тот же объект в памяти будет делиться между всеми вызовами функции или всеми экземплярами класса, которые используют значение по умолчанию. То есть и Алиса, и Боб получают ссылку на один и тот же список в памяти. Изменяя список Алисы, мы меняем список Боба, так как это один и тот же объект!
Как Senior-разработчик исправляет эту ситуацию? Правильный паттерн (Pythonic way) — использовать None в качестве значения по умолчанию, а внутри метода проверять это значение и создавать новый пустой список. Код должен выглядеть так: def __init__(self, name, borrowed_books=None): и затем self.borrowed_books = borrowed_books if borrowed_books is not None else []. Таким образом, при каждом создании нового объекта User без указания списка книг, интерпретатор будет создавать абсолютно новый, независимый пустой список. Понимание этого механизма отличает новичка от уверенного программиста уровня Intermediate. Всегда помните разницу между Mutable (list, dict, set) и Immutable (int, str, tuple) типами данных при проектировании интерфейсов функций.
class User:
def __init__(self, name, user_id, borrowed_books=None):
self.name = name
self.user_id = user_id
# Правильная обработка изменяемого аргумента по умолчанию
self.borrowed_books = borrowed_books if borrowed_books is not None else []
def add_book(self, book):
if book not in self.borrowed_books:
self.borrowed_books.append(book)
def remove_book(self, book):
if book in self.borrowed_books:
self.borrowed_books.remove(book)
def __str__(self):
return f"User {self.name} (ID: {self.user_id}) - Books: {len(self.borrowed_books)}"
Почему использование `def __init__(self, name, items=[])` является плохой практикой в Python?
Напишите ключевое слово (значение), которое принято использовать по умолчанию для аргументов вместо изменяемых типов (например, вместо пустого списка).
Гибкость логирования: Использование *args и **kwargs
В любом серьезном приложении необходимо вести журнал событий — логирование. Логи помогают понять, что происходило в программе до того, как она завершилась с ошибкой, какие действия совершали пользователи и какие данные обрабатывались. В нашем проекте мы создадим собственную гибкую функцию логгера, которая продемонстрирует мощь распаковки аргументов в Python с помощью операторов *args и **kwargs.
На уровне Intermediate вы должны понимать, что функции часто должны быть готовы к приему переменного количества аргументов. Если мы пишем функцию логирования, мы заранее не знаем, сколько дополнительных деталей или метаданных захочет передать разработчик в момент вызова. Оператор *args (позиционные аргументы) собирает все переданные позиционные значения в кортеж (tuple). Оператор **kwargs (именованные аргументы или ключевые слова) собирает все именованные параметры в словарь (dict). Сочетание этих двух конструкций позволяет создать универсальный интерфейс.
В нашем приложении функция custom_log будет принимать один обязательный аргумент message (основное сообщение лога). Далее будут идти *args для передачи дополнительных деталей (например, кодов ошибок, имен связанных объектов), а затем **kwargs для метаданных системы (например, timestamp, user_id, action_type). Внутри функции мы будем проверять наличие args и kwargs (помните, что пустые коллекции в Python оцениваются как False в логическом контексте) и динамически формировать итоговую строку вывода. Использование методов строк, таких как ', '.join(map(str, args)), позволит нам элегантно и в стиле Pythonic way преобразовать любые переданные объекты в читаемую строку.
Такой подход делает архитектуру приложения невероятно масштабируемой. Сегодня мы логируем просто выдачу книги, передавая ID пользователя. Завтра мы захотим логировать еще и IP-адрес терминала и время операции. Благодаря **kwargs нам не придется переписывать сигнатуру функции логгера во всем проекте — мы просто добавим новые именованные аргументы в месте вызова, и логгер автоматически их обработает и выведет. Это яркий пример того, как глубокое понимание синтаксиса языка позволяет писать поддерживаемый и расширяемый код промышленного уровня.
import datetime
def custom_log(message, *args, **kwargs):
"""
Гибкая функция для логирования системных событий.
"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] LOG: {message}")
if args:
# Преобразуем все args в строки и объединяем
details = ', '.join(map(str, args))
print(f" ├─ Details: {details}")
if kwargs:
print(" └─ Meta:")
for key, value in kwargs.items():
print(f" ├─ {key}: {value}")
# Примеры использования в проекте:
custom_log("Инициализация системы", "Версия 1.0", "Модуль ядра")
custom_log("Выдача книги", 1984, user_id=101, action="borrow", success=True)
В какие структуры данных упаковываются аргументы *args и **kwargs внутри функции?
Задание
Этап 2: Создание главного класса менеджера (Library).
- Создать класс Library, который будет хранить списки книг и пользователей.
- Реализовать метод регистрации пользователя.
- Реализовать метод добавления новой книги в фонд.
- Использовать встроенные функции для поиска (например, генераторы списков) внутри методов менеджера.
Продвинутые структуры данных: List Comprehensions в бизнес-логике
Теперь, когда у нас есть базовые модели Книги и Пользователя, нам нужен класс-оркестратор — Library. Этот класс будет содержать коллекции объектов и управлять взаимодействием между ними. Внутри менеджера нам часто придется выполнять операции фильтрации и поиска. Например, найти все доступные книги, найти книги определенного автора или найти пользователя по его ID. Здесь на сцену выходят List Comprehensions (Списковые включения или генераторы списков).
Многие новички пишут код поиска с использованием классических циклов for. Например, создают пустой список, перебирают все книги в цикле, проверяют условие через if, и если оно выполняется — делают append() в список. Этот подход (процедурный) работает, но он медленный и занимает много строк. На уровне Intermediate вы должны использовать 'Pythonic way' — встроенные возможности языка для краткости и скорости выполнения (написанные на C под капотом). List comprehension позволяет уместить весь этот процесс в одну элегантную и читаемую строку: [book for book in self.books if not book._is_borrowed].
Но мощь list comprehensions не заканчивается на простой фильтрации. Представим задачу: нам нужно получить список только названий (строк) тех товаров (книг), которые есть в наличии, и привести их к верхнему регистру для красивого вывода в меню. Генератор списка справится с этим легко: [book.title.upper() for book in self.books if book.status == 'Доступна']. В нашем классе Library мы будем активно использовать этот паттерн для реализации методов поиска. Однако, помните про баланс: если генератор списка становится слишком длинным, содержит вложенные циклы и сложные условия (становится нечитабельным), философия Python (Читаемость имеет значение) диктует нам вернуться к классическому циклу for или разбить логику на отдельные функции.
Кроме списков, менеджер библиотеки может использовать и словари для оптимизации поиска. Если поиск пользователя по ID в списке из 100 000 записей занимает линейное время O(n), то поиск в словаре, где ключом является ID, а значением — объект User, занимает константное время O(1) благодаря хеш-таблицам под капотом. В нашем учебном проекте мы пока будем использовать списки для простоты и отработки list comprehensions, но в реальных высоконагруженных системах выбор правильной структуры данных (список, множество или словарь) является критически важным архитектурным решением.
class Library:
def __init__(self, name):
self.name = name
self.books = []
self.users = []
def add_book(self, book):
self.books.append(book)
custom_log("Книга добавлена в фонд", book.title, isbn=book.isbn)
def register_user(self, user):
self.users.append(user)
custom_log("Пользователь зарегистрирован", user.name, id=user.user_id)
def get_available_books(self):
# Использование List Comprehension для эффективной фильтрации
return [book for book in self.books if book.status == "Доступна"]
def find_books_by_author(self, author_name):
# Игнорируем регистр при поиске для удобства
return [b for b in self.books if author_name.lower() in b.author.lower()]
def find_user_by_id(self, user_id):
# Использование next() с генератором для поиска одного элемента
# Это эффективнее list comprehension, так как останавливается при первом совпадении
return next((u for u in self.users if u.user_id == user_id), None)
Какое преимущество имеет использование выражения `next((u for u in self.users if u.user_id == user_id), None)` по сравнению со списковым включением (list comprehension) при поиске ОДНОГО пользователя?
Управление исключениями: Custom Exceptions (Собственные классы ошибок)
Пришло время реализовать бизнес-логику выдачи и возврата книг. В реальных приложениях ошибки неизбежны. Пользователь может попытаться взять книгу, которой нет в библиотеке. Он может попытаться взять книгу, которая уже выдана кому-то другому. Или он может попытаться вернуть книгу, которую никогда не брал. Как правильно обрабатывать такие сценарии? Начинающие программисты часто используют конструкцию return False или возвращают строку с сообщением об ошибке. Но это плохой подход, так как он заставляет вызывающий код (например, интерфейс CLI) постоянно проверять тип возвращаемого значения. Более того, ошибка 'проглатывается', что усложняет отладку.
Продвинутый подход (Intermediate/Senior) — это генерация исключений (Exception Handling). В Python мы можем и должны создавать собственные классы исключений, наследуя их от базового встроенного класса Exception. Создав классы BookNotAvailableError и UserNotFoundError, мы делаем наш код самодокументируемым. Когда возникает исключительная ситуация, мы используем ключевое слово raise, чтобы выбросить нашу кастомную ошибку. Выполнение текущего блока прерывается, и программа ищет ближайший блок try/except, который способен перехватить ошибку этого конкретного типа.
Такая архитектура жестко разделяет бизнес-логику (класс Library) и логику отображения интерфейса (CLI). Класс Library ничего не выводит в консоль (никаких print() внутри логики транзакций!). Он только принимает данные, меняет состояние объектов и raise исключения, если что-то идет не так. А уже интерфейс CLI будет оборачивать вызовы методов библиотеки в блоки try/except и красиво выводить сообщения пользователю. Это реализация принципа разделения ответственности (Single Responsibility Principle из SOLID), который является краеугольным камнем качественного программного обеспечения.
Кроме того, при обработке исключений мы можем использовать блоки else (выполняется, если исключение НЕ возникло) и finally (выполняется ВСЕГДА, независимо от того, была ошибка или нет). Блок finally критически важен при работе с внешними ресурсами (файлами, базами данных, сетевыми соединениями), чтобы гарантировать их закрытие и освобождение памяти, даже если программа падает с ошибкой. В нашем проекте мы покажем, как правильно выстроить всю цепочку обработки исключительных ситуаций.
# Определение пользовательских исключений
class LibraryException(Exception):
"""Базовый класс для всех исключений библиотеки."""
pass
class BookNotAvailableError(LibraryException):
"""Выбрасывается, когда книга уже выдана."""
pass
class ItemNotFoundError(LibraryException):
"""Выбрасывается, когда книга или пользователь не найдены."""
pass
# Добавление методов в класс Library (продолжение)
def borrow_book(self, user_id, isbn):
user = self.find_user_by_id(user_id)
if not user:
raise ItemNotFoundError(f"Пользователь с ID {user_id} не найден.")
# Поиск книги по ISBN
book = next((b for b in self.books if b.isbn == isbn), None)
if not book:
raise ItemNotFoundError(f"Книга с ISBN {isbn} отсутствует в фонде.")
if book._is_borrowed:
raise BookNotAvailableError(f"Книга '{book.title}' сейчас недоступна.")
# Транзакция: меняем состояние обоих объектов
book._is_borrowed = True
user.add_book(book)
custom_log("Транзакция: выдача", book.title, user.name)
Почему создание собственных (пользовательских) исключений, наследуемых от класса Exception, считается хорошей практикой?
Флеш-карточки
Какое ключевое слово используется для генерации (выброса) исключения в коде Python?
Нажмите, чтобы увидеть ответ
Ключевое слово `raise`.
Нажмите, чтобы вернуться
В каком случае выполняется блок `else` в конструкции try/except/else/finally?
Нажмите, чтобы увидеть ответ
Блок `else` выполняется только в том случае, если в блоке `try` НЕ возникло никаких исключений.
Нажмите, чтобы вернуться
Каково главное предназначение блока `finally`?
Нажмите, чтобы увидеть ответ
Блок `finally` выполняется всегда (как при успешном выполнении, так и при ошибке) и используется для гарантированного освобождения ресурсов (закрытие файлов, соединений).
Нажмите, чтобы вернуться
Постоянное хранение данных: Сериализация в JSON
Наше приложение умеет создавать объекты, связывать их логикой и обрабатывать ошибки. Но есть одна фундаментальная проблема: все данные хранятся в оперативной памяти (RAM). Как только мы закроем консоль или остановим скрипт, все созданные книги, пользователи и транзакции исчезнут навсегда. Нам нужен механизм персистентности (постоянного хранения). В реальном мире для этого используют базы данных (SQL/NoSQL). В нашем проекте мы используем промежуточный, но не менее важный навык — работу с текстовыми файлами формата JSON (JavaScript Object Notation).
JSON — это текстовый формат обмена данными, который легко читается людьми и машинами. Он структурно очень похож на комбинацию словарей и списков в Python. Встроенная библиотека json предоставляет методы dump/dumps (сериализация — превращение объектов Python в строку JSON) и load/loads (десериализация — чтение строки JSON и превращение ее обратно в словари и списки). Процесс сериализации требует преобразования наших сложных объектов классов (Book, User) в простые встроенные типы (словари, строки, числа), так как модуль JSON не знает, как сохранить в файл инстанс кастомного класса.
Для работы с файлами мы обязательно должны использовать Контекстные менеджеры (Context Managers) — конструкцию with open('filename.json', 'w') as file:. Почему это так важно? При открытии файла операционная система выделяет ресурсы (дескрипторы файлов). Если программа упадет или мы забудем вызвать file.close(), ресурс останется заблокированным (утечка памяти или блокировка файла для других программ). Контекстный менеджер with использует под капотом магические методы __enter__ и __exit__. Метод __exit__ гарантированно закрывает файл, даже если внутри блока произошел критический сбой (исключение). Это тот самый встроенный аналог блока finally, но более элегантный.
При сохранении состояния нашей библиотеки (класса Library), мы напишем метод save_to_file(). Он будет собирать данные из self.books и self.users, преобразовывать каждый объект в словарь (с помощью генератора списка, конечно же!), формировать единый большой словарь и записывать его в файл с помощью json.dump(). При загрузке (метод load_from_file()) мы будем читать JSON, получать словари и 'распаковывать' их обратно в объекты классов Book и User. Этот процесс называется ORM-маппингом на минималках.
import json
import os
# Добавляем функционал сохранения в класс Library
def save_data(self, filename="library_data.json"):
"""Сериализация объектов в JSON и сохранение в файл."""
data = {
"books": [
{"title": b.title, "author": b.author, "isbn": b.isbn, "_is_borrowed": b._is_borrowed}
for b in self.books
],
"users": [
{
"name": u.name,
"user_id": u.user_id,
"borrowed_isbns": [b.isbn for b in u.borrowed_books]
}
for u in self.users
]
}
# Контекстный менеджер для безопасной записи
try:
with open(filename, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
custom_log("Данные успешно сохранены", filename)
except IOError as e:
custom_log("Ошибка записи файла", str(e))
raise LibraryException("Не удалось сохранить базу данных.")
Какое главное преимущество использования контекстного менеджера (конструкции `with open(...) as file:`) при работе с файлами в Python?
Задание
Этап 3: Десериализация (Загрузка данных из JSON).
- Создать метод load_data(filename).
- Использовать блок try/except (FileNotFoundError) для проверки существования файла.
- Прочитать JSON с помощью json.load().
- Воссоздать объекты Book и User из полученных словарей.
- Восстановить связи (списки borrowed_books у пользователей на основе ISBN).
Архитектура интерфейса: Главный цикл приложения (CLI Loop)
Мы разработали надежное 'ядро' (backend) нашего приложения: модели данных, бизнес-логику транзакций, кастомные ошибки и систему хранения. Теперь пришло время связать все это с пользователем через интерфейс командной строки (CLI - Command Line Interface). В профессиональной разработке этот этап часто называют созданием слоя представления (View/Controller). Главное правило: слой логики (класс Library) и слой интерфейса должны быть максимально изолированы друг от друга.
Как работает классическое консольное приложение? В его основе лежит бесконечный цикл while True:. Внутри этого цикла программа выводит меню (список доступных опций), запрашивает ввод от пользователя с помощью функции input(), анализирует этот ввод через условные операторы if/elif/else (или современный match/case в Python 3.10+), вызывает соответствующий метод библиотеки и затем снова переходит к началу цикла. Цикл прерывается только тогда, когда пользователь выбирает опцию 'Выход', при этом вызывается команда break.
Именно в этом главном цикле мы будем активно применять наши пользовательские исключения (Custom Exceptions). Когда пользователь хочет взять книгу, он вводит свой ID и ISBN книги. Интерфейс передает эти данные в метод library.borrow_book(user_id, isbn), но оборачивает этот вызов в блок try/except. Если книга не найдена, метод выбросит ItemNotFoundError. Интерфейс перехватит его в блоке except ItemNotFoundError as e: и выведет красным цветом понятное сообщение: 'Ошибка: Книга с таким номером не найдена'. При этом приложение не 'упадет' (не завершится с красным трейсбеком), а продолжит работу, снова показав главное меню. Это называется Graceful Degradation — изящная обработка сбоев.
Также в главном цикле мы реализуем вызов методов загрузки и сохранения. При запуске скрипта (до старта цикла while) мы пытаемся загрузить данные из файла. Если файла еще нет (первый запуск), мы просто начинаем с пустой библиотекой. А при выборе опции 'Выход' (перед break) мы обязательно вызываем library.save_data(), чтобы все изменения за сессию были сохранены на жесткий диск. Таким образом, мы создаем полноценный жизненный цикл приложения: Инициализация -> Рабочий цикл (с обработкой ошибок) -> Завершение с сохранением состояния.
def main():
print("--- Добро пожаловать в систему Librarian Pro ---")
lib = Library("Центральная")
# Попытка загрузки данных при старте
try:
# lib.load_data("library_data.json") # Предполагаем реализацию метода
pass
except Exception as e:
print("Запуск с пустой базой данных.")
while True:
print("\nМЕНЮ:")
print("1. Показать доступные книги")
print("2. Выдать книгу")
print("3. Выход")
choice = input("Выберите действие (1-3): ")
if choice == '1':
books = lib.get_available_books()
print(f"\n--- Доступно книг: {len(books)} ---")
for b in books:
print(f"- {b}")
elif choice == '2':
try:
u_id = int(input("Введите ID пользователя: "))
isbn = input("Введите ISBN книги: ")
lib.borrow_book(u_id, isbn)
print("Успех! Транзакция завершена.")
except ValueError:
print("Ошибка: ID пользователя должен быть числом!")
except LibraryException as e: # Перехват наших кастомных бизнес-ошибок
print(f"Ошибка операции: {e}")
elif choice == '3':
print("Сохранение данных...")
lib.save_data()
print("До свидания!")
break
else:
print("Неверный ввод, попробуйте снова.")
if __name__ == "__main__":
main()
Почему вызов `lib.borrow_book()` в главном цикле (CLI) обернут в блок `try/except`?
Какая конструкция (idiom) в Python используется для проверки того, запущен ли скрипт напрямую, а не импортирован как модуль? Введите строку полностью (например, if x == 'y':).
Финальный обзор: Собираем все концепции вместе (Синтез знаний)
Наше приложение Librarian Pro готово. Давайте оглянемся назад и проанализируем, какой огромный пласт знаний уровня Intermediate мы применили в этом финальном проекте. Мы не просто написали код, мы спроектировали систему. Это и есть разница между кодером и инженером-разработчиком.
Во-первых, мы применили Объектно-Ориентированное Программирование (ООП). Мы создали классы Book и User, инкапсулировали их внутреннее состояние с помощью приватных атрибутов (одинарное подчеркивание) и предоставили безопасный доступ к ним через декораторы @property. Мы переопределили магические методы __str__ для красивого вывода интерфейса и __repr__ для отладки. Мы избежали классической ловушки новичков с Mutable Default Arguments, используя None в конструкторе списка книг пользователя.
Во-вторых, мы применили функциональный подход и продвинутые структуры данных. Вместо громоздких циклов for мы использовали элегантные списковые включения (List Comprehensions) и выражения-генераторы с функцией next() для высокопроизводительного поиска по фонду библиотеки. Мы спроектировали гибкий логгер custom_log, который способен принимать любое количество параметров благодаря распаковке позиционных (*args) и именованных (**kwargs) аргументов.
В-третьих, мы внедрили промышленную обработку исключений и работу с I/O (Input/Output). Мы спроектировали собственную иерархию ошибок, наследовав LibraryException от базового Exception, что позволило нам изолировать бизнес-логику от интерфейса отображения. При работе с файловой системой мы использовали контекстные менеджеры with open(), гарантируя, что наша JSON-база данных не повредится из-за незакрытых дескрипторов файлов при внезапном сбое или выходе из программы.
Этот проект является полноценной 'строчкой в резюме'. Вы можете расширить его: добавить наследование (например, класс AudioBook, наследуемый от Book), внедрить модуль datetime для отслеживания сроков возврата и начисления штрафов, или переписать сохранение с JSON на легковесную базу данных SQLite с использованием модуля sqlite3. Вы освоили фундамент современного Python. Практикуйтесь, читайте чужой код, изучайте исходники стандартной библиотеки, и ваш путь к уровню Senior будет открыт!
Флеш-карточки
В чем заключается принцип разделения ответственности (на примере нашего проекта)?
Нажмите, чтобы увидеть ответ
Класс Library занимается только бизнес-логикой и данными (меняет списки, генерирует исключения). Он не содержит функции print() или input(). Весь вывод и ввод вынесен в отдельный слой CLI (функция main).
Нажмите, чтобы вернуться
Что такое JSON и почему он удобен в Python?
Нажмите, чтобы увидеть ответ
JSON (JavaScript Object Notation) - текстовый формат данных. Он удобен тем, что его структура почти полностью совпадает со словарями (dict) и списками (list) в Python, и легко конвертируется через встроенный модуль json.
Нажмите, чтобы вернуться
Какая разница между list comprehension (генератор списка) и выражением-генератором (generator expression)?
Нажмите, чтобы увидеть ответ
List comprehension `[x for x in data]` сразу создает весь список в памяти. Выражение-генератор `(x for x in data)` вычисляет элементы по одному (лениво), что экономит память при работе с большими данными.
Нажмите, чтобы вернуться