Repositorio: h,ps://github.com/arlemorales27/python_Arrays_dict_clase_crud.
git
1. Listas (List)
from typing import List, Any
# Lista con tipo específico (solo enteros)
numeros: List[int] = [1, 2, 3, 4, 5]
# Lista que puede contener diferentes tipos
datos_mixtos: List[Any] = [1, "hola", 3.14, True]
# Operaciones comunes con listas tipadas
def suma_lista(numeros: List[int]) -> int:
return sum(numeros)
def agregar_elemento(lista: List[str], elemento: str) -> None:
lista.append(elemento)
# Ejemplo de uso
nombres: List[str] = ["Ana", "Juan"]
agregar_elemento(nombres, "Carlos")
print(nombres) # Output: ['Ana', 'Juan', 'Carlos']
2. Tuplas (Tuple)
from typing import Tuple
# Tupla con tipos específicos
coordenadas: Tuple[float, float] = (10.5, 20.7)
# Tupla con diferentes tipos
usuario: Tuple[int, str, bool] = (1, "Juan Pérez", True)
# Función que retorna múltiples valores tipados
def obtener_datos_usuario() -> Tuple[str, int, str]:
return ("Juan", 25, "
[email protected]")
nombre, edad, email = obtener_datos_usuario()
3. Diccionarios (Dict)
from typing import Dict, Union
# Diccionario con tipos específicos
usuarios: Dict[str, str] = {
"usuario1": "Juan",
"usuario2": "María"
}
# Diccionario con valores de diferentes tipos
perfil_usuario: Dict[str, Union[str, int, bool]] = {
"nombre": "Ana",
"edad": 25,
"activo": True
}
# Función que procesa un diccionario tipado
def obtener_edad(usuario: Dict[str, Union[str, int, bool]]) -> int:
return int(usuario["edad"])
edad_usuario = obtener_edad(perfil_usuario)
4. Conjuntos (Set)
from typing import Set
# Conjunto de elementos únicos tipados
numeros_unicos: Set[int] = {1, 2, 3, 4, 5}
# Función que trabaja con conjuntos
def unir_conjuntos(conjunto1: Set[str], conjunto2: Set[str]) -> Set[str]:
return conjunto1.union(conjunto2)
nombres1: Set[str] = {"Ana", "Juan"}
nombres2: Set[str] = {"María", "Pedro"}
todos_nombres = unir_conjuntos(nombres1, nombres2)
5. Arrays (usando NumPy para arrays más eficientes)
import numpy as np
from typing import List, Any
from numpy.typing import NDArray
# Array tipado de NumPy
numeros_array: NDArray[np.int64] = np.array([1, 2, 3, 4, 5])
# Función que procesa arrays NumPy
def calcular_promedio(arr: NDArray[np.float64]) -> float:
return float(np.mean(arr))
datos: NDArray[np.float64] = np.array([1.5, 2.7, 3.2, 4.8])
promedio = calcular_promedio(datos)
Funciones especiales
from typing import List, Any, Optional
def demostrar_operaciones_listas() -> None:
# Creamos una lista inicial
frutas: List[str] = ["manzana", "banana", "naranja"]
# 1. Agregar elementos (similar a push en otros lenguajes)
frutas.append("pera") # Agrega al final
print(f"Después de append: {frutas}")
# Output: ['manzana', 'banana', 'naranja', 'pera']
# 2. Insertar en una posición específica
frutas.insert(1, "uva") # Inserta en el índice 1
print(f"Después de insert: {frutas}")
# Output: ['manzana', 'uva', 'banana', 'naranja', 'pera']
# 3. Extender la lista con otra lista
mas_frutas: List[str] = ["mango", "papaya"]
frutas.extend(mas_frutas)
print(f"Después de extend: {frutas}")
# Output: ['manzana', 'uva', 'banana', 'naranja', 'pera', 'mango',
'papaya']
# 4. Eliminar elementos
# Por valor
frutas.remove("uva")
print(f"Después de remove: {frutas}")
# Por índice
fruta_eliminada: str = frutas.pop(2) # Elimina y retorna el elemento
print(f"Elemento eliminado: {fruta_eliminada}")
print(f"Después de pop: {frutas}")
# Eliminar último elemento
ultima_fruta: str = frutas.pop() # Sin índice elimina el último
# 5. Buscar elementos
indice_naranja: int = frutas.index("naranja")
print(f"Índice de naranja: {indice_naranja}")
# 6. Contar ocurrencias
frutas.append("manzana")
cantidad_manzanas: int = frutas.count("manzana")
print(f"Cantidad de manzanas: {cantidad_manzanas}")
# 7. Ordenar la lista
frutas.sort() # Ordena la lista original
print(f"Lista ordenada: {frutas}")
frutas.sort(reverse=True) # Orden descendente
print(f"Lista ordenada descendente: {frutas}")
# 8. Invertir la lista
frutas.reverse()
print(f"Lista invertida: {frutas}")
# 9. Copiar una lista
nueva_lista: List[str] = frutas.copy()
# 10. Limpiar la lista
frutas.clear()
print(f"Lista después de clear: {frutas}")
# Ejecutamos la demostración
demostrar_operaciones_listas()
Ahora veamos los Diccionarios:
from typing import Dict, Union, List, Optional
def demostrar_operaciones_diccionarios() -> None:
# Creamos un diccionario inicial
estudiante: Dict[str, Union[str, int, List[float]]] = {
"nombre": "Ana",
"edad": 20,
"notas": [8.5, 9.0, 7.5]
}
# 1. Agregar o modificar elementos
estudiante["carrera"] = "Informática" # Agregar nuevo par clave-
valor
estudiante["edad"] = 21 # Modificar valor existente
# 2. Obtener valores
# Con get (manera segura, permite valor por defecto)
nombre: str = estudiante.get("nombre", "Sin nombre")
# Si la clave no existe, retorna el valor por defecto
telefono: str = estudiante.get("telefono", "No disponible")
# 3. Obtener todas las claves y valores
claves: List[str] = list(estudiante.keys())
valores: List[Union[str, int, List[float]]] =
list(estudiante.values())
items: List[tuple] = list(estudiante.items())
# 4. Verificar si existe una clave
tiene_nombre: bool = "nombre" in estudiante
# 5. Eliminar elementos
# Pop - Elimina y retorna el valor
edad_eliminada: int = estudiante.pop("edad")
# Popitem - Elimina y retorna el último par insertado
ultimo_par: tuple = estudiante.popitem()
# Del - Elimina directamente
if "notas" in estudiante:
del estudiante["notas"]
# 6. Actualizar el diccionario
nuevos_datos: Dict[str, Union[str, int]] = {
"semestre": 3,
"estado": "activo"
}
estudiante.update(nuevos_datos)
# 7. Obtener valor con manejo de error
try:
nota_media: float = sum(estudiante.get("notas", [0])) /
len(estudiante.get("notas", [1]))
except ZeroDivisionError:
nota_media = 0.0
# 8. Crear un diccionario a partir de listas
claves_nuevas: List[str] = ["a", "b", "c"]
valores_nuevos: List[int] = [1, 2, 3]
nuevo_dict: Dict[str, int] = dict(zip(claves_nuevas, valores_nuevos))
# 9. Diccionario por comprensión
cuadrados: Dict[int, int] = {x: x**2 for x in range(5)}
# 10. Fusionar diccionarios (Python 3.9+)
dict1: Dict[str, int] = {"a": 1, "b": 2}
dict2: Dict[str, int] = {"c": 3, "d": 4}
dict_combinado: Dict[str, int] = dict1 | dict2
# Imprimir resultados para demostración
print(f"Estudiante final: {estudiante}")
print(f"Nuevo diccionario: {nuevo_dict}")
print(f"Cuadrados: {cuadrados}")
print(f"Diccionarios combinados: {dict_combinado}")
# Ejecutamos la demostración
demostrar_operaciones_diccionarios()
Algunos puntos importantes a destacar:
1. En listas:
o append() es el equivalente a push() en otros lenguajes
o pop() puede eliminar y retornar tanto el último elemento como uno
específico
o Hay múltiples formas de eliminar elementos: remove(), pop(), del
2. En diccionarios:
o Usar get() es más seguro que acceso directo con corchetes
o update() es muy útil para combinar diccionarios
o Los diccionarios son muy flexibles en cuanto a los tipos de valores que
pueden almacenar
3. Tips generales:
o Siempre verifica la existencia de claves en diccionarios antes de acceder
o Usa type hints para hacer el código más mantenible
from typing import Optional, List
from datetime import date
class Empleado:
# Variable de clase (compartida por todas las instancias)
empresa: str = "TechCorp"
# Constructor
def __init__(self, nombre: str, salario: float) -> None:
# Atributos públicos (accesibles desde cualquier parte)
self.nombre: str = nombre
# Atributos protegidos (convención de un guion bajo)
# Se sugiere no acceder directamente desde fuera de la clase
self._departamento: str = "Sin asignar"
# Atributos privados (doble guion bajo)
# Python hace name mangling para dificultar el acceso directo
self.__salario: float = salario
self.__fecha_ingreso: date = date.today()
# Lista para almacenar el historial de roles
self.__historial_roles: List[str] = []
# Property getter - Obtener el salario
@property
def salario(self) -> float:
"""Obtiene el salario del empleado"""
return self.__salario
# Property setter - Establecer el salario
@salario.setter
def salario(self, nuevo_salario: float) -> None:
"""Establece el salario del empleado con validación"""
if nuevo_salario < 0:
raise ValueError("El salario no puede ser negativo")
self.__salario = nuevo_salario
# Método público
def asignar_departamento(self, departamento: str) -> None:
"""Asigna un departamento al empleado"""
self._departamento = departamento
# Método protegido
def _calcular_bonus(self) -> float:
"""Calcula el bonus basado en el salario"""
return self.__salario * 0.1
# Método privado
def __registrar_cambio(self, nuevo_rol: str) -> None:
"""Registra internamente un cambio de rol"""
self.__historial_roles.append(nuevo_rol)
# Método público que usa métodos privados y protegidos
def promocionar(self, nuevo_rol: str) -> None:
"""Promociona al empleado a un nuevo rol"""
self.__registrar_cambio(nuevo_rol)
bonus = self._calcular_bonus()
self.__salario += bonus
print(f"{self.nombre} ha sido promocionado a {nuevo_rol}")
# Método estático (no necesita acceso a la instancia)
@staticmethod
def verificar_dni(dni: str) -> bool:
"""Verifica si un DNI es válido"""
return len(dni) == 8 and dni.isdigit()
# Método de clase (tiene acceso a la clase pero no a la instancia)
@classmethod
def cambiar_empresa(cls, nueva_empresa: str) -> None:
"""Cambia el nombre de la empresa para todos los empleados"""
cls.empresa = nueva_empresa
def __str__(self) -> str:
"""Representación en string del empleado"""
return f"Empleado: {self.nombre}, Departamento:
{self._departamento}"
Ahora veamos cómo usar esta clase:
# Creación de instancias
empleado1 = Empleado("Ana García", 30000.0)
empleado2 = Empleado("Juan Pérez", 35000.0)
# Uso de métodos públicos
empleado1.asignar_departamento("Desarrollo")
print(empleado1) # Output: Empleado: Ana García, Departamento:
Desarrollo
# Uso de properties (getters y setters)
print(empleado1.salario) # Usa el getter
empleado1.salario = 32000.0 # Usa el setter
# Uso de método estático
dni_valido = Empleado.verificar_dni("12345678")
# Uso de método de clase
Empleado.cambiar_empresa("NewCorp")
print(empleado1.empresa) # Output: NewCorp
print(empleado2.empresa) # Output: NewCorp
# Promocionar empleado (usa métodos privados y protegidos internamente)
empleado1.promocionar("Tech Lead")
Vamos a crear una clase heredada para mostrar la herencia:
class Gerente(Empleado):
def __init__(self, nombre: str, salario: float, equipo:
Optional[List[Empleado]] = None) -> None:
# Llamamos al constructor de la clase padre
super().__init__(nombre, salario)
# Atributos específicos de Gerente
self.equipo: List[Empleado] = equipo or []
# Sobrescribimos el método de la clase padre
def _calcular_bonus(self) -> float:
"""Sobrescribe el cálculo de bonus para gerentes"""
# Los gerentes reciben un bonus mayor
return self.salario * 0.2
def agregar_miembro_equipo(self, empleado: Empleado) -> None:
"""Agrega un miembro al equipo del gerente"""
self.equipo.append(empleado)
Aspectos importantes a destacar:
1. Encapsulación:
o Atributos públicos: nombre
o Atributos protegidos: _departamento
o Atributos privados: __salario, __fecha_ingreso
2. Propiedades (Properties):
o Permiten controlar el acceso a atributos
o Pueden incluir validación
o Mantienen la encapsulación
3. Métodos especiales:
o __init__: Constructor
o __str__: Representación en string
o Pueden ser públicos, protegidos o privados
4. Decoradores:
o @property: Para getters
o @x.setter: Para setters
o @staticmethod: Para métodos estáticos
o @classmethod: Para métodos de clase
5. Herencia:
o La clase Gerente hereda de Empleado
o Usa super() para llamar al constructor padre
o Puede sobrescribir métodos del padre
Ejemplo de uso de la herencia:
# Crear un gerente
gerente = Gerente("Carlos López", 50000.0)
# Agregar empleados a su equipo
gerente.agregar_miembro_equipo(empleado1)
gerente.agregar_miembro_equipo(empleado2)
# El gerente tiene acceso a los métodos de Empleado
gerente.asignar_departamento("Dirección")
# Pero su bonus se calcula diferente (polimorfismo)
gerente.promocionar("Director de Área")
Métodos abstractos
from abc import ABC, abstractmethod
from typing import List, Optional
# Clase abstracta base que define una interfaz para figuras geométricas
class FiguraGeometrica(ABC):
def __init__(self, color: str) -> None:
self.color = color
# Método abstracto que DEBE ser implementado por las clases hijas
@abstractmethod
def calcular_area(self) -> float:
"""Calcula el área de la figura geométrica"""
pass
# Método abstracto que DEBE ser implementado por las clases hijas
@abstractmethod
def calcular_perimetro(self) -> float:
"""Calcula el perímetro de la figura geométrica"""
pass
# Método normal (no abstracto) que pueden usar todas las clases hijas
def describir(self) -> str:
"""Retorna una descripción de la figura"""
return f"Soy una figura de color {self.color}"
# Implementación concreta de la clase abstracta
class Rectangulo(FiguraGeometrica):
def __init__(self, color: str, base: float, altura: float) -> None:
# Llamamos al constructor de la clase padre
super().__init__(color)
self.base = base
self.altura = altura
# Implementación del método abstracto
def calcular_area(self) -> float:
return self.base * self.altura
# Implementación del método abstracto
def calcular_perimetro(self) -> float:
return 2 * (self.base + self.altura)
# Otra implementación concreta
class Circulo(FiguraGeometrica):
def __init__(self, color: str, radio: float) -> None:
super().__init__(color)
self.radio = radio
def calcular_area(self) -> float:
return 3.14159 * self.radio ** 2
def calcular_perimetro(self) -> float:
return 2 * 3.14159 * self.radio
# Sistema que utiliza las figuras geométricas
class SistemaGeometrico:
def __init__(self) -> None:
self.figuras: List[FiguraGeometrica] = []
def agregar_figura(self, figura: FiguraGeometrica) -> None:
"""
Agrega una figura al sistema.
Nota cómo el tipo es FiguraGeometrica, permitiendo cualquier
subclase
"""
self.figuras.append(figura)
def calcular_area_total(self) -> float:
"""Calcula el área total de todas las figuras"""
return sum(figura.calcular_area() for figura in self.figuras)
# Ejemplo de uso
def demostrar_sistema_geometrico() -> None:
# Intentar crear una instancia de la clase abstracta causará un error
try:
figura = FiguraGeometrica("rojo") # Esto causará un TypeError
except TypeError as e:
print(f"Error al intentar instanciar clase abstracta: {e}")
# Crear instancias de las clases concretas
rectangulo = Rectangulo("azul", 5, 3)
circulo = Circulo("rojo", 2)
# Crear y usar el sistema
sistema = SistemaGeometrico()
sistema.agregar_figura(rectangulo)
sistema.agregar_figura(circulo)
# Calcular y mostrar resultados
print(f"Área del rectángulo: {rectangulo.calcular_area()}")
print(f"Perímetro del círculo: {circulo.calcular_perimetro()}")
print(f"Área total en el sistema: {sistema.calcular_area_total()}")
# Usar método no abstracto heredado
print(rectangulo.describir())
print(circulo.describir())
# Ejecutar la demostración
demostrar_sistema_geometrico()
Veamos otro ejemplo más avanzado con una interfaz abstracta para un sistema de
notificaciones:
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
from datetime import datetime
class NotificacionBase(ABC):
def __init__(self) -> None:
self.fecha_creacion: datetime = datetime.now()
@abstractmethod
def enviar(self, destinatario: str, mensaje: str) -> bool:
"""Envía una notificación al destinatario"""
pass
@abstractmethod
def verificar_estado(self) -> str:
"""Verifica el estado del servicio de notificación"""
pass
# Método template (patrón Template Method)
def procesar_notificacion(self, destinatario: str, mensaje: str) ->
Dict[str, Any]:
"""
Implementa el patrón Template Method para procesar notificaciones
Este método define el esqueleto del algoritmo
"""
estado = self.verificar_estado()
if estado != "ACTIVO":
return {
"exito": False,
"error": f"Servicio no disponible. Estado: {estado}"
}
try:
resultado = self.enviar(destinatario, mensaje)
return {
"exito": resultado,
"fecha": self.fecha_creacion,
"destinatario": destinatario
}
except Exception as e:
return {
"exito": False,
"error": str(e)
}
# Implementaciones concretas
class NotificacionEmail(NotificacionBase):
def __init__(self, servidor_smtp: str) -> None:
super().__init__()
self.servidor_smtp = servidor_smtp
self._conexion_activa = True
def enviar(self, destinatario: str, mensaje: str) -> bool:
# Simulación de envío de email
print(f"Enviando email a {destinatario} vía
{self.servidor_smtp}")
return True
def verificar_estado(self) -> str:
return "ACTIVO" if self._conexion_activa else "INACTIVO"
class NotificacionSMS(NotificacionBase):
def __init__(self, proveedor_sms: str) -> None:
super().__init__()
self.proveedor_sms = proveedor_sms
self._saldo = 100 # Saldo para enviar SMS
def enviar(self, destinatario: str, mensaje: str) -> bool:
if self._saldo <= 0:
raise ValueError("Saldo insuficiente para enviar SMS")
# Simulación de envío de SMS
print(f"Enviando SMS a {destinatario} vía {self.proveedor_sms}")
self._saldo -= 1
return True
def verificar_estado(self) -> str:
return "ACTIVO" if self._saldo > 0 else "SIN_SALDO"
# Sistema que utiliza las notificaciones
class SistemaNotificaciones:
def __init__(self) -> None:
self.servicios: Dict[str, NotificacionBase] = {}
def agregar_servicio(self, nombre: str, servicio: NotificacionBase) -
> None:
self.servicios[nombre] = servicio
def notificar(self, tipo: str, destinatario: str, mensaje: str) ->
Dict[str, Any]:
servicio = self.servicios.get(tipo)
if not servicio:
return {"exito": False, "error": f"Servicio {tipo} no
encontrado"}
return servicio.procesar_notificacion(destinatario, mensaje)
# Demostración de uso
def demostrar_sistema_notificaciones() -> None:
# Crear el sistema
sistema = SistemaNotificaciones()
# Agregar servicios
email_service = NotificacionEmail("smtp.empresa.com")
sms_service = NotificacionSMS("twilioprovider")
sistema.agregar_servicio("email", email_service)
sistema.agregar_servicio("sms", sms_service)
# Enviar notificaciones
resultado_email = sistema.notificar(
"email",
"[email protected]",
"Bienvenido al sistema"
)
print(f"Resultado email: {resultado_email}")
resultado_sms = sistema.notificar(
"sms",
"+1234567890",
"Código de verificación: 123456"
)
print(f"Resultado SMS: {resultado_sms}")
# Ejecutar la demostración
demostrar_sistema_notificaciones()
Puntos importantes sobre métodos abstractos en Python:
1. Definición:
o Se usan con el decorador @abstractmethod
o Deben estar dentro de una clase que hereda de ABC
o No pueden ser instanciados directamente
2. Propósito:
o Definen una interfaz común
o Fuerzan a las clases hijas a implementar ciertos métodos
o Permiten el polimorfismo
3. Ventajas:
o Mejoran la organización del código
o Facilitan el mantenimiento
o Permiten crear sistemas extensibles