Datavalidasjon og klassar,

Skip to content

Dette er ein maskinomsett tekst som kann innehalda feil!

I Python (gjeld òg dei fleste andre språka) er det mogleg å skapa eigne objekt, med eigne verdiar, reglar og funksjonar. Dette vert kalla klassar (classes på engelsk). Me brukar klassar til å samla relatert data og funksjonar i ei eining, t.d. ein Ordre klasse som har data som ordre_id, kunde_namn, produkt og funksjonar som legg_til_produkt(), berekn_total(), osv.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) er eit format for å lagra og overføra data uavhengig av programmeringsspråk, medan ei klasse er ei struktur i eit spesifikt programmeringsspråk.

Når me skal sende data over nettverket, eller lagra det i ei fil, så brukar me ofte JSON (eller tabellar i databasar).

Når me skal arbeida med strukturert data i kode, så brukar me klassar.

I denne modulen skal me sjå på korleis me kan bruke klassar til å validere data.

Det lettaste er å bruke @dataclass frå dataclasses biblioteket. Dette gjer at me slepp å skrive mykje boilerplate kode for å opprette ei klasse. (Som f.eks. dei innebygde __init__ og __repr__ (representasjon) funksjonane).

Døme på ei klasse utan å 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)  # Utdata: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Døme med dataclass-dekoratør, som oppnår det same som ovanfor (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)  # Utskrift: Car(make='Toyota', model='Corolla', year=2020)

Easy Oppgåve 1 – Lag ei klasse

Lag ei klasse som heiter Person. Denne klassen skal hava følgjande eigenskapar (attributt):

  • name: Namnet på personen
  • eye_color: Augafargen til personen
  • phone_number: Telefonnummeret til personen
  • email: E-postadressen til personen

Instansier (bruk) eit objekt av klassen Person med gyldige verdiar for alle eigenskapane slik som i dømet 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: Ein dataklasse for Person

Her er ein mogleg løysing:

from dataclasses import dataclass

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

Medium Oppgåve 2 - Validering i klassen

I dømet ovanfor så har me ikkje lagt til nokon validering. Det tyder at me kan lage ein Person med ugyldige verdiar, 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)
# Utgang: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')

Dette er (potensielt) problematisk, og kan fort by på teknisk gjeld i framtida. Heldigvis finst det enkle måtar å leggja til validering i klassar.

Me skal byrja med å sjå på e-post validering. Det finst innebygde bibliotek i Python som kan hjelpa oss med dette, men fordi me gjer dette for å læra, så skal me laga vår eiga enkle validering, ved å laga ein ny klasse til “Email”, og undersøka ein __post_init__ funksjon (berre for dataclass).

Merk

Me kan òg gjera dette i sjølve Person klassen, men det er ofte betre å laga eigne klassar for ting som kan nyttast på nytt.

Døme på ein post_init funksjon
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validerar e-post: {self.address}")
        # Koden din her

For ein enkel e-post validering, so kann me t.d. sjekka at e-posten inneheld både @ og . teikn. Eventuelt so kann du sjekka at e-posten matchar eit regex mønster. (Meir avansert, men søk gjerne på nettet!)

Løysing: Kode for enkel e-post validering

Her er ei mogleg løysing, me brukar unntak for å «krasje» dersom e-posten er ugyldig, dette vil stoppe programmet med ein gong, og gje ei 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 ikkje innehalde 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 Oppgåve 3 – Telefonnummer validering

Lag til tilsvarande ein klasse som du gjorde for e-postar, men no for telefonnummer

Utfordring med telefonvalidering!

Kan du rette valideringa for telefonnummer til å akseptere både bokstavar (str) og tal (int)? T.d. 12345678 og "12345678" skal begge vera gyldige.

Prøv og å leggje til landskodar som ein attributt (underverdi til klassen). T.d. 47 for Noreg, 46 for Sverige

Medium Oppgåve 4 – Bruk validering i Person-klassa

No som me har laga validering for e-post og telefonnummer, so kann me bruka desse i Person-klassa.

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

Ny utfordring uppstår!

No som me har endra Person klassen til å bruka PhoneNumber og Email klassar, so må me óg endra korleis me instansierer (lagar) ein Person. Me må no fyrst laga eit PhoneNumber og eit Email objekt, før me kan laga ein Person.

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

# Obs: ei endring må til i måten me hentar ut verdiane også
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Oppgåve 5 – Eigenskapar i klassar (Valfritt)

Når me brukar objekt til å representera verdiar som e-post og telefonnummer, so er me nøydt til å spesifisera underverdien (t.d. address for e-post og number for telefonnummer) kvar gong me skal hente ut verdien. Dette kann verte litt tungvint i lengden. Heldigvis finst det ei løysing på dette, ved å bruke @property dekoratøren i ei klasse, som gjer at me kann hente ut verdien direkte frå objektet, utan å måtte spesifisera underverdien.

Dette byr dog på endå ei utfordring, og det er at me behøver __init__ funksjonen i Person klassen. Dette er fordi me ikkje kann bruke same namnet til både ein property og eit attributt i ein dataclass.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Indre variabel (byrjar med _ for å visa at ho er «privat»)

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

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

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

Merk

Eigenskapar er unike i og med at dei ikkje treng parametrar, og treng ikkje parentesar for å køyrast. I dømet hentar me ut person.verdi utan parentesar (ikkje person.verdi()), sjølv om det teknisk sett er ei funksjon.

Alternativt døme 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å vere av type str eller EksempelVerdi")

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

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

Hard Oppgåve 6 – Eigenskapar med logikk (Valfritt)

Oppdater Person ved å leggja til ein ny attributt som heiter birthday (fødselsdag). Denne skal vera av typen datetime.date (frå datetime biblioteket).

Deretter skal du laga følgjande eigenskapar:

  • Lag ein eigenskap age som reknar ut alderen til personen basert på birthday og dagens dato.
  • Lag ein eigenskap is_adult som returnerer True dersom personen er 18 år eller eldre, elles False.

Løysing: Alder og vaksen som properties

Her er ei mogleg løysing:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

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

    @property
    def is_adult(self) -> bool:
        """Returnerer True viss 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 viss dagens dato er etter 15. mai 2023
print(person.is_adult)  # True

Endå ei utfordring!

Klarar du å få instansiering av Person til å akseptera både datetime.date og ein tekst i formatet "DD-MM-YYYY" for birthday? (Hint: bruk datetime.strptime for å konvertera strengen til ein datetime.date)