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
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)
Oppgåve 1 – Lag ei klasse
Lag ei klasse som heiter Person. Denne klassen skal hava følgjande eigenskapar (attributt):
name: Namnet på personeneye_color: Augafargen til personenphone_number: Telefonnummeret til personenemail: 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
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 @
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
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(?)
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
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
agesom reknar ut alderen til personen basert påbirthdayog dagens dato. - Lag ein eigenskap
is_adultsom returnererTruedersom personen er 18 år eller eldre, ellesFalse.
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)
