Validación de datos y clases

Skip to content

¡Este es un texto traducido automáticamente que puede contener errores!

En Python (esto también se aplica a la mayoría de los otros lenguajes) es posible crear objetos propios, con valores, reglas y funciones propias. Esto se llama clases (classes en inglés). Usamos clases para recopilar datos y funciones relacionados en una unidad, por ejemplo, una clase Orden que tiene datos como orden_id, nombre_cliente, productos y funciones como agregar_producto(), calcular_total(), etc.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) es un formato para almacenar y transferir datos independientemente del lenguaje de programación, mientras que una clase es una estructura en un lenguaje de programación específico.

Cuando necesitamos enviar datos a través de la red, o almacenarlos en un archivo, a menudo usamos JSON (o tablas en bases de datos).

Cuando necesitamos trabajar con datos estructurados en el código, usamos clases.

En este módulo, veremos cómo podemos usar clases para validar datos.

Lo más fácil es usar @dataclass de la biblioteca dataclasses. Esto nos permite evitar escribir mucho código repetitivo para crear una clase. (Como, por ejemplo, las funciones integradas __init__ y __repr__ (representación)).

Ejemplo de una clase sin usar el decorador 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)  # Salida: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

Ejemplo con decorador de dataclass, que logra lo mismo que lo anterior (pero con menos código):

from dataclasses import dataclass

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

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

Easy Tarea 1 - Crea una clase

Crea una clase llamada Person. Esta clase debe tener las siguientes propiedades (atributos):

  • name: El nombre de la persona
  • eye_color: El color de ojos de la persona
  • phone_number: El número de teléfono de la persona
  • email: La dirección de correo electrónico de la persona

Instancia (utiliza) un objeto de la clase Person con valores válidos para todas las propiedades como en el ejemplo a continuación.

@dataclass
class Person:
    ... # Tu código aquí

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

Solución: Una dataclass para Persona

Aquí hay una posible solución:

from dataclasses import dataclass

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

Medium Tarea 2 - Validación en la clase

En el ejemplo anterior, no hemos añadido ninguna validación. Esto significa que podemos crear una Persona con valores inválidos, como:

@dataclass
class Person:
    ... # Tu código aquí

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

Esto es (potencialmente) problemático, y puede fácilmente generar deuda técnica en el futuro. Afortunadamente, existen formas sencillas de añadir validación a las clases.

Vamos a empezar viendo la validación de correo electrónico. Existen bibliotecas integradas en Python que pueden ayudarnos con esto, pero como lo hacemos para aprender, vamos a crear nuestra propia validación sencilla, creando una nueva clase para “Email” y examinando una función __post_init__ (solo para dataclass).

Merk

Nosotros podemos también hacer esto en la propia clase Persona, pero a menudo es mejor crear clases separadas para cosas que puedan reutilizarse.

Ejemplo de una función post_init
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validando correo electrónico: {self.address}")
        # Tu código aquí

Para una validación de correo electrónico sencilla, por ejemplo, podemos comprobar que el correo electrónico contiene tanto el carácter @ como el carácter .. Eventualmente, puedes comprobar que el correo electrónico coincida con un patrón regex. (Más avanzado, ¡pero busca en internet!)

Solución: Código para validación simple de correo electrónico

Aquí hay una posible solución, usamos excepciones para “fallar” si el correo electrónico no es válido, esto detendrá el programa inmediatamente y mostrará un mensaje de error.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Falta @ en la dirección de correo electrónico: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"Falta . en el dominio de la dirección de correo electrónico: {self.address}")
        if " " in self.address:
            raise ValueError(f"La dirección de correo electrónico no puede contener espacios: {self.address}")

# Prueba el código 
test = Email("hola@example.com")  # Válido
try:
    test = Email("holaexample.com")
except ValueError as e:
    print(e) # Inválido, falta @

Medium Tarea 3 - Validación de números de teléfono

Crea una clase similar a la que hiciste para los correos electrónicos, pero ahora para números de teléfono.

¡Desafío con la validación de teléfonos!

¿Puedes solucionar la validación para números de teléfono para que acepte tanto letras (str) como números (int)? Por ejemplo, 12345678 y "12345678" deben ser ambos válidos.

Intenta también agregar códigos de país como un atributo (subvalor de la clase). Por ejemplo, 47 para Noruega, 46 para Suecia

Medium Tarea 4 - Utilizar la validación en la clase Persona

Ahora que hemos creado la validación para el correo electrónico y el número de teléfono, podemos utilizar estas en la clase Persona.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # Usa la clase PhoneNumber
    email: Email               # Usa la clase Email

¡Nuevo desafío surge!

Ahora que hemos cambiado la clase Person para usar las clases PhoneNumber y Email, también debemos cambiar la forma en que instanciamos (creamos) una Person. Ahora debemos primero crear un objeto PhoneNumber y un objeto Email, antes de poder crear una Person.

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # Nota el cambio aquí
                   email=Email("bob_kaare@example.com"))  # Nota el cambio aquí
print(bob_kaare)

# Obs: un cambio debe hacerse en la forma en que extraemos los valores también
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard Tarea 5 - Propiedades en clases (Opcional)

Cuando usamos objetos para representar valores como correo electrónico y número de teléfono, necesitamos especificar el subvalor (por ejemplo, address para correo electrónico y number para número de teléfono) cada vez que queremos obtener el valor. Esto puede volverse un poco engorroso a la larga. Afortunadamente, existe una solución a esto, utilizando el decorador @property en una clase, lo que nos permite obtener el valor directamente del objeto, sin tener que especificar el subvalor.

Esto plantea, sin embargo, otro desafío, y es que necesitamos la función __init__ en la clase Person. Esto se debe a que no podemos usar el mismo nombre para una propiedad y un atributo en una dataclass.

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # Variable interna (comienza con _ para indicar que es "privada")

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

    @property
    def verdi(self):
        return self._verdi.attributt  # Obtiene el subvalor directamente

# Prueba el código
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # Salida: Noe tekst

Merk

Las propiedades son únicas en el sentido de que no necesitan parámetros y no necesitan paréntesis para ejecutarse. En el ejemplo, extraemos person.verdi sin paréntesis (no person.verdi()), aunque técnicamente sea una función.

Ejemplo alternativo que acepta tanto str como 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å være av type str eller EksempelVerdi")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # Henter ut underverdien direkte

# Test koden
person = Person(name="Alice", verdi="Noe tekst")
print(person.verdi)  # Output: Noe tekst

Hard Tarea 6 - Propiedades con lógica (Opcional)

Actualiza Person añadiendo un nuevo atributo llamado birthday (cumpleaños). Este debe ser de tipo datetime.date (del la biblioteca datetime).

Luego, crea las siguientes propiedades:

  • Crea una propiedad age que calcule la edad de la persona basándose en birthday y la fecha actual.
  • Crea una propiedad is_adult que retorne True si la persona tiene 18 años o más, de lo contrario False.

Solución: Edad y adulto como propiedades

Aquí hay una posible solución:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """Calcula la edad basándose en la fecha de nacimiento y la fecha actual"""
        today = date.today()
        age = today.year - self.birthday.year
        # Ajusta hacia abajo en 1 si la persona aún no ha tenido su cumpleaños este año
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """Devuelve True si la persona tiene 18 años o más"""
        return self.age >= 18

# Prueba el código
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # Por ejemplo, 18 si la fecha actual es posterior al 15 de mayo de 2023
print(person.is_adult)  # True

¡Otro desafío!

¿Puedes hacer que la instanciación de Person acepte tanto datetime.date como un texto en el formato "DD-MM-AAAA" para birthday? (Pista: usa datetime.strptime para convertir la cadena a un datetime.date)