Datavalidering og klasser

Skip to content

I Python (gjelder også de fleste andre språk) er det mulig å opprette egne objekter, med egne verdier, regler og funksjoner. Dette kalles klasser (classes på engelsk). Vi bruker classes til å samle relatert data og funksjoner i en enhet, f.eks. en Ordre klasse som har data som ordre_id, kunde_navn, produkter og funksjoner som legg_til_produkt(), beregn_total(), osv.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) er et format for å lagre og overføre data uavhengig av programmeringsspråk, mens en klasse er en struktur i et spesifikt programmeringsspråk.

Når vi skal sende data over nettverket, eller lagre det i en fil, så bruker vi ofte JSON (eller tabeller i databaser).

Når vi skal jobbe med strukturert data i kode, så bruker vi klasser.

I denne modulen skal vi se på hvordan vi kan bruke klasser til å validere data.

Det letteste er å bruke @dataclass fra dataclasses biblioteket. Dette gjør at vi slipper å skrive mye boilerplate kode for å opprette en klasse. (Som f.eks. de innebygde __init__ og __repr__ (representasjon) funksjonene).

Eksempel på en class uten å bruke dataclass dekoratøren:

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

British python devs be like thats a constructor, __init__?

Eksempel med dataclass dekoratør, som oppnår det samme som over (men med mindre kode):

from dataclasses import dataclass

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

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

Easy Oppgave 1 - Lag en klasse

Lag en klasse som heter Person. Denne klassen skal ha følgende egenskaper (attributes):

  • name: Navnet på personen
  • eye_color: Øyenfargen til personen
  • phone_number: Telefonnummeret til personen
  • email: Epostadressen til personen

Instansier (ta i bruk) et objekt av klassen Person med gyldige verdier for alle egenskapene slik som i eksempelet under.

@dataclass
class Person:
    ... # Din kode her

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

Her er en mulig løsning:

from dataclasses import dataclass

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

Medium Oppgave 2 - Validering i klassen

I eksempelet over så har vi ikke lagt til noen validering. Det betyr at vi kan lage en Person med ugyldige verdier, slik som:

@dataclass
class Person:
    ... # Din kode her

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

Dette er (potensielt) problematisk, og kan fort by på teknisk gjeld i fremtiden. Heldigvis finnes det enkle måter å legge til validering i klasser.

Vi skal begynne med å se på e-post validering. Det finnes innebygde biblioteker i Python som kan hjelpe oss med dette, men fordi vi gjør dette for å lære, så skal vi lage vår egen enkle validering, ved å lage en ny klasse til “Email”, og undersøke en __post_init__ funksjon (kun for dataclass).

Merk

Vi kan også gjøre dette i selve Person klassen, men det er ofte bedre å lage egne klasser for ting som kan gjenbrukes.

Eksempel på en post_init funksjon
from dataclasses import dataclass

@dataclass
class Email:
    address: str

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

For en enkel e-post validering, så kan vi f.eks. sjekke at e-posten inneholder både @ og . tegn. Eventuelt så kan du sjekke at e-posten matcher et regex mønster. (Mer avansert, men søk gjerne på nettet!)

Løsning: Kode for enkel e-post validering

Her er en mulig løsning, vi bruker exceptions for å “krasje” dersom e-posten er ugyldig, dette vil stoppe programmet med en gang, og gi en feilmelding.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Mangler @ i epostadressen: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Mangler . i domenet til epostadressen: {self.address}")
        if " " in self.address:
            raise ValueError(f"Epostadressen kan ikke inneholde mellomrom: {self.address}")

# Test koden 
test = Email("hei@example.com")  # Gyldig
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Ugyldig, mangler @

Medium Oppgave 3 - Telefonnummer validering

Lag tilsvarende en class som du gjorde for e-poster, men nå for telefonnummer

Utfordring med telefonvalidering!

Kan du fikse validering for telefonnummer til å akseptere både bokstaver (str) og tall (int)? F.eks. 12345678 og "12345678" skal begge være gyldige.

Prøve også å legge til landskoder som en attributt (underverdi til klassen). F.eks. 47 for Norge, 46 for Sverige

Medium Oppgave 4 - Bruk validering i Person klassen

Nå som vi har laget validering for e-post og telefonnummer, så kan vi bruke disse i Person klassen.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Bruk PhoneNumber klassen
    email: Email               # Bruk Email klassen

Ny utfordring oppstår!

Nå som vi har endret Person klassen til å bruke PhoneNumber og Email klasser, så må vi også endre hvordan vi instansierer (lager) en Person. Vi må nå først lage et PhoneNumber og en Email objekt, før vi kan lage en Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Merk endringen her
                   email=Email("bob_kaare@example.com"))  # Merk endringen her
print(bob_kaare)

# Obs: en endring må til i måten vi henter ut verdiene også
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Oppgave 5 - Properties i classes (Valgfri)

Når vi bruker objekter til å representere verdier som e-post og telefonnummer, så er vi nødt til å spesifisere underverdien (f.eks. address for e-post og number for telefonnummer) hver gang vi skal hente ut verdien. Dette kan bli litt tungvint i lengden. Heldigvis finnes det en løsning på dette, ved å bruke @property dekoratøren i en klasse, som gjør at vi kan hente ut verdien direkte fra objektet, uten å måtte spesifisere underverdien.

Dette byr dog på enda en utfordring, og det er at vi behøver __init__ funksjonen i Person klassen. Dette er fordi vi ikke kan bruke samme navn til både en property og en attributt i en dataclass.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Intern variabel (begynner med _ for å indikere at den er "privat")

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

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

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

Merk

Properties er unike i og med at de ikke trenger parametre, og trenger ikke parantereser for å kjøres. I eksempelet henter vi ut person.verdi uten paranteser (ikke person.verdi()), selv om det teknisk sett er en funksjon.

Alternativt eksempel som aksepterer både str og 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 må være av type 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 Oppgave 6 - Properties med logikk (Valgfri)

Oppdater Person ved å legge inn en ny attributt som heter birthday (fødselsdag). Denne skal være av typen datetime.date (fra datetime biblioteket).

Deretter skal du lage følgende properties:

  • Lag en property age som regner ut alderen til personen basert på birthday og dagens dato.
  • Lag en property is_adult som returnerer True dersom personen er 18 år eller eldre, ellers False.
Løsning: Alder og voksen som properties

Her er en mulig løsning:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Regner ut alderen basert på fødselsdato og dagens dato"""
        today = date.today()
        age = today.year - self.birthday.year
        # Juster ned med 1 hvis personen ikke har hatt bursdag i år ennå
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Returnerer True hvis personen er 18 år eller eldre"""
        return self.age >= 18

# Test koden
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # F.eks. 18 hvis dagens dato er etter 15. mai 2023
print(person.is_adult)  # True

Enda en utfordring!

Klarer du å få instansiering av Person til å akseptere både datetime.date og en tekst i formatet "DD-MM-YYYY" for birthday? (Hint: bruk datetime.strptime for å konvertere strengen til en datetime.date)