Datavalidointi ja luokat

Skip to content

Tämä on konekäännetty teksti, joka saattaa sisältää virheitä!

Pythonissa (koskee myös useimpia muita kieliä) on mahdollista luoda omia objekteja, omilla arvoilla, säännöillä ja funktioilla. Tätä kutsutaan luokiksi (classes englanniksi). Käytämme luokkia kerätäksemme liittyvää dataa ja funktioita yhteen yksikköön, esim. Ordre-luokka, jolla on dataa kuten ordre_id, kunde_navn, produkter ja funktioita kuten legg_til_produkt(), beregn_total(), jne.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) on muoto datan tallentamiseen ja siirtämiseen riippumatta ohjelmointikielestä, kun taas luokka on rakenne tietyssä ohjelmointikielessä.

Kun haluamme lähettää dataa verkossa tai tallentaa sen tiedostoon, käytämme usein JSON:ia (tai tauluja tietokannoissa).

Kun haluamme työskennellä jäsennellyn datan kanssa koodissa, käytämme luokkia.

Tässä moduulissa tarkastelemme, miten luokkia voidaan käyttää datan validoimiseen.

Helpoin tapa on käyttää @dataclass-dekoraattoria dataclasses-kirjastosta. Tämä säästää meiltä paljon boilerplate-koodin kirjoittamista luokan luomiseksi. (Kuten esimerkiksi sisäänrakennetut __init__- ja __repr__- (esitys) -funktiot).

Esimerkki luokasta ilman dataclass-dekoraattorin käyttöä:

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)  # Tulostaa: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Esimerkki dataclass-dekoraattorista, joka saavuttaa saman kuin yllä (mutta vähemmällä koodilla):

from dataclasses import dataclass

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

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

Easy Tehtävä 1 - Luo luokka

Luo luokka nimeltä Person. Tämän luokan tulee sisältää seuraavat ominaisuudet (attributes):

  • name: Henkilön nimi
  • eye_color: Henkilön silmien väri
  • phone_number: Henkilön puhelinnumero
  • email: Henkilön sähköpostiosoite

Luo (instansioi) luokan Person olio, jossa on kaikille ominaisuuksille pätevät arvot, kuten alla olevassa esimerkissä.

@dataclass
class Person:
    ... # Koodisi täällä

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

Tässä on mahdollinen ratkaisu:

from dataclasses import dataclass

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

Medium Tehtävä 2 – Validointi luokassa

Yllä olevassa esimerkissä emme ole lisänneet mitään validointia. Se tarkoittaa, että voimme luoda Person-olion virheellisillä arvoilla, kuten:

@dataclass
class Person:
    ... # Koodisi täällä

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')

Tämä on (mahdollisesti) ongelmallista ja voi helposti aiheuttaa teknistä velkaa tulevaisuudessa. Onneksi on olemassa yksinkertaisia tapoja lisätä validointia luokkiin.

Aloitamme tarkastelemalla sähköpostin validointia. Pythonissa on sisäänrakennettuja kirjastoja, jotka voivat auttaa meitä tässä, mutta koska teemme tätä oppiaksemme, luomme oman yksinkertaisen validoinnin luomalla uuden luokan nimeltä “Email” ja tutkimalla __post_init__-funktiota (vain dataclassille).

Merk

Voimme myös tehdä tämän itse Person -luokassa, mutta on usein parempi luoda omia luokkia asioille, joita voidaan käyttää uudelleen.

Esimerkki post_init -funktiosta
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Koodisi tähän

Yksinkertaisen sähköpostin validoinnin osalta voimme esimerkiksi tarkistaa, että sähköpostiosoite sisältää sekä @- että .-merkit. Halutessasi voit myös tarkistaa, että sähköpostiosoite vastaa regex-mallia. (Edistyneempää, mutta etsi tietoa verkosta!)

Løsning: Kode for enkel e-post validering

Tässä on mahdollinen ratkaisu, käytämme poikkeuksia “kaataaksemme” ohjelman, jos sähköpostiosoite on virheellinen, tämä pysäyttää ohjelman heti ja antaa virheilmoituksen.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Sähköpostiosoitteesta puuttuu @: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Sähköpostiosoitteen verkkotunnuksesta puuttuu .: {self.address}")
        if " " in self.address:
            raise ValueError(f"Sähköpostiosoite ei voi sisältää välilyöntejä: {self.address}")

# Testaa koodi 
test = Email("hei@example.com")  # Voimassa oleva
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Virheellinen, puuttuu @

Medium Tehtävä 3 – Puhelinnumeron validointi

Luo vastaava luokka kuin sähköpostiosoitteille, mutta nyt puhelinnumeroille.

Haaste puhelinnumeron validoinnissa!

Voitko korjata puhelinnumeron validoinnin siten, että se hyväksyy sekä kirjaimet (str) että numerot (int)? Esim. 12345678 ja "12345678" molempien tulisi olla kelvollisia.

Yritä myös lisätä maatunnukset attribuutiksi (luokan aliarvoksi). Esim. 47 Norjalle, 46 Ruotsille

Medium Tehtävä 4 – Käytä validointia Person-luokassa

Nyt kun olemme luoneet validoinnin sähköpostille ja puhelinnumerolle, voimme käyttää niitä Person-luokassa.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Käytä PhoneNumber-luokkaa
    email: Email               # Käytä Email-luokkaa

Uusi haaste ilmenee!

Nyt kun olemme muuttaneet Person -luokan käyttämään PhoneNumber ja Email -luokkia, meidän on myös muutettava tapa, jolla Person -olio luodaan (tehdään). Meidän on nyt ensin luotava PhoneNumber ja Email -oliot, ennen kuin voimme luoda Person -olion.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Huomaa muutos tässä
                   email=Email("bob_kaare@example.com"))  # Huomaa muutos tässä
print(bob_kaare)

# Huom: muutos on tehtävä myös siihen, miten arvoja haetaan
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Tehtävä 5 - Ominaisuudet luokissa (Valinnainen)

Kun käytämme objekteja arvojen, kuten sähköpostin ja puhelinnumeron, esittämiseen, meidän on määritettävä aliarvo (esim. address sähköpostille ja number puhelinnumerolle) joka kerta, kun haluamme hakea arvon. Tämä voi tulla hieman hankalaksi pitkällä aikavälillä. Onneksi tähän on ratkaisu käyttämällä @property -koristetta luokassa, jolloin voimme hakea arvon suoraan objektista ilman, että meidän tarvitsee määritellä aliarvoa.

Tämä tuo kuitenkin vielä yhden haasteen, ja se on, että tarvitsemme __init__ -funktion Person -luokassa. Tämä johtuu siitä, että emme voi käyttää samaa nimeä sekä ominaisuudelle että attribuutille dataluokassa.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Sisäinen muuttuja (alkaa _:lla osoittaakseen, että se on "yksityinen")

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Hakee aliarvon suoraan

# Testaa koodi
person = Person(name="Alice", verdi=EksempelVerdi("Jotain tekstiä"))
print(person.verdi)  # Output: Jotain tekstiä

Merk

Ominaisuudet ovat ainutlaatuisia siinä mielessä, että ne eivät tarvitse parametreja, eivätkä sulkeita suorittaakseen. Esimerkissä haetaan person.verdi ilman sulkeita (ei person.verdi()), vaikka se teknisesti ottaen on funktio.

Vaihtoehtoinen esimerkki, joka hyväksyy sekä str- että EksempelVerdi-tyypin
@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 ska vara av typ str eller EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Henter ut underverdien direkte

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

Hard Tehtävä 6 - Ominaisuudet logiikalla (Valinnainen)

Päivitä Person lisäämällä uusi attribuutti nimeltä birthday (syntymäpäivä). Sen tyypin tulee olla datetime.date (datetime-kirjastosta).

Luo sen jälkeen seuraavat ominaisuudet:

  • Luo ominaisuus age, joka laskee henkilön iän birthday- ja tämän päivän päivämäärän perusteella.
  • Luo ominaisuus is_adult, joka palauttaa True, jos henkilö on 18 vuotta tai vanhempi, muuten False.

Løsning: Alder og voksen som properties

Tässä on mahdollinen ratkaisu:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Laskee iän syntymäpäivän ja tämän päivän päivämäärän perusteella"""
        today = date.today()
        age = today.year - self.birthday.year
        # Vähennä 1, jos henkilöllä ei ole vielä ollut syntymäpäivää tänä vuonna
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Palauttaa True, jos henkilö on 18-vuotias tai vanhempi"""
        return self.age >= 18

# Testaa koodi
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # Esim. 18 jos tämän päivän päivämäärä on 15. toukokuuta 2023 jälkeen
print(person.is_adult)  # True

Vielä yksi haaste!

Pystytkö saamaan Person-instansoinnin hyväksymään sekä datetime.date-tyypin että tekstin muodossa "DD-MM-YYYY" olevan syntymäpäivän? (Vihje: käytä datetime.strptime-funktiota muuntaaksesi merkkijonon datetime.date-tyypiksi)