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
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)
Zadanie 1 – Utwórz klasę
Utwórz klasę o nazwie Person. Ta klasa ma mieć następujące atrybuty:
name: Imię osobyeye_color: Kolor oczu osobyphone_number: Numer telefonu osobyemail: 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
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 @
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
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(?)
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
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 podstawiebirthdayi 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 razieFalse.
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)
