Datenvalidierung und Klassen

Skip to content

Dies ist ein maschinell übersetzter Text, der Fehler enthalten kann!

In Python (dies gilt auch für die meisten anderen Sprachen) ist es möglich, eigene Objekte mit eigenen Werten, Regeln und Funktionen zu erstellen. Dies nennt man Klassen (classes auf Englisch). Wir verwenden Klassen, um verwandte Daten und Funktionen in einer Einheit zusammenzufassen, z. B. eine Bestellung Klasse mit Daten wie bestellungs_id, kunden_name, produkte und Funktionen wie produkt_hinzufügen(), gesamtbetrag_berechnen(), usw.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) ist ein Format zum Speichern und Übertragen von Daten unabhängig von der Programmiersprache, während eine Klasse eine Struktur in einer bestimmten Programmiersprache ist.

Wenn wir Daten über das Netzwerk senden oder in einer Datei speichern möchten, verwenden wir oft JSON (oder Tabellen in Datenbanken).

Wenn wir mit strukturierten Daten im Code arbeiten möchten, verwenden wir Klassen.

In diesem Modul werden wir uns ansehen, wie wir Klassen zur Datenvalidierung verwenden können.

Am einfachsten ist es, @dataclass aus der dataclasses-Bibliothek zu verwenden. Dies erspart uns das Schreiben viel Boilerplate-Code zum Erstellen einer Klasse. (Wie z. B. die eingebauten __init__ und __repr__ (Repräsentation) Funktionen).

Beispiel für eine Klasse ohne die Verwendung des dataclass-Dekorators:

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)  # Ausgabe: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Beispiel mit dem Dataclass-Dekorator, der das Gleiche wie oben erreicht (aber mit weniger Code):

from dataclasses import dataclass

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

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

Easy Aufgabe 1 - Erstelle eine Klasse

Erstelle eine Klasse namens Person. Diese Klasse soll die folgenden Eigenschaften (Attribute) haben:

  • name: Der Name der Person
  • eye_color: Die Augenfarbe der Person
  • phone_number: Die Telefonnummer der Person
  • email: Die E-Mail-Adresse der Person

Instanziiere (verwende) ein Objekt der Klasse Person mit gültigen Werten für alle Eigenschaften, wie im folgenden Beispiel.

@dataclass
class Person:
    ... # Dein Code hier

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number="12345678",
                   email="bob_kaare@example.com")
print(bob_kaare)

Lösung: Eine Dataclass für Person

Hier ist eine mögliche Lösung:

from dataclasses import dataclass

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

Medium Aufgabe 2 - Validierung in der Klasse

In dem Beispiel oben haben wir keine Validierung hinzugefügt. Das bedeutet, dass wir eine Person mit ungültigen Werten erstellen können, wie zum Beispiel:

@dataclass
class Person:
    ... # Ihr Code hier

invalid_person = Person(name="",
                        eye_color="yes",
                        phone_number="12345",
                        email="not-an-email")
print(invalid_person)
# Ausgabe: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')

Dies ist (potenziell) problematisch und kann in Zukunft zu technischer Schuld führen. Glücklicherweise gibt es einfache Möglichkeiten, Klassen Validierungen hinzuzufügen.

Wir werden zunächst die E-Mail-Validierung betrachten. Es gibt integrierte Bibliotheken in Python, die uns dabei helfen können, aber da wir dies zum Lernen tun, werden wir unsere eigene einfache Validierung erstellen, indem wir eine neue Klasse für “Email” erstellen und eine __post_init__-Funktion untersuchen (nur für Dataclasses).

Merk

Wir können dies auch in der eigentlichen Person-Klasse tun, aber es ist oft besser, eigene Klassen für Dinge zu erstellen, die wiederverwendet werden können.

Beispiel einer post_init Funktion
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # Dein Code hier

Für eine einfache E-Mail-Validierung können wir beispielsweise prüfen, ob die E-Mail sowohl das @- als auch das .-Zeichen enthält. Gegebenenfalls können Sie prüfen, ob die E-Mail mit einem Regex-Muster übereinstimmt. (Fortgeschrittener, aber suchen Sie gerne im Internet!)

Lösung: Code für einfache E-Mail-Validierung

Hier ist eine mögliche Lösung, wir verwenden Exceptions, um “abzustürzen”, falls die E-Mail ungültig ist, dies wird das Programm sofort stoppen und eine Fehlermeldung ausgeben.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Mangelndes @ in der E-Mail-Adresse: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Mangelnder . in der Domäne der E-Mail-Adresse: {self.address}")
        if " " in self.address:
            raise ValueError(f"Die E-Mail-Adresse darf keine Leerzeichen enthalten: {self.address}")

# Testen Sie den Code 
test = Email("hei@example.com")  # Gültig
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # Ungültig, mangelt @

Medium Aufgabe 3 - Telefonnummer Validierung

Erstellen Sie analog eine Klasse, wie Sie es für E-Mail-Adressen getan haben, aber jetzt für Telefonnummern.

Herausforderung mit der Telefonvalidierung!

Können Sie die Validierung für Telefonnummern so reparieren, dass sie sowohl Buchstaben (str) als auch Zahlen (int) akzeptiert? Z.B. 12345678 und "12345678" sollen beide gültig sein.

Versuchen Sie auch, Ländercodes als Attribut (Unterwert der Klasse) hinzuzufügen. Z.B. 47 für Norwegen, 46 für Schweden

Medium Aufgabe 4 – Verwenden Sie Validierung in der Person-Klasse

Nun, da wir die Validierung für E-Mail und Telefonnummer erstellt haben, können wir diese in der Person-Klasse verwenden.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Verwenden Sie die PhoneNumber-Klasse
    email: Email               # Verwenden Sie die Email-Klasse

Neue Herausforderung entsteht!

Nun, da wir die Person-Klasse so geändert haben, dass sie die Klassen PhoneNumber und Email verwendet, müssen wir auch ändern, wie wir eine Person instanziieren (erstellen). Wir müssen jetzt zuerst ein PhoneNumber- und ein Email-Objekt erstellen, bevor wir eine Person erstellen können.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Beachte die Änderung hier
                   email=Email("bob_kaare@example.com"))  # Beachte die Änderung hier
print(bob_kaare)

# Anmerkung: Eine Änderung muss auch an der Art und Weise vorgenommen werden, wie wir die Werte extrahieren
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Aufgabe 5 - Properties in Klassen (Optional)

Wenn wir Objekte verwenden, um Werte wie E-Mail und Telefonnummern darzustellen, müssen wir den Unterwert (z. B. address für E-Mail und number für Telefonnummer) jedes Mal angeben, wenn wir den Wert abrufen möchten. Dies kann auf Dauer etwas umständlich werden. Glücklicherweise gibt es eine Lösung dafür, indem wir den @property-Dekorator in einer Klasse verwenden, der es uns ermöglicht, den Wert direkt aus dem Objekt abzurufen, ohne den Unterwert angeben zu müssen.

Dies birgt jedoch eine weitere Herausforderung, und zwar benötigen wir die __init__-Funktion in der Person-Klasse. Dies liegt daran, dass wir denselben Namen nicht sowohl für eine Property als auch für ein Attribut in einer Dataclass verwenden können.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Interne Variable (beginnt mit _ um anzuzeigen, dass sie "privat" ist)

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Ruft den Unterwert direkt ab

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

Merk

Eigenschaften sind einzigartig, da sie keine Parameter benötigen und keine Klammern für die Ausführung benötigen. In dem Beispiel holen wir person.verdi ohne Klammern (nicht person.verdi()), auch wenn es sich technisch gesehen um eine Funktion handelt.

Alternativ Beispiel, das sowohl str als auch EksempelVerdi akzeptiert
@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 muss vom Typ str oder EksempelVerdi sein")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Ruft den Unterwert direkt ab

# Teste den Code
person = Person(name="Alice", verdi="Noch ein Text")
print(person.verdi)  # Ausgabe: Noch ein Text

Hard Aufgabe 6 - Properties mit Logik (Optional)

Aktualisiere Person, indem du ein neues Attribut namens birthday (Geburtstag) hinzufügst. Dieses soll vom Typ datetime.date (aus der datetime Bibliothek) sein.

Erstelle anschließend folgende Properties:

  • Erstelle eine Property age, die das Alter der Person basierend auf birthday und dem heutigen Datum berechnet.
  • Erstelle eine Property is_adult, die True zurückgibt, wenn die Person 18 Jahre oder älter ist, andernfalls False.

Lösung: Alter und Erwachsener als Properties

Hier ist eine mögliche Lösung:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Berechnet das Alter basierend auf dem Geburtsdatum und dem heutigen Datum"""
        today = date.today()
        age = today.year - self.birthday.year
        # Reduziere um 1, wenn die Person dieses Jahr noch keinen Geburtstag hatte
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Gibt True zurück, wenn die Person 18 Jahre oder älter ist"""
        return self.age >= 18

# Teste den Code
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # Z.B. 18, wenn das heutige Datum nach dem 15. Mai 2023 liegt
print(person.is_adult)  # True

Eine weitere Herausforderung!

Schaffen Sie es, die Instanziierung von Person so zu gestalten, dass sie sowohl datetime.date als auch einen Text im Format "DD-MM-YYYY" für birthday akzeptiert? (Hinweis: Verwenden Sie datetime.strptime, um den String in ein datetime.date zu konvertieren)