Datumvalidigo kaj klasoj

Skip to content

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

En Python (validas ankaŭ por la plej multaj aliaj lingvoj) estas eble krei propraj objektojn, kun propraj valoroj, reguloj kaj funkcioj. Tio nomiĝas klasoj (classes en la angla). Ni uzas klasojn por kolekti rilatajn datumojn kaj funkciojn en unuĵo, ekzemple, klason Ordre kiu havas datumojn kiel ordre_id, kunde_navn, produkter kaj funkciojn kiel legg_til_produkt(), beregn_total(), ktp.

Vortaro (JSON) kontra Klasoj (Objektoj)

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

Kiam ni devas sendi datumojn tra la reto, aŭ konservi ilin en dosiero, ni ofte uzas JSON (aŭ tabelojn en datumbazoj).

Kiam ni laboras kun strukturitaj datumoj en kodo, ni uzas klasojn.

En ĉi tiu modulo ni vidos kiel ni povas uzi klasojn por validegi datumojn.

La plej facila maniero estas uzi @dataclass el la biblioteko dataclasses. Tio permesas al ni eviti skribi multan bultan kodon (boilerplate) por krei klason. (Kiel ekzemple la integritaj __init__ kaj __repr__ (representacio) funkcioj).

Ekzemplo de klaso sen uzi la dataclass dekoratoron:

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)

Solvo: Datklaso por Persono

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 supre ni ne aldonis ajnan validigon. Tio signifas, ke ni povas krei Person 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!)

Solvo: Kodo por simpla retpoŝt-validigo

Jen ebla solvo, ni uzas esceptojn por “kraŝi” se la retpoŝto estas nevalidiga, tio ĉesigos la programon tuj kaj donos erarmesaĝon.

```python
from dataclasses import dataclass

@dataclass
class Email:
address: str

def __post_init__(self):
    if "@" not in self.address:
        raise ValueError(f"Manĝas @ en la retpoŝtadreso: {self.address}")
    if "." not in self.address.split("@")[1]:
        raise ValueError(f"Manĝas . en la domajno de la retpoŝtadreso: {self.address}")

La retpoŝtadreso ne povas enhavi spacojn: {self.address}”)

    if " " in self.address:
        raise ValueError(f"La retpoŝtadreso ne povas enhavi spacojn: {self.address}")

Testu la kodon

test = Email(“hei@example.com”) # Validiga
try:
test = Email(“heiexample.com”)
except ValueError as e:
print(e) # Nevalidiga, mankas @
```

Medium Tasko 3 - Telefona nombra validigo

Krea ekvivalentan klason kiel vi faris por retpoŝtoj, sed nun por telefonaj numeroj

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, kiam ni ŝanĝis la Person klason por uzi PhoneNumber kaj Email klasojn, ni devas ankaŭ ŝanĝi kiel ni instancigas (kreas) Person. Ni nun devas unue krei PhoneNumber kaj Email objektojn, 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  # Intern 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  # Elprenas la subvaloron rekte

# Testu la kodon
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Eligo: Noe tekst

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.attributt  # Elprenas la subvaloron rekte

# Testu la kodon
person = Person(name="Alice", verdi="Iom da teksto")
print(person.verdi)  # Output: Iom da 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.

Solvo: Aĝo kaj plenkuleco kiel propriectoj

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 bazante sur naskiĝdato kaj hodiaŭa dato"""
        today = date.today()
        age = today.year - self.birthday.year
        # Regardigu malsupren kun 1 se la persono ankoraŭ ne havas 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:
        """Rekonligas True se la persono estas 18 jarojn aŭ pli 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 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)