Hopp til innhold

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

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)

Easy Задание 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}")
        # Ваш код здесь

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

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

Вот возможное решение, мы используем исключения (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, так и EksempelVerdi
@dataclass
class Person:
    name: str
    _value: str

    def __init__(self, name: str, value: str | EksempelVerdi):
        self.name = name
        if isinstance(value, EksempelVerdi):
            self._value = value
        elif isinstance(value, str):
            self._value = EksempelVerdi(value)
        else:
            raise TypeError("value должен быть типа str или EksempelVerdi")

    @property
    def value(self) -> str:
        return self._value.attribute  # Извлекает подзначение напрямую

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

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)