Сохранение и загрузка из JSON

Сохранение и загрузка из JSON

Данные в памяти исчезают при закрытии программы. Чтобы телефонная книга была полезной, контакты нужно сохранять на диск. В этом уроке реализуем методы save() и _load() в классе AddressBook.

Проблема: объекты нельзя напрямую сериализовать

json.dump умеет работать со словарями, списками, строками и числами. Но наши объекты Contact — это пользовательские классы. Попытка сохранить список Contact напрямую даст TypeError:

import json
from contact import Contact

contacts = [Contact("Алиса", "+7-999-111")]
# json.dumps(contacts)  # TypeError: Object of type Contact is not JSON serializable

Решение: конвертировать объект в словарь (сериализация) и обратно (десериализация).

Сериализация Contact в словарь

Добавим метод to_dict() и фабричный метод from_dict() в класс Contact:

# contact.py — дополнение к классу Contact

class Contact:
    def __init__(self, name, phone, email=None):
        # ... как раньше

    def to_dict(self) -> dict:
        """Конвертировать контакт в словарь для JSON."""
        return {
            "name": self.name,
            "phone": self.phone,
            "email": self.email,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Contact":
        """Создать Contact из словаря (загруженного из JSON)."""
        return cls(
            name=data["name"],
            phone=data["phone"],
            email=data.get("email"),
        )

@classmethod — декоратор, который делает метод привязанным к классу, а не к экземпляру. Первый аргумент cls — сам класс (аналог self для обычных методов). from_dict — фабричный метод: создаёт объект из данных другого формата.

Реализация save() и _load()

# address_book.py

import json
from pathlib import Path
from contact import Contact, ContactNotFoundError

class AddressBook:
    def __init__(self, filepath):
        self.filepath = Path(filepath)
        self._contacts = []
        self._load()

    def save(self) -> None:
        """Сохранить все контакты в JSON-файл."""
        data = [c.to_dict() for c in self._contacts]
        self.filepath.write_text(
            json.dumps(data, ensure_ascii=False, indent=2),
            encoding="utf-8"
        )

    def _load(self) -> None:
        """Загрузить контакты из JSON-файла (при старте)."""
        if not self.filepath.exists():
            return   # файла нет — начинаем с пустой книги
        try:
            text = self.filepath.read_text(encoding="utf-8")
            data = json.loads(text)
            self._contacts = [Contact.from_dict(item) for item in data]
        except (json.JSONDecodeError, KeyError, ValueError):
            print(f"Предупреждение: не удалось загрузить {self.filepath}, начинаем заново")
            self._contacts = []

Автосохранение после изменений

Сохранять вручную — риск забыть. Лучший паттерн — сохранять сразу после каждого изменения:

class AddressBook:
    def add(self, contact):
        for i, existing in enumerate(self._contacts):
            if existing.name.lower() == contact.name.lower():
                self._contacts[i] = contact
                self.save()   # автосохранение
                return
        self._contacts.append(contact)
        self.save()   # автосохранение

    def delete(self, name):
        contact = self.find(name)
        self._contacts.remove(contact)
        self.save()   # автосохранение

Альтернативный подход — явный вызов book.save() после всех операций. Для нашего проекта автосохранение удобнее: пользователь не потеряет данные при неожиданном завершении программы.

Проверка: данные сохраняются между запусками

# Первый запуск:
from address_book import AddressBook
from contact import Contact

book = AddressBook("contacts.json")
book.add(Contact("Алиса", "+7-999-111-22-33"))
print("Сохранено")

# Второй запуск:
book2 = AddressBook("contacts.json")
for c in book2.all():
    print(c)
# Алиса: +7-999-111-22-33

После второго запуска данные загрузились из contacts.json. Файл выглядит так:

[
  {
    "name": "Алиса",
    "phone": "+7-999-111-22-33",
    "email": null
  }
]

Итог

  • to_dict() — объект → словарь для сериализации.
  • @classmethod from_dict(cls, data) — словарь → объект; фабричный метод.
  • save() использует list comprehension: [c.to_dict() for c in self._contacts].
  • _load() обрабатывает отсутствие файла и повреждённые данные без падения.
  • Автосохранение в add() и delete() защищает от потери данных.

В следующем уроке реализуем CLI-интерфейс — меню в цикле для взаимодействия с пользователем.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике