Datumvalidigo kaj klasoj

Skip to content

Ĉi tio estas maŝine tradukita teksto kiu povas enhavi erarojn!

En Python (kaj ankaŭ en la plejparto de aliaj lingvoj) estas eble krei proprajn objektojn, kun propraj valoroj, reguloj kaj funkcioj. Ĉi tio estas nomata klasoj (classes en la angla). Ni uzas klasojn por kunigi rilatajn datumojn kaj funkciojn en unu tutaĵo, ekz. klaso Ordre kiu havas datumojn kiel ordre_id, kunde_navn, produkter kaj funkciojn kiel legg_til_produkt(), beregn_total(), ktp.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) estas formato por konservi kaj transdoni datumojn sendepende de programlingvoj, dum klaso estas strukturo en specifa programlingvo.

Kiam ni volas sendi datumojn trans la reton, aŭ konservi ilin en dosiero, ni ofte uzas JSON (aŭ tabelojn en datumbazoj).

Kiam ni volas labori kun strukturitaj datumoj en kodo, ni uzas klasojn.

En ĉi tiu modulo ni rigardos kiel ni povas uzi klasojn por validigi datumojn.

La plej facila estas uzi @dataclass el la biblioteko dataclasses. Tio permesas al ni eviti skribi multan ripeteman kodon por krei klasojn. (Kiel ekzemple la enkonstruitaj funkcioj __init__ kaj __repr__ (reprezentado)).

Ekzemplo de klaso sen uzi la dekoratoron 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)  # Eligo: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Ekzemplo kun datklasa dekoratorio, kiu atingas la saman kiel supre (sed kun malpli da kodo):

from dataclasses import dataclass

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

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

Easy Ekzerco 1 - Kreu Klaso

Kreu klaso nomata Person. Ĉi tiu klaso devas havi la jenajn atributojn:

  • name: La nomo de la persono
  • eye_color: La okulkoloro de la persono
  • phone_number: La telefonnumero de la persono
  • email: La retpoŝtadreso de la persono

Instanciu (uzu) objekton de la klaso Person kun validaj valoroj por ĉiuj atributoj kiel en la ekzemplo sube.

@dataclass
class Person:
    ... # Via kodo ĉi tie

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

Jen ebla solvo:

from dataclasses import dataclass

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

Medium Tasko 2 - Validigo en la klaso

En la ekzemplo super ni ne aldonis validigon. Tio signifas, ke ni povas krei Person-on kun nevalidaj valoroj, kiel ekzemple:

@dataclass
class Person:
    ... # Via kodo ĉi tie

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

Tio estas (eble) problema, kaj povas facile kaŭzi teknikan ŝuldon en la estonteco. Feliĉe ekzistas simplaj manieroj aldoni validigon al klasoj.

Ni komencu per rigardi retpoŝtan validigon. Ekzistas enkonstruitaj bibliotekoj en Python kiuj povas helpi nin kun tio, sed ĉar ni faras tion por lerni, ni kreos nian propran simplan validigon, kreante novan klaso por “Retpoŝto”, kaj esplorante funkcion __post_init__ (nur por datklaso).

Merk

Ni povas ankaŭ fari tion en la mem Person klaso, sed ĝi estas ofte pli bona krei proprajn klasojn por aĵoj kiuj povas esti reuzitaj.

Ekzemplo de funkcio post_init
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validigante retpoŝton: {self.address}")
        # Via kodo ĉi tie

Por simpla retpoŝta validigo, ni povas ekzemple kontroli ĉu la retpoŝto enhavas kaj @ kaj . signojn. Eble vi povas kontroli ĉu la retpoŝto kongruas kun regex ŝablono. (Pli progresinta, sed serĉu ĝin sur la reto!)

Løsning: Kode for enkel e-post validering

Jen ebla solvo, ni uzas esceptojn por “kraxigi” se la e-poŝto estas invalida, tio haltigos la programon tuj, kaj donos erarmesaĝon.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Mankas @ en la e-poŝtadreso: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Mankas . en la domajno de la e-poŝtadreso: {self.address}")
        if " " in self.address:
            raise ValueError(f"La e-poŝtadreso ne povas enhavi spacojn: {self.address}")

# Testu la kodon 
test = Email("hei@example.com")  # Valida
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Invalida, mankas @

Medium Tasko 3 - Telefonnumero Validigo

Kreu samforman klaso kiel vi faris por retpoŝtoj, sed nun por telefonnumeroj.

Defio kun telefonvalidigo!

Ĉu vi povas ripari la validigon por telefonnumeroj por akcepti kaj literojn (str) kaj nombrojn (int)? Ekz. 12345678 kaj "12345678" devus esti ambaŭ validaj.

Provu ankaŭ aldoni landokodojn kiel atributon (subvaloron de la klaso). Ekz. 47 por Norvegio, 46 por Svedio

Medium Tasko 4 - Uzu validigon en la Klaso Persono

Nun kiam ni kreis validigon por retpoŝto kaj telefonnumero, ni povas uzi ilin en la klaso Persono.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Uzu la klaso PhoneNumber
    email: Email               # Uzu la klaso Email

Nova defio aperas!

Nun, ĉar ni ŝanĝis la klaso Person por uzi la klasojn PhoneNumber kaj Email, ni ankaŭ devas ŝanĝi la manieron kiel ni instancigas (kreas) Person. Ni nun unue devas krei objekton PhoneNumber kaj Email, antaŭ ol ni povas krei Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Notu la ŝanĝon ĉi tie
                   email=Email("bob_kaare@example.com"))  # Notu la ŝanĝon ĉi tie
print(bob_kaare)

# Notu: ŝanĝo devas esti farita en la maniero kiel ni elprenas la valorojn ankaŭ
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Tasko 5 - Propraĵoj en klasoj (Elektebla)

Kiam ni uzas objektojn por reprezenti valorojn kiel retpoŝto kaj telefonnumero, ni devas specifigi la subvaloron (ekz. address por retpoŝto kaj number por telefonnumero) ĉiufoje kiam ni volas elpreni la valoron. Tio povas iĝi iomete malfacila en la longa termino. Feliĉe ekzistas solvo por tio, uzante la dekoratoron @property en klaso, kiu permesas al ni elpreni la valoron rekte de la objekto, sen devi specifigi la subvaloron.

Tio tamen prezentas ankoraŭ unu defion, kaj tio estas, ke ni bezonas la funkcion __init__ en la klaso Person. Tio estas ĉar ni ne povas uzi la saman nomon por kaj propraĵo kaj atributo en dataklase.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Interna variablo (komencas per _ por indiki ke ĝi estas "privata")

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Akiras la subvaloron rekte

# Testu la kodon
person = Person(name="Alice", verdi=EksempelVerdi("Iu teksto"))
print(person.verdi)  # Output: Iu teksto

Merk

Ecoj estas unikaj ĉar ili ne bezonas parametrojn, kaj ili ne bezonas kramojn por ekzekutiĝi. En la ekzemplo, ni elprenas person.valoro sen kramoj (ne person.valoro()), kvankam teknike ĝi estas funkcio.

Alternativa ekzemplo kiu akceptas kaj str kaj 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 devas esti de tipo str aŭ EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributo  # Hentas la subvaloron rekte

# Testu la kodon
person = Person(name="Alice", verdi="Iu teksto")
print(person.verdi)  # Eligo: Iu teksto

Hard Tasko 6 - Propraĵoj kun logiko (Elekta)

Ĝisdatigu Person per aldono de nova atributo nomata birthday (naskiĝtago). Ĝi devas esti de tipo datetime.date (de la biblioteko datetime).

Poste, kreu la jenajn propraĵojn:

  • Kreu propraĵon age kiu kalkulas la aĝon de la persono bazite sur birthday kaj la nuntatan daton.
  • Kreu propraĵon is_adult kiu returnas True se la persono estas 18-jara aŭ pli aĝa, alie False.

Løsning: Alder og voksen som properties

Jen ebla solvo:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Kalkulas la aĝon bazite sur la naskiĝdato kaj la hodiaŭa dato"""
        today = date.today()
        age = today.year - self.birthday.year
        # Ĝustigu malkielon kun 1 se la persono ankoraŭ ne havis naskiĝtagon ĉi-jare
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Returnas True se la persono estas 18-jara aŭ pli aĝa"""
        return self.age >= 18

# Testu la kodon
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # Ekz. 18 se la hodiaŭa dato estas post la 15-a de majo 2023
print(person.is_adult)  # True

Ankoraŭ unu defio!

Ĉu vi povas igi la instanciigon de Person akcepti kaj datetime.date kaj tekston en la formato "DD-MM-YYYY" por birthday? (Konsilo: uzu datetime.strptime por konverti la ŝnuron al datetime.date)