Weryfikacja danych i klasy

Skip to content

To jest tekst przetłumaczony maszynowo, który może zawierać błędy!

W Pythonie (dotyczy to również większości innych języków) możliwe jest tworzenie własnych obiektów, z własnymi wartościami, regułami i funkcjami. Nazywa się to klasami (classes po angielsku). Używamy klas do zgrupowania powiązanych danych i funkcji w jedną całość, np. klasa Zamówienie posiadająca dane takie jak id_zamówienia, imię_klienta, produkty i funkcje takie jak dodaj_produkt(), oblicz_sumę(), itp.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) to format służący do przechowywania i przesyłania danych niezależnie od języka programowania, podczas gdy klasa to struktura w konkretnym języku programowania.

Gdy chcemy wysłać dane przez sieć lub zapisać je w pliku, często używamy JSON (lub tabel w bazach danych).

Gdy chcemy pracować ze strukturalnymi danymi w kodzie, używamy klas.

W tej moduł będziemy badać, jak możemy używać klas do walidacji danych.

Najłatwiej jest użyć @dataclass z biblioteki dataclasses. Pozwala to uniknąć pisania dużej ilości kodu szablonowego do tworzenia klasy. (Takich jak wbudowane funkcje __init__ i __repr__ (reprezentacja)).

Przykład klasy bez użycia dekoratora 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)  # Wyjście: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Przykład z dekoratorem dataclass, który osiąga to samo, co powyżej (ale z mniejszą ilością kodu):

from dataclasses import dataclass

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

my_car = Car("Toyota", "Corolla", 2020)
print(my_car)  # Wyjście: Car(make='Toyota', model='Corolla', year=2020)

Easy Zadanie 1 - Stwórz klasę

Stwórz klasę o nazwie Person. Ta klasa powinna mieć następujące właściwości (atrybuty):

  • name: Imię osoby
  • eye_color: Kolor oczu osoby
  • phone_number: Numer telefonu osoby
  • email: Adres e-mail osoby

Utwórz instancję (użyj) obiektu klasy Person z poprawnymi wartościami dla wszystkich właściwości, jak w przykładzie poniżej.

@dataclass
class Person:
    ... # Twój kod tutaj

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number="12345678",
                   email="bob_kaare@example.com")
print(bob_kaare)

Rozwiązanie: Klasa danych dla Osoby

Oto możliwe rozwiązanie:

from dataclasses import dataclass

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

Medium Zadanie 2 - Walidacja w klasie

W przykładzie powyżej nie dodaliśmy żadnej walidacji. Oznacza to, że możemy utworzyć Person z nieprawidłowymi wartościami, takimi jak:

@dataclass
class Person:
    ... # Twój kod tutaj

invalid_person = Person(name="",
                        eye_color="yes",
                        phone_number="12345",
                        email="not-an-email")
print(invalid_person)
# Output: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')

To jest (potencjalnie) problematyczne i może łatwo prowadzić do długu technologicznego w przyszłości. Na szczęście istnieją proste sposoby na dodanie walidacji do klas.

Zaczniemy od spojrzenia na walidację adresu e-mail. Istnieją wbudowane biblioteki w Pythonie, które mogą nam w tym pomóc, ale ponieważ robimy to po to, by się uczyć, stworzymy własną prostą walidację, tworząc nową klasę „Email” i badając funkcję __post_init__ (tylko dla dataclass).

Merk

Możemy również zrobić to w samej klasie Person, ale często lepiej jest tworzyć oddzielne klasy dla rzeczy, które można ponownie wykorzystać.

Przykład funkcji post_init
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Din kode her

Dla prostego walidowania adresu e-mail, możemy na przykład sprawdzić, czy adres zawiera zarówno znak @, jak i .. Opcjonalnie możesz sprawdzić, czy adres pasuje do wzorca wyrażenia regularnego (regex). (Bardziej zaawansowane, ale poszukaj w Internecie!)

Rozwiązanie: Kod do prostej walidacji e-mail

Oto możliwe rozwiązanie, używamy wyjątków, aby “zepsuć” działanie, jeśli e-mail jest nieprawidłowy, co natychmiast zatrzyma program i wyświetli komunikat o błędzie.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Brakuje @ w adresie e-mail: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Brakuje . w domenie adresu e-mail: {self.address}")
        if " " in self.address:
            raise ValueError(f"Adres e-mail nie może zawierać spacji: {self.address}")

# Test kodu 
test = Email("hei@example.com")  # Poprawny
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Nieprawidłowy, brakuje @

Medium Zadanie 3 - Walidacja numerów telefonów

Stwórz klasę analogiczną do tej, którą stworzyłeś dla adresów e-mail, ale teraz dla numerów telefonów.

Wyzwanie z walidacją numeru telefonu!

Czy możesz naprawić walidację numerów telefonów, aby akceptowała zarówno litery (str), jak i cyfry (int)? Na przykład 12345678 i "12345678" powinny być oba poprawne.

Spróbuj również dodać kody krajowe jako atrybut (podwartość klasy). Na przykład 47 dla Norwegii, 46 dla Szwecji

Medium Zadanie 4 - Użyj walidacji w klasie Osoba

Teraz, gdy stworzyliśmy walidację dla adresu e-mail i numeru telefonu, możemy użyć ich w klasie Osoba.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Użyj klasy PhoneNumber
    email: Email               # Użyj klasy Email

Pojawia się nowe wyzwanie!

Teraz, gdy zmieniliśmy klasę Person tak, aby używała klas PhoneNumber i Email, musimy również zmienić sposób tworzenia instancji (tworzenia) Person. Musimy teraz najpierw utworzyć obiekt PhoneNumber i Email, zanim będziemy mogli utworzyć Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Zauważ zmianę tutaj
                   email=Email("bob_kaare@example.com"))  # Zauważ zmianę tutaj
print(bob_kaare)

# Uwaga: konieczna jest zmiana w sposobie pobierania wartości
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Zadanie 5 - Właściwości w klasach (Opcjonalne)

Kiedy używamy obiektów do reprezentowania wartości takich jak adres e-mail i numer telefonu, musimy określić podwartość (np. address dla adresu e-mail i number dla numeru telefonu) za każdym razem, gdy chcemy pobrać wartość. Może to być nieco uciążliwe w dłuższej perspektywie. Na szczęście istnieje rozwiązanie, polegające na użyciu dekoratora @property w klasie, który pozwala nam pobierać wartość bezpośrednio z obiektu, bez konieczności określania podwartości.

To jednak stwarza kolejną przeszkodę, a mianowicie potrzebę funkcji __init__ w klasie Person. Dzieje się tak, ponieważ nie możemy użyć tej samej nazwy zarówno dla właściwości, jak i atrybutu w dataclass.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Zmienna wewnętrzna (zaczyna się od _ aby wskazać, że jest "prywatna")

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Pobiera podwartość bezpośrednio

# Testuj kod
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Output: Noe tekst

Merk

Właściwości są unikalne, ponieważ nie wymagają parametrów i nie potrzebują nawiasów, aby się uruchomić. W przykładzie pobieramy person.verdi bez nawiasów (nie person.verdi()), mimo że technicznie rzecz biorąc jest to funkcja.

Alternatywny przykład, który akceptuje zarówno str, jak i 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 musi być typu str lub EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Pobiera podwartość bezpośrednio

# Test kodu
person = Person(name="Alice", verdi="Noe tekst")
print(person.verdi)  # Output: Noe tekst

Hard Zadanie 6 - Właściwości z logiką (Opcjonalne)

Zaktualizuj Person dodając nowy atrybut o nazwie birthday (data urodzenia). Powinien on być typu datetime.date (z biblioteki datetime).

Następnie utwórz następujące właściwości:

  • Utwórz właściwość age (wiek), która oblicza wiek osoby na podstawie birthday i aktualnej daty.
  • Utwórz właściwość is_adult (czy_dorosły), która zwraca True jeśli osoba ma 18 lat lub więcej, w przeciwnym razie False.

Rozwiązanie: Wiek i dorosłość jako właściwości

Oto możliwe rozwiązanie:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Oblicza wiek na podstawie daty urodzenia i dzisiejszej daty"""
        today = date.today()
        age = today.year - self.birthday.year
        # Zmniejsz o 1, jeśli osoba jeszcze nie obchodziła urodzin w tym roku
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Zwraca True, jeśli osoba ma 18 lat lub więcej"""
        return self.age >= 18

# Przetestuj kod
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # Np. 18, jeśli dzisiejsza data to po 15 maja 2023
print(person.is_adult)  # True

Kolejne wyzwanie!

Czy potrafisz sprawić, aby instancjonowanie Person akceptowało zarówno datetime.date, jak i tekst w formacie "DD-MM-YYYY" dla birthday? (Wskazówka: użyj datetime.strptime aby przekonwertować ciąg na datetime.date)