Hopp til innhold

Walidacja danych i klasy

Skip to content

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

W Pythonie (a także w większości innych języków programowania) 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 grupowania powiązanych danych i funkcji w jednej jednostce, np. klasy Zamowienie, która zawiera dane takie jak zamowienie_id, imie_klienta, produkty oraz funkcje takie jak dodaj_produkty(), oblicz_sume() itd.

Słownik (JSON) vs klasy (Obiekty)

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

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

Kiedy pracujemy z danymi strukturalnymi w kodzie, korzystamy z klas.

W tym module zajmiemy się sposobami wykorzystywania klas do walidacji danych.

Wideo: Jak klasy zbierają dane i reguły

W wideo wyjaśniono, czym jest klasa oraz jak klasy mogą być wykorzystywane do strukturyzacji i walidacji danych.

Najłatwiejszym rozwiązaniem jest użycie @dataclass z biblioteki dataclasses. Pozwala to uniknąć pisania dużej ilości kodu szablonowego przy tworzeniu klasy. (Na przykład wbudowanych funkcji __init__ i __repr__ (reprezentacja)).

Przykład klasy bez używania 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)  # Wynik: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Przykład z użyciem dekoratora dataclass, który osiąga ten sam efekt co powyżej (ale przy mniejszej liczbie kodu):

from dataclasses import dataclass

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

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

Łatwy Zadanie 1 – Utwórz klasę

Utwórz klasę o nazwie Person. Ta klasa ma mieć następujące atrybuty:

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

Zainstancjuj (utwórz) obiekt klasy Person, nadając mu ważne wartości dla wszystkich atrybutów, zgodnie z przykładem 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: Dataclass dla Osoby (Person)

Oto jedno z możliwych rozwiązań:

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 szybko prowadzić do długów technicznych w przyszłości. Na szczęście istnieją proste sposoby dodawania walidacji do klas.

Zacznijmy od przyjrzenia się walidacji adresu e-mail. W Pythonie dostępne są wbudowane biblioteki, które mogą nam to ułatwić, ale ponieważ robimy to dla nauki, stworzymy własną prostą walidację, tworząc nową klasę „Email” i badając funkcję __post_init__ (tylko dla klasy danych / dataclassa).

Uwaga

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

Przykład funkcji _postinit
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Twój kod tutaj

W przypadku prostej walidacji adresu e-mail możemy np. sprawdzić, czy adres zawiera zarówno znak @, jak i kropkę .. Ewentualnie możesz sprawdzić, czy adres pasuje do wzorca regularnego wyrażenia regularnego (regex). (Bardziej zaawansowane podejście, ale zachęcamy do przeszukania sieci internetowej!)

Rozwiązanie: Kod do prostego walidowania adresu e-mail

Oto możliwe rozwiązanie, używamy wyjątków (exceptions), aby „wywołać błąd”, jeśli adres e-mail jest nieprawidłowy; spowoduje to natychmiastowe zatrzymanie programu z komunikatem o błędzie.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Brak @ w adresie e-mail: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Brak . 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) # Niepoprawny, brak @

Medium Zadanie 3 – Walidacja numeru telefonu

Utwórz klasę w podobny sposób jak dla adresów e-mail, ale tym razem dla numerów telefonicznych.

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)

Gdy używamy obiektów do reprezentowania wartości takich jak adres e-mail lub numer telefonu, musimy za każdym razem, gdy chcemy pobrać wartość, podać wartość podrzędną (np. address dla adresu e-mail oraz number dla numeru telefonu). Może to być nieco uciążliwe przy dłuższym kodzie. Na szczęście istnieje na to rozwiązanie: korzystając z dekoratora @property w klasie, możemy pobierać wartość bezpośrednio z obiektu bez konieczności podawania wartości podrzędnej.

Stwarza to jednak dodatkowe wyzwanie, mianowicie potrzebujemy funkcji __init__ w klasie Person. Wynika to z faktu, że nie można używać tej samej nazwy zarówno dla właściwości (property), jak i atrybutu w klasie danych (data class).

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 wartość podrzędną bezpośrednio

# Test kodu
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Wyjście: Noe tekst

Uwaga

Właściwości są wyjątkowe, ponieważ nie wymagają parametrów ani nawiasów, aby je uruchomić. W przykładzie pobieramy wartość person.wartosc bez nawiasów (nie person.wartosc()), mimo że technicznie jest to funkcja.

Alternatywny przykład akceptujący zarówno str, jak i EksempelValuta
@dataclass
class Osoba:
    imie: str
    _wartosc: str

    def __init__(self, imie: str, wartosc: str | EksempelValuta):
        self.imie = imie
        if isinstance(wartosc, EksempelValuta):
            self._wartosc = wartosc
        elif isinstance(wartosc, str):
            self._wartosc = EksempelValuta(wartosc)
        else:
            raise TypeError("wartosc musi być typu str lub EksempelValuta")

    @property
    def wartosc(self) -> str:
        return self._wartosc.atrybut  # Pobiera wartość podrzędną bezpośrednio

# Test kodu
osoba = Osoba(imie="Alice", wartosc="Jakiś tekst")
print(osoba.wartosc)  # Wyjście: Jakiś tekst

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

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

Następnie należy utworzyć następujące właściwości:

  • Utwórz właściwość age, która oblicza wiek osoby na podstawie birthday i bieżącej daty.
  • Utwórz właściwość is_adult, która zwraca wartość 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:
        """Wylicza wiek na podstawie daty urodzenia i bieżącej daty"""
        today = date.today()
        age = today.year - self.birthday.year
        # Odejmij 1, jeśli osoba nie obchodziła jeszcze w tym roku dnia urodzin
        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

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

Jeszcze jedno wyzwanie!

Czy potrafisz sprawić, aby instancjonowanie Person akceptowało zarówno datetime.date, jak i tekst w formacie "DD-MM-RRRR" dla parametru birthday? (Podpowiedź: użyj datetime.strptime, aby przekonwertować łańcuch znaków na datetime.date)