Перевірка даних та класи

Skip to content

Це машинний переклад, який може містити помилки!

У Python (це стосується також більшості інших мов) є можливість створювати власні об’єкти, з власними значеннями, правилами та функціями. Це називається класами (classes англійською). Ми використовуємо класи для об’єднання пов’язаних даних та функцій в одну одиницю, наприклад, клас Замовлення (Ordre), який має дані, такі як замовлення_id (ordre_id), ім'я_клієнта (kunde_navn), продукти (produkter) та функції, такі як додати_продукт() (legg_til_produkt()), обчислити_загальну_суму() (beregn_total()), тощо.

Dictionary (JSON) vs Klasser (Objekter)

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

British python devs be like thats a constructor, __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)

Løsning: En dataclass for 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).

Merk

Ми можемо також зробити це в самому Person класі, але часто краще створювати окремі класи для речей, які можна повторно використовувати.

Приклад функції post_init
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Ваш код тут

Для простої валідації електронної пошти, наприклад, ми можемо перевірити, чи містить електронна пошта як @, так і . символи. Можливо, ви також можете перевірити, чи відповідає електронна пошта шаблону регулярного виразу. (Більш просунуто, але пошукайте в інтернеті!)

Løsning: Kode for enkel e-post validering

Ось можливе рішення, ми використовуємо винятки, щоб “зруйнувати” програму, якщо електронна пошта недійсна, це зупинить програму відразу і дасть повідомлення про помилку.

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 - Валідація телефонних номерів

Створіть аналогічний клас, як ви робили для електронних адрес, але тепер для телефонних номерів.

Виклик з валідацією телефону!

Чи можете ви виправити валідацію для номерів телефонів, щоб вона приймала як літери (str), так і цифри (int)? Наприклад, 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. Це тому, що ми не можемо використовувати одне й те саме ім’я як для властивості, так і для атрибута в dataclass.

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

Merk

Властивості унікальні тим, що їм не потрібні параметри, і їм не потрібні дужки для виконання. У прикладі ми отримуємо person.verdi без дужок (не person.verdi()), навіть якщо технічно це функція.

Альтернативний приклад, який приймає як str, так і EksempelVerdi
@dataclass
class Person:
    name: str
    _verdi: str

    def __init__(self, name: str, verdi: str | EksempelVerdi):
        self.name = name
        if isinstance(verdi, EksempelVerdi):
            self._verdi = verdi
        elif isinstance(verdi, str):
            self._verdi = EksempelVerdi(verdi)
        else:
            raise TypeError("verdi має бути типу str або EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Отримує підпорядковане значення безпосередньо

# Перевірте код
person = Person(name="Alice", verdi="Noe tekst")
print(person.verdi)  # Вивід: Noe tekst

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, так і текст у форматі "DD-MM-YYYY" для birthday? (Підказка: використовуйте datetime.strptime для перетворення рядка на datetime.date)