Валидация данных и классы

Skip to content

Это машинный перевод текста, который может содержать ошибки!

В Python (а также в большинстве других языков) возможно создавать собственные объекты со своими значениями, правилами и функциями. Это называется классами (на английском — classes). Мы используем классы для объединения связанных данных и функций в единую единицу, например, класс Ordre, который содержит данные вроде ordre_id, kunde_navn, produkter и функции вроде legg_til_produkt(), beregn_total() и т. д.

Словарь (JSON) и классы (объекты)

JSON (JavaScript Object Notation) — это формат для хранения и передачи данных независимо от языка программирования, тогда как класс представляет собой структуру в конкретном языке программирования.

При передаче данных по сети или их сохранении в файл мы часто используем JSON (или таблицы в базах данных).

При работе со структурированными данными в коде мы используем классы.

В этом модуле мы рассмотрим, как можно использовать классы для валидации данных.

Видео: Как классы собирают данные и правила

Видео объясняет, что такое класс, и как классы можно использовать для структурирования и валидации данных.

Проще всего использовать @dataclass из библиотеки dataclasses. Это позволяет избежать написания большого количества шаблонного кода при создании класса. (Например, встроенных функций __init__ и __repr__ (представление)).

Пример класса без использования декоратора dataclass:

class Car:
    def __init__(self, make: str, model: str, year: int):
        self.make = make
        self.model = model
        self.year = year

    def __repr__(self):
        return f"{self.year} {self.make} {self.model}"

my_car = Car("Toyota", "Corolla", 2020)
print(my_car)  # Вывод: 2020 Toyota Corolla

Британские разработчики Python говорят «это конструктор, __init__?»

Пример с декоратором dataclass, который достигает того же результата, что и выше (но требует меньше кода):

from dataclasses import dataclass

@dataclass
class Car:
    make: str
    model: str
    year: int

my_car = Car("Toyota", "Corolla", 2020)
print(my_car)  # Вывод: Car(make='Toyota', model='Corolla', year=2020)

Легко Задание 1 — Создайте класс

Создайте класс под названием Person. Этот класс должен иметь следующие атрибуты:

  • name: Имя человека
  • eye_color: Цвет глаз человека
  • phone_number: Номер телефона человека
  • email: Адрес электронной почты человека

Создайте экземпляр объекта класса Person, задав для всех атрибутов допустимые значения, как показано в примере ниже.

@dataclass
class Person:
    ... # Ваш код здесь

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number="12345678",
                   email="bob_kaare@example.com")
print(bob_kaare)
Решение: Dataclass для Person

Вот возможное решение:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: str
    email: str

Medium Задача 2 — Валидация в классе

В примере выше мы не добавили никакой валидации. Это означает, что мы можем создать объект Person с некорректными значениями, например:

@dataclass
class Person:
    ... # Ваш код здесь

invalid_person = Person(name="",
                        eye_color="yes",
                        phone_number="12345",
                        email="not-an-email")
print(invalid_person)
# Вывод: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')

Это потенциально проблематично и может быстро привести к техническому долгу в будущем. К счастью, существуют простые способы добавить валидацию в классы.

Мы начнём с рассмотрения валидации электронной почты. В Python есть встроенные библиотеки, которые могут помочь нам в этом, но поскольку мы делаем это для обучения, мы создадим собственную простую валидацию, определив новый класс «Email» и исследовав функцию __post_init__ (только для dataclass).

Примечание

Мы можем сделать это и в самом классе Person, но часто лучше создавать отдельные классы для того, что можно переиспользовать.

Пример функции _postinit
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Валидация email: {self.address}")
        # Ваш код здесь

Для простой валидации электронной почты мы можем, например, проверить наличие как символа @, так и точки (.). Также можно проверить соответствие адреса регулярному выражению (regex). (Более продвинутый вариант, но не стесняйтесь искать информацию в интернете!)

Решение: Код для простой валидации электронной почты

Вот возможное решение, мы используем исключения (exceptions), чтобы «сломать» программу, если адрес электронной почты недействителен. Это немедленно остановит работу программы и выдаст сообщение об ошибке.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Отсутствует @ в адресе электронной почты: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Отсутствует . в домене адреса электронной почты: {self.address}")
        if " " in self.address:
            raise ValueError(f"Адрес электронной почты не может содержать пробелы: {self.address}")

# Тестируем код 
test = Email("hei@example.com")  # Действительный
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Недействительный, отсутствует @

Medium Задание 3 — Валидация номера телефона

Создайте класс аналогично тому, как вы делали это для адресов электронной почты, но теперь для номеров телефонов

Проблема с валидацией телефона!

Можешь исправить валидацию номера телефона так, чтобы она принимала как буквы (строки), так и цифры (целые числа)? Например, оба варианта — 12345678 и "12345678" — должны считаться допустимыми.

Также попробуй добавить коды стран в качестве атрибута (подзначения для класса). Например, 47 для Норвегии, 46 для Швеции

Medium Задача 4 — Использование валидации в классе Person

Теперь, когда мы создали валидацию для электронной почты и номера телефона, мы можем использовать их в классе Person.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Используйте класс PhoneNumber
    email: Email               # Используйте класс Email

Возникает новый вызов!

Теперь как мы изменили класс Person, чтобы использовать классы PhoneNumber и Email, нам также нужно изменить способ создания экземпляра класса Person. Нам теперь нужно сначала создать объекты PhoneNumber и Email, прежде чем создавать объект Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Обратите внимание на изменение здесь
                   email=Email("bob_kaare@example.com"))  # Обратите внимание на изменение здесь
print(bob_kaare)

# Примечание: необходимо внести изменения и в способ получения значений
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Задача 5 — Свойства в классах (Необязательно)

Когда мы используем объекты для представления значений, таких как адрес электронной почты и номер телефона, нам необходимо каждый раз указывать подзначение (например, address для адреса электронной почты и number для номера телефона), чтобы извлечь значение. Со временем это может стать несколько неудобным. К счастью, существует решение этой проблемы: использование декоратора @property в классе позволяет извлекать значение непосредственно из объекта без необходимости указания подзначения.

Однако это порождает еще одну проблему: функции __init__ требуются в классе Person. Это связано с тем, что одно и то же имя нельзя использовать одновременно для свойства и атрибута в датаклассе.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Внутренняя переменная (начинается с _, чтобы указать, что она «приватная»)

    def __init__(self, name: str, verdi: EksempelVerdi):
        self.name = name
        self._verdi = verdi

    @property
    def verdi(self):
        return self._verdi.attributt  # Извлекает подзначение напрямую

# Тест кода
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Вывод: Noe tekst

Примечание

Свойства уникальны тем, что им не нужны параметры и их вызов не требует скобок. В примере мы получаем значение person.value без скобок (а не person.value()) , хотя технически это функция.

Альтернативный пример, принимающий как str, так и ПримерЗначения
@dataclass
class Person:
    name: str
    _значение: str

    def __init__(self, name: str, значение: str | ПримерЗначения):
        self.name = name
        if isinstance(значение, ПримерЗначения):
            self._значение = значение
        elif isinstance(значение, str):
            self._значение = ПримерЗначения(значение)
        else:
            raise TypeError("значение должно быть типа str или ПримерЗначения")

    @property
    def значение(self) -> str:
        return self._значение.атрибут  # Получает дочернее значение напрямую

# Тестируем код
человек = Person(name="Алиса", значение="Некоторый текст")
print(человек.значение)  # Вывод: Некоторый текст

Hard Задание 6 — Свойства с логикой (Необязательное)

Обновите класс Person, добавив новый атрибут под названием birthday (день рождения). Его тип должен быть datetime.date (из библиотеки datetime).

Затем создайте следующие свойства:

  • Создайте свойство age, которое вычисляет возраст человека на основе даты birthday и текущей даты.
  • Создайте свойство is_adult, которое возвращает True, если человеку исполнилось 18 лет или больше, иначе возвращает False.
Решение: Возраст и совершеннолетие в виде свойств

Вот возможное решение:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Вычисляет возраст на основе даты рождения и текущей даты"""
        today = date.today()
        age = today.year - self.birthday.year
        # Уменьшить на 1, если человек еще не отметил день рождения в этом году
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Возвращает True, если человеку исполнилось 18 лет или больше"""
        return self.age >= 18

# Тестируем код
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # Например, 18, если текущая дата позже 15 мая 2023 года
print(person.is_adult)  # True

Ещё одна задача!

Можете ли вы заставить инстанцирование Person принимать как datetime.date, так и текст в формате "ДД-ММ-ГГГГ" для параметра birthday? (Подсказка: используйте datetime.strptime для преобразования строки в объект datetime.date)