Dátové triedy

ako na dátové triedy pomocou triedy BaseModel

Dátové triedy sú špeciálne triedy, ktoré slúžia na prenos dát. Obsahujú obyčajne iba členské premenné a minimum logiky. Často sa používajú na manažment konfigurácie alebo na správu serializovateľných údajov, ktoré sa napr. prenášajú prostredníctvom REST API alebo protokolom MQTT. Štandardná knižnica jazyka Python obsahuje priamu podporu v balíku dataclasses, ale najznámejšou knižnicou je rozhodne Pydantic.

Natívna podpora dátových tried v jazyku MicroPython však nie je a rovnako vlastnosti tohto jazyka obmedzujú ich plnohodnotné využívanie. Pre naše potreby budeme používať jednoduchú implementáciu v podobe triedy BaseModel z modulu models/base.py, ktorá je inšpirovaná práve knižnicou Pydantic. Jej kód je súčasťou kostry projektu chytrého zariadenia.

Vlastnosti triedy BaseModel

Vytvorenie triedy

Dátovú triedu vytvoríme ako potomka triedy BaseModel. Jej členské premenné uvedieme ako triedne premenné. Typ každej premennej určí jej predvolená hodnota, prípadne parameter type funkcie Field(). Typové anotácie (napr. name: str) slúžia len na lepšiu čitateľnosť kódu, pretože jazyk MicroPython ich pri behu ignoruje.

from models.base import BaseModel, Field


class Address(BaseModel):
    city: str = Field(type=str)
    zip: str = Field(type=str)


class Person(BaseModel):
    name: str = Field(type=str)
    age: int = 0
    email: str = Field(default=None, type=str, optional=True)
    address: Address = Field(default=None, type=Address, optional=True)

Členské premenné triedy Person ukazujú tri možnosti:

Ak do premennej address priradíme slovník, automaticky sa prevedie na objekt triedy Address.

Funkcia Field()

Funkcia Field() umožňuje podrobnejšie opísať členskú premennú. Má tieto parametre:

Ak nie je uvedený ani parameter default, ani default_factory, premenná je povinná.

Parameter default_factory je potrebný pri hodnotách, ktoré sa dajú meniť – napr. pri zoznamoch alebo vnorených dátových triedach. Ak by sme použili default, všetky objekty by zdieľali ten istý zoznam či objekt a zmena v jednom by sa prejavila vo všetkých:

class WiFi(BaseModel):
    ssid: str = Field(default=None, type=str, optional=True)


class Settings(BaseModel):
    wifi: WiFi = Field(default_factory=WiFi, type=WiFi)
    tags: list = Field(default_factory=list)

Každý objekt triedy Settings tak dostane vlastný objekt WiFi aj vlastný prázdny zoznam tags.

Vytvorenie inštancie

Inštanciu je možné vytvoriť priamo pomocou konštruktora:

p = Person(
    name='Alice',
    age=30,
    address=Address(city='Paris', zip='12345')
)

Rovnako ju je možné vytvoriť rozbalením slovníka. Vnorený slovník sa pritom automaticky prevedie na objekt triedy Address:

>>> d = {
  'address': {
    'zip': '54321',
    'city': 'Kosice'
  },
  'age': 23,
  'name': 'Juraj'
}

>>> p2 = Person(**d)
>>> print(p2)
Person(age=23,email=None,address=Address(zip='54321',city='Kosice'),name='Juraj')

Ak pri vytváraní objektu neuvedieme povinnú premennú, vznikne výnimka TypeError:

>>> Person(age=23)
Traceback (most recent call last):
  ...
TypeError: Missing required field "name" for Person.

Čítanie hodnôt

>>> print(f'{p.name} is {p.age} years old and lives in {p.address.city}.')
Alice is 30 years old and lives in Paris.

Zápis hodnôt

Pri zápise hodnôt je dôležité, aby typ zapisovanej hodnoty zodpovedal typu členskej premennej. V opačnom prípade dôjde k výnimke ValueError:

# typ clenskej premennej p.age je int
>>> type(p.age)
<class 'int'>

# ak priradime hodnotu typu int, je vsetko v poriadku
>>> p.age = 32

# ak vsak priradime hodnotu ineho typu, dojde k vynimke
>>> p.age = 'twenty'
Traceback (most recent call last):
  ...
ValueError: Value "twenty" for attribute "age" is not of type "<class 'int'>".

Do voliteľnej členskej premennej je okrem hodnoty jej typu možné priradiť aj None:

>>> p.email = 'alice@example.com'
>>> p.email = None
>>> p.email = 23
Traceback (most recent call last):
  ...
ValueError: Value "23" for attribute "email" is not of type "<class 'str'>".

Do objektu nie je možné pridať členskú premennú, ktorá nie je definovaná v triede:

>>> p.phone = '0905 123 456'
Traceback (most recent call last):
  ...
AttributeError: Attribute "phone" is not in class Person.

Rovnako to platí aj pri vytváraní objektu – ak konštruktor alebo vnorený slovník obsahuje kľúč, ktorý nezodpovedá žiadnej členskej premennej, vznikne výnimka AttributeError:

>>> Person(name='Alice', phone='0905 123 456')
Traceback (most recent call last):
  ...
AttributeError: Attribute "phone" is not in class Person.

Validácia hodnôt

V triede, ktorá je potomkom triedy BaseModel, je možné vytvárať aj vlastné validátory členských premenných. Stačí vytvoriť metódu s dekorátorom @validator(), ktorý dostane názov validovanej premennej. Metóda dostane novú hodnotu a ak ju odmietne, vyvolá výnimku:

from models.base import BaseModel, Field, validator


class Person(BaseModel):
    name: str = Field(type=str)
    age: int = 0

    @validator('age')
    def check_age(self, value):
        if value < 0:
            raise ValueError('Age is negative.')

Validátor sa spustí pri každom priradení hodnoty, aj pri vytváraní objektu:

>>> p = Person(name='Alice', age=-1)
Traceback (most recent call last):
  ...
ValueError: Age is negative.

Validátor sa spustí ešte pred uložením hodnoty. Ak hodnotu odmietne, hodnota členskej premennej sa nezmení:

>>> p = Person(name='Alice', age=30)
>>> p.age = -5
Traceback (most recent call last):
  ...
ValueError: Age is negative.
>>> p.age
30

Serializácia a deserializácia

Objekt dátovej triedy je možné previesť na slovník pomocou metódy .model_dump(). Prevedú sa aj vnorené dátové triedy, takže výsledok je možné priamo uložiť napr. do súboru vo formáte JSON (serializácia):

import json

with open('person.json', 'w') as file:
    json.dump(p.model_dump(), file)

Opačným postupom je vytvorenie objektu zo slovníka, napr. načítaného zo súboru vo formáte JSON (deserializácia). Slovník rozbalíme do konštruktora triedy a vnorené slovníky sa automaticky prevedú na objekty vnorených dátových tried:

import json

with open('person.json') as file:
    p = Person(**json.load(file))

Pri deserializácii sa uplatnia všetky pravidlá triedy – typová kontrola, povinné premenné aj validátory. Ak slovník obsahuje kľúč, ktorý v triede nie je, vznikne výnimka AttributeError.