Contenido del curso
Fundamentos de Python
Los fundamentos de Python incluyen la sintaxis (sangría para bloques de código), los tipos de datos básicos (numéricos, booleanos, cadenas de texto), las variables, el control de flujo (condicionales como if/elif/else y bucles como for/while), y las funciones (bloques de código reutilizables).
0/6
Operadores y Control de Flujo
Los operadores en Python se clasifican en varios tipos (aritméticos, de comparación, lógicos, de asignación, etc.), mientras que el control de flujo se refiere al orden en que se ejecutan las instrucciones, modificándolo con estructuras como if, elif, else (condicionales), y for o while (bucles). Las instrucciones break, continue y pass también controlan el flujo dentro de los bucles.
0/3
Funciones y Manejo de Errores
Las funciones en Python son bloques de código reutilizables, mientras que el manejo de errores (excepciones) se hace con los bloques try, except, else y finally para gestionar errores de ejecución y evitar que el programa se detenga abruptamente. try ejecuta un código, except lo captura si ocurre un error específico, else se ejecuta si no hay error y finally se ejecuta siempre, haya o no error.
0/3
Estructuras de Datos
Las estructuras de datos principales en Python son las listas, tuplas, diccionarios y conjuntos. Estos tipos de datos se diferencian por su mutabilidad (si sus elementos se pueden cambiar después de su creación) y si mantienen el orden de los elementos. Las listas son ordenadas y mutables, mientras que las tuplas son ordenadas e inmutables. Los diccionarios son colecciones no ordenadas de pares clave-valor, y los conjuntos son colecciones desordenadas de elementos únicos.
0/5
Programación Orientada a Objetos (POO)
La Programación Orientada a Objetos (POO) en Python es un paradigma que organiza el código en torno a objetos, que son instancias de clases. Las clases actúan como plantillas que definen los atributos (datos) y métodos (comportamientos) de los objetos, permitiendo crear programas más modularizados, reutilizables y fáciles de mantener. Python soporta conceptos clave de la POO como la herencia, el encapsulamiento y el polimorfismo.
0/5
Ambientes virtuales
Un entorno virtual de Python es un espacio aislado que permite instalar paquetes y dependencias específicos para un proyecto concreto sin afectar a otras aplicaciones o a la instalación global de Python. Se crea una carpeta con una instalación de Python y una copia local de pip dentro de este entorno, lo que permite a cada proyecto tener sus propias bibliotecas y versiones, evitando así conflictos entre diferentes proyectos que puedan requerir versiones distintas de la misma librería.
0/1
Archivos
El manejo de archivos en Python se realiza principalmente usando la función open() para abrir un archivo y los métodos read(), write(), append() y close() para manipularlo. Es crucial gestionar los archivos adecuadamente, cerrándolos para liberar recursos, aunque es más recomendable usar la sentencia with, que cierra el archivo automáticamente. Python permite trabajar con archivos de texto y binarios, así como con distintos modos de apertura como 'r' (solo lectura), 'w' (escritura/sobreescritura), y 'a' (añadir).
0/1
Módulos y Librerías Estándar
Un "módulo" en Python se refiere a dos conceptos distintos: un archivo .py con código que se puede importar para reutilizar funciones, clases y variables, y el operador % que calcula el residuo de una división entera. Ambos son útiles para organizar el código y resolver problemas matemáticos, respectivamente.
0/2
Hilos y tareas en Python
En Python, los hilos (threads) son secuencias de ejecución dentro de un proceso que permiten la concurrencia, ejecutando tareas simultáneamente para aprovechar mejor los recursos del sistema. Las tareas son las unidades de trabajo a realizar, como descargar archivos o procesar datos. Se utilizan para manejar operaciones que implican espera (I/O-bound) de forma eficiente, permitiendo que una aplicación no se bloquee mientras espera. Para ello, se usa el módulo threading, se crean objetos Thread que representan las tareas, se inician con .start() y se pueden sincronizar con mecanismos como Lock para evitar conflictos.
0/1
Curso de Programación en Pythón 3.

    📘 Lección 5.1 — Clases y Objetos en Python

    1. Parte 1 — Introducción a las clases y objetos
    2. Parte 2 — Constructor __init__ y creación de objetos
    3. Parte 3 — Atributos: instancia, clase y propiedades
    4. Parte 4 — Métodos de instancia y de clase
    5. Parte 5 — Métodos especiales (__str__, __repr__) y buenas prácticas

    Haz clic en cada parte para navegar. Este módulo introduce los fundamentos de la Programación Orientada a Objetos (POO) en Python, desde la teoría hasta la práctica con ejemplos claros y ejercicios guiados.

    Parte 1 — Introducción a las clases y objetos

    Objetivo: comprender qué son las clases y los objetos en Python, por qué el enfoque orientado a objetos organiza mejor el código, y cómo dar los primeros pasos creando instancias.


    1) ¿Qué es una clase? ¿Qué es un objeto?

    En POO, una clase es como un plano: define las características (atributos) y los comportamientos (métodos) que tendrán los objetos. Un objeto (o instancia) es una realización concreta de ese plano. Es decir, con el plano Coche puedes crear múltiples coches reales con colores o kilometrajes distintos. 

    # Plano (clase)
    class Coche:
        pass
    
    # Casas construidas (objetos)
    mi_coche = Coche()
    coche_de_amigo = Coche()
    

    Aunque ambos objetos provengan de la misma clase, cada uno tendrá sus valores propios para los atributos que definamos más adelante (color, modelo, etc.). 

    Analogía rápida: Clase = plano. Objeto = casa construida desde ese plano. Cada casa (objeto) puede pintarse de un color distinto.

    2) ¿Por qué usar POO?

    • Organización: agrupa datos y comportamientos relacionados en un único módulo lógico (la clase).
    • Reutilización: define una vez, crea muchas instancias sin repetir código.
    • Modularidad: puedes cambiar la implementación sin romper a quien usa la clase (si mantienes la interfaz).
    • Modelado natural del mundo real: mapea entidades (Libro, Persona, Cuenta) con facilidad.
    Abstracción = te centras en qué hace un objeto, no en cómo lo hace. Encapsulamiento = agrupas datos+métodos y controlas el acceso.

    3) Primer contacto: clase mínima y objetos

    Definir una clase en Python es directo: class NombreDeLaClase:. Luego puedes instanciar llamando a la clase como si fuera una función.

    class Libro:
        # Aquí más adelante añadiremos atributos y métodos
        pass
    
    # Dos objetos (instancias) distintos
    libro_python = Libro()
    novela_fantasia = Libro()
    

    La clase Libro actúa como plantilla; cada objeto representará un libro concreto.


    4) Añadiendo atributos y comportamiento (vista previa)

    Los atributos son datos asociados al objeto (p. ej., titulo, autor), mientras que los métodos son funciones que describen su comportamiento (p. ej., abrir(), leer()). Profundizamos en esto en las Partes 2–5; aquí una vista conceptual.

    class Libro:
        def __init__(self, titulo, autor, paginas):
            self.titulo = titulo
            self.autor = autor
            self.paginas = paginas
            self.pagina_actual = 0
            self.abierto = False
    
        def abrir(self):
            if self.abierto:
                return f"{self.titulo} ya está abierto"
            self.abierto = True
            return f"{self.titulo} ha sido abierto"
    
        def leer(self, n):
            if not self.abierto:
                return f"No puedes leer: {self.titulo} está cerrado"
            self.pagina_actual = min(self.pagina_actual + n, self.paginas)
            if self.pagina_actual == self.paginas:
                return f"Has terminado {self.titulo}"
            return f"Página {self.pagina_actual}/{self.paginas}"
    
        def __str__(self):
            estado = "abierto" if self.abierto else "cerrado"
            return f"{self.titulo} por {self.autor} — {estado}"
    

    Este patrón integra constructor __init__, métodos de instancia y un método especial __str__ para imprimir amigable.


    5) Tabla de conceptos clave

    ConceptoQué esEjemplo
    ClasePlantilla (plano) que define atributos y métodos.class Coche: ...
    ObjetoInstancia concreta de una clase con valores propios.mi_coche = Coche()
    AtributoDato asociado (estado) del objeto.self.titulo = "El Quijote"
    MétodoFunción de la clase que define comportamiento.def leer(self, n): ...
    Abstracción/EncapsulamientoCentrarse en el qué; agrupar y controlar acceso.Modelo Libro oculta detalles internos.

    6) Mini-práctica guiada

    1. Crea la clase vacía Persona y dos objetos:
      class Persona:
          pass
      
      ana = Persona()
      juan = Persona()

      Compara ana is juan (debe ser False): son instancias distintas del mismo plano.

    2. Añade un constructor y prueba atributos:
      class Persona:
          def __init__(self, nombre, edad):
              self.nombre = nombre
              self.edad = edad
      
      ana = Persona("Ana García", 28)
      juan = Persona("Juan López", 35)
      print(ana.nombre, juan.edad)

      Cada objeto guarda sus propios valores.

    3. Añade un método simple y llámalo:
      class Persona:
          def __init__(self, nombre, edad):
              self.nombre = nombre
              self.edad = edad
          def presentarse(self):
              return f"Hola, soy {self.nombre} y tengo {self.edad} años."
      
      print(ana.presentarse())

      Los métodos definen el comportamiento (acción) del objeto.


    7) Preguntas rápidas de chequeo

    • ¿Qué diferencia hay entre clase y objeto en tu propio wording?
    • ¿Por qué la POO mejora organización y reutilización en tu proyecto? Da un
    • ¿Qué significa “abstraer” al modelar una entidad?
    En la Parte 2 entraremos en el constructor __init__, el parámetro self, cómo inicializar atributos (con valores por defecto, cálculos y validaciones) y cómo crear instancias correctamente.

    Parte 2 — Constructor __init__ y creación de objetos

    Objetivo: dominar el constructor __init__, entender el uso de self, inicializar atributos de forma segura (con valores por defecto y validaciones) y crear instancias correctamente.


    1) ¿Qué es el constructor __init__?

    El constructor es un método especial que se ejecuta automáticamente al crear un objeto (instancia) de una clase. Su función es inicializar el estado del objeto (sus atributos).

    class Libro:
        def __init__(self, titulo, autor, paginas):
            # "self" es la propia instancia recién creada
            self.titulo = titulo
            self.autor = autor
            self.paginas = paginas
            self.pagina_actual = 0   # atributo con valor por defecto
    
    # Crear instancias (objetos)
    l1 = Libro("El Quijote", "Cervantes", 863)
    l2 = Libro("1984", "Orwell", 328)
    print(l1.titulo, l2.autor)
    Regla mental: __init__ prepara el objeto para ser usado: fija atributos mínimos y deja el estado consistente.

    2) El parámetro self: la referencia al objeto

    self es el primer parámetro de los métodos de instancia; representa la propia instancia. Con self.atributo guardas datos dentro del objeto.

    class Persona:
        def __init__(self, nombre):
            self.nombre = nombre   # atributo de instancia
    
        def saludar(self):
            return f"Hola, soy {self.nombre}"
    
    p = Persona("Ana")
    print(p.saludar())  # Hola, soy Ana
    Antipatrón: olvidar self en la definición del método o acceder a variables locales en vez de self.atributo.

    3) Atributos en el constructor: obligatorios y por defecto

    Diseña __init__ pensando en lo mínimo imprescindible y lo opcional con valores por defecto.

    class Usuario:
        def __init__(self, nombre, email, activo=True, pais="ES"):
            self.nombre = nombre          # obligatorio
            self.email = email            # obligatorio
            self.activo = activo          # por defecto True
            self.pais = pais              # por defecto "ES"
    
    u = Usuario("Javier", "javi@example.com")
    print(u.activo, u.pais)  # True ES

    3.1 Validaciones mínimas

    Valida entradas para evitar objetos inválidos (fail-fast).

    class CuentaBancaria:
        def __init__(self, titular, saldo_inicial=0.0):
            if not titular or not isinstance(titular, str):
                raise ValueError("Titular inválido")
            if saldo_inicial < 0:
                raise ValueError("El saldo no puede ser negativo")
            self.titular = titular
            self.saldo = float(saldo_inicial)

    3.2 Conversión de tipos (normalización)

    class Producto:
        def __init__(self, nombre, precio):
            self.nombre = str(nombre).strip()
            self.precio = float(precio)   # normaliza a float

    4) Tabla de patrones de inicialización

    PatrónUsoEjemplo
    Obligatorios + opcionalesParámetros mínimos + defaults sensatos__init__(a, b, opt=True)
    ValidaciónEvitar estados inválidosif x < 0: raise ValueError
    NormalizaciónConvertir/limpiar entradasself.precio = float(p)
    Atributos derivadosCalcular campos a partir de otrosself.slug = nombre.lower()

    5) Creación de objetos (instanciación) paso a paso

    1. Python reserva memoria para la nueva instancia.
    2. Se llama a __init__(self, ...) con los argumentos que pasaste.
    3. Dentro de __init__ se inicializan atributos en self.
    4. Se devuelve la instancia lista para usar.
    class Ticket:
        def __init__(self, id, prioridad="media"):
            self.id = id
            self.prioridad = prioridad
            self.resuelto = False
    
    t1 = Ticket(101)                 # prioridad por defecto
    t2 = Ticket(102, prioridad="alta")
    print(t1.prioridad, t2.prioridad)  # media alta

    6) Errores comunes y cómo evitarlos

    • Olvidar self en la firma de __init__ o métodos de instancia.
    • No usar self. al guardar atributos (quedan como variables locales y se pierden).
    • Mutables como valor por defecto (p. ej. lista=[]): comparten estado entre instancias. Usa None y crea una nueva colección dentro.
    # ❌ Peligroso: mutable como default
    class Carrito:
        def __init__(self, items=[]):
            self.items = items
    
    c1 = Carrito()
    c2 = Carrito()
    c1.items.append("laptop")
    print(c2.items)  # "laptop" aparece en ambos (sorpresa)
    
    # ✅ Seguro: usar None y crear la lista adentro
    class CarritoSeguro:
        def __init__(self, items=None):
            self.items = list(items) if items is not None else []

    7) Buenas prácticas de diseño del constructor

    • Mínimos imprescindibles: pide solo lo necesario; el resto con valores por defecto.
    • Validación ligera: comprueba lo crítico y falla pronto con mensajes claros.
    • Coherencia de nombres: usa nombres de atributos autoexplicativos.
    • Documenta: añade docstring en la clase explicando parámetros y efectos.
    • Tipos (opcional): puedes añadir type hints para mayor claridad.
    class Pedido:
        """Representa un pedido simple.
    
        Args:
            ref (str): Referencia única.
            unidades (int): Cantidad pedida (> 0).
            precio_unitario (float): Precio por unidad (>= 0).
        """
        def __init__(self, ref: str, unidades: int, precio_unitario: float):
            if unidades <= 0:
                raise ValueError("unidades debe ser > 0")
            if precio_unitario < 0:
                raise ValueError("precio_unitario debe ser >= 0")
            self.ref = ref
            self.unidades = int(unidades)
            self.precio_unitario = float(precio_unitario)
    
        def total(self) -> float:
            return self.unidades * self.precio_unitario

    8) Caso práctico integrado

    Construimos una clase con defaults, validación y un método operativo.

    class Alumno:
        def __init__(self, nombre, curso, nota=0.0):
            if not nombre or not curso:
                raise ValueError("nombre y curso son obligatorios")
            if not (0.0 <= nota <= 10.0):
                raise ValueError("nota fuera de rango (0..10)")
            self.nombre = nombre.strip().title()
            self.curso = curso.upper()
            self.nota = float(nota)
    
        def calificacion(self):
            if self.nota >= 9:
                return "Sobresaliente"
            if self.nota >= 7:
                return "Notable"
            if self.nota >= 5:
                return "Aprobado"
            return "Suspenso"
    
    a1 = Alumno("ana pérez", "python", 8.5)
    print(a1.nombre, a1.curso, a1.calificacion())

    9) Checklist rápido (calidad del __init__)

    • ¿Los parámetros mínimos están claros y ordenados?
    • ¿Defaults razonables para lo no esencial?
    • ¿Validaciones clave y mensajes útiles?
    • ¿Sin mutables como valores por defecto?
    • ¿Atributos escritos siempre como self.x?

    10) Mini-ejercicios

    1. ProductoSeguro: crea una clase con nombre (str), precio (float >= 0) y tags (lista opcional). Evita el pitfall de mutables por defecto.
      class ProductoSeguro:
          def __init__(self, nombre, precio, tags=None):
              if precio < 0:
                  raise ValueError("precio >= 0")
              self.nombre = str(nombre)
              self.precio = float(precio)
              self.tags = list(tags) if tags is not None else []
    2. Sesion: almacena usuario y expira_en (minutos, > 0). Normaliza a int y valida rango.
      class Sesion:
          def __init__(self, usuario, expira_en):
              expira_en = int(expira_en)
              if expira_en <= 0:
                  raise ValueError("expira_en debe ser > 0")
              self.usuario = usuario
              self.expira_en = expira_en
    3. Persona con nombre y edad: limpia espacios en nombre y limita edad a 0..120.
      class Persona:
          def __init__(self, nombre, edad):
              nombre = str(nombre).strip()
              edad = int(edad)
              if not nombre:
                  raise ValueError("nombre requerido")
              if not (0 <= edad <= 120):
                  raise ValueError("edad fuera de rango")
              self.nombre = nombre.title()
              self.edad = edad
    En la Parte 3 profundizaremos en atributos (de instancia y de clase) y en cómo exponer/encapsular estado con propiedades.

    Parte 3 — Atributos: instancia, clase y propiedades

    Objetivo: comprender los diferentes tipos de atributos (de instancia, de clase y propiedades), cómo se definen, cómo se accede a ellos y buenas prácticas para mantener el código claro y seguro.


    1) Atributos de instancia

    Los atributos de instancia pertenecen a cada objeto de forma individual. Se definen normalmente dentro del constructor __init__ mediante self.

    class Coche:
        def __init__(self, marca, color):
            self.marca = marca      # atributo de instancia
            self.color = color      # atributo de instancia
    
    c1 = Coche("Toyota", "rojo")
    c2 = Coche("Ford", "azul")
    
    print(c1.marca, c2.marca)  # Toyota Ford

    Cada objeto (c1, c2) tiene sus propios valores de marca y color, independientes uno del otro.

    Clave: los atributos de instancia viven en el namespace del objeto (obj.__dict__), no en la clase.
    print(c1.__dict__)
    # {'marca': 'Toyota', 'color': 'rojo'}

    2) Atributos de clase

    Los atributos de clase son compartidos por todas las instancias. Se definen directamente dentro del bloque de la clase (fuera de los métodos).

    class Coche:
        ruedas = 4  # atributo de clase compartido
    
        def __init__(self, marca):
            self.marca = marca
    
    a = Coche("Seat")
    b = Coche("Renault")
    
    print(a.ruedas, b.ruedas)  # 4 4
    Coche.ruedas = 5           # cambia para todos
    print(a.ruedas, b.ruedas)  # 5 5

    Los atributos de clase viven en el namespace de la clase, y son comunes a todas las instancias (a menos que una instancia defina su propio atributo con el mismo nombre).

    TipoDefiniciónAlcanceEjemplo
    InstanciaDentro de __init__Cada objetoself.nombre
    ClaseFuera de métodosCompartido por todosCoche.ruedas

    3) Precedencia y sombreado

    Si un objeto tiene un atributo con el mismo nombre que uno de clase, el de la instancia “sombrea” al de clase.

    class Persona:
        especie = "Humano"
    
    p1 = Persona()
    p2 = Persona()
    p1.especie = "Cyborg"  # crea atributo de instancia
    
    print(p1.especie)  # Cyborg
    print(p2.especie)  # Humano
    print(Persona.especie)  # Humano

    Modificar Persona.especie afecta solo al valor compartido, no a las instancias que lo hayan sobrescrito.


    4) Atributos dinámicos

    Python permite añadir o eliminar atributos a objetos existentes en tiempo de ejecución (dinámicamente). Aunque flexible, debe usarse con cuidado para mantener coherencia.

    class Animal:
        pass
    
    a = Animal()
    a.nombre = "Toby"
    a.tipo = "Perro"
    print(a.nombre, a.tipo)
    
    del a.tipo  # se puede eliminar
    print(a.__dict__)

    Este comportamiento flexible es potente, pero puede generar errores difíciles de detectar si se abusa.

    ⚠️ Recomendación: define siempre los atributos dentro de __init__ para mayor claridad y previsibilidad.

    5) Propiedades: control de acceso con @property

    Las propiedades permiten controlar el acceso a los atributos (lectura, escritura y borrado) mediante decoradores. Son útiles para validar, transformar o proteger datos internos sin cambiar la interfaz pública.

    class Cuenta:
        def __init__(self, titular, saldo):
            self.titular = titular
            self._saldo = saldo   # atributo “protegido” (por convención)
    
        @property
        def saldo(self):
            """Obtiene el saldo actual"""
            return self._saldo
    
        @saldo.setter
        def saldo(self, valor):
            if valor < 0:
                raise ValueError("El saldo no puede ser negativo")
            self._saldo = valor
    
    c = Cuenta("Ana", 1000)
    c.saldo += 200
    print(c.saldo)
    # c.saldo = -50  # ValueError

    Así, el atributo _saldo sigue siendo interno, pero se accede como si fuera público (c.saldo), con validaciones automáticas.

    DecoradorFunciónEjemplo
    @propertyDefinir lectura controladadef saldo(self):
    @atributo.setterDefinir escritura controladadef saldo(self, valor):
    @atributo.deleterDefinir borrado controladodef saldo(self): del self._saldo

    6) Propiedades de solo lectura

    Si defines solo el getter y omites el setter, el atributo será de solo lectura.

    class Rectangulo:
        def __init__(self, ancho, alto):
            self.ancho = ancho
            self.alto = alto
    
        @property
        def area(self):
            return self.ancho * self.alto
    
    r = Rectangulo(10, 5)
    print(r.area)   # 50
    # r.area = 60   # Error: no tiene setter

    7) Encapsulamiento: convención de nombres

    Python no tiene atributos realmente privados, pero usa prefijos por convención:

    PrefijoSignificadoEjemplo
    Sin prefijoPúbliconombre
    _guionProtegido (interno, no forzar acceso)_saldo
    __doble_guionPrivado (name mangling interno)__clave_Clase__clave

    8) Buenas prácticas con atributos y propiedades

    • Define todos los atributos de instancia en __init__ (evita definirlos fuera).
    • Usa @property para controlar lectura/escritura si se requiere validación.
    • Evita exponer atributos internos directamente (_atributo).
    • Prefiere claridad: no abuses de __doble_guion salvo para evitar colisiones de nombres.

    9) Mini-ejercicios

    1. Crea una clase Empleado con atributos de clase empresa = "TechCorp" y de instancia nombre y salario. Luego imprime el nombre y la empresa de dos empleados distintos.
      class Empleado:
          empresa = "TechCorp"
      
          def __init__(self, nombre, salario):
              self.nombre = nombre
              self.salario = salario
      
      e1 = Empleado("Ana", 2500)
      e2 = Empleado("Luis", 3000)
      print(e1.nombre, e1.empresa)
      print(e2.nombre, e2.empresa)
    2. Implementa una clase Producto con atributo _precio y propiedad precio que impida asignar valores negativos.
      class Producto:
          def __init__(self, nombre, precio):
              self.nombre = nombre
              self._precio = precio
      
          @property
          def precio(self):
              return self._precio
      
          @precio.setter
          def precio(self, valor):
              if valor < 0:
                  raise ValueError("Precio inválido")
              self._precio = valor
    3. Diseña una clase Rectangulo que tenga atributos ancho y alto y una propiedad area (solo lectura).
    En la Parte 4 veremos los métodos de instancia y de clase, junto con @classmethod y @staticmethod, aprendiendo cuándo y cómo usarlos correctamente.

    Parte 4 — Métodos de instancia y de clase

    Objetivo: entender las diferencias entre los métodos de instancia, de clase y estáticos, así como las situaciones prácticas donde cada uno es más apropiado.


    1) Métodos de instancia

    Son los más comunes. Se definen con def y su primer parámetro es siempre self. Pueden acceder y modificar los atributos del objeto.

    class Persona:
        def __init__(self, nombre, edad):
            self.nombre = nombre
            self.edad = edad
    
        def saludar(self):
            return f"Hola, soy {self.nombre} y tengo {self.edad} años."
    
    p1 = Persona("Ana", 30)
    print(p1.saludar())

    Este método actúa sobre self (la instancia concreta). Si modificamos un atributo dentro del método, cambia solo en ese objeto.

    p1.edad += 1
    print(p1.saludar())  # Hola, soy Ana y tengo 31 años
    Regla práctica: usa métodos de instancia cuando necesites acceder o modificar datos del objeto (self).

    2) Métodos de clase (@classmethod)

    Un método de clase recibe cls como primer parámetro en lugar de self. Se asocia a la clase en sí, no a un objeto particular.

    class Empleado:
        empresa = "TechCorp"
        contador = 0
    
        def __init__(self, nombre):
            self.nombre = nombre
            Empleado.contador += 1
    
        @classmethod
        def total_empleados(cls):
            return f"Hay {cls.contador} empleados en {cls.empresa}."
    
    e1 = Empleado("Carlos")
    e2 = Empleado("Lucía")
    print(Empleado.total_empleados())

    Aquí, el método total_empleados() accede a un atributo de clase (cls.contador), compartido por todas las instancias. Se puede invocar tanto desde la clase como desde una instancia.

    print(e1.total_empleados())  # También válido
    TipoPrimer parámetroAccesoEjemplo de uso
    InstanciaselfAtributos del objetop1.saludar()
    ClaseclsAtributos de claseEmpleado.total_empleados()

    3) Uso práctico de los métodos de clase

    Son ideales para crear constructores alternativos o comportamientos que afectan a la clase completa.

    class Fecha:
        def __init__(self, dia, mes, anio):
            self.dia = dia
            self.mes = mes
            self.anio = anio
    
        @classmethod
        def desde_cadena(cls, cadena):
            d, m, a = map(int, cadena.split("/"))
            return cls(d, m, a)   # crea una nueva instancia
    
    f = Fecha.desde_cadena("18/10/2025")
    print(f.dia, f.mes, f.anio)

    El método desde_cadena() actúa como un constructor alternativo, muy usado para inicializar objetos desde diferentes formatos o fuentes de datos (archivos, JSON, cadenas, etc.).


    4) Métodos estáticos (@staticmethod)

    Un método estático pertenece a la clase, pero no recibe ni self ni cls. Es una simple función agrupada dentro de la clase por organización o coherencia semántica.

    class Calculadora:
        @staticmethod
        def sumar(a, b):
            return a + b
    
        @staticmethod
        def es_par(n):
            return n % 2 == 0
    
    print(Calculadora.sumar(3, 7))
    print(Calculadora.es_par(10))

    Se usan cuando una función tiene relación conceptual con la clase, pero no necesita acceder a los datos del objeto ni a los atributos de clase.


    5) Diferencias clave entre los tres tipos

    TipoDecoradorPrimer argumentoAcceso permitidoEjemplo
    InstanciaselfAtributos del objetoobj.metodo()
    Clase@classmethodclsAtributos de claseClase.metodo()
    Estático@staticmethodNinguno (solo argumentos pasados)Clase.utilidad(x)

    6) Combinación práctica en una sola clase

    Veamos una clase que mezcla los tres tipos para distintos fines.

    class Temperatura:
        factor_c_f = 9 / 5  # atributo de clase
    
        def __init__(self, celsius):
            self.celsius = celsius
    
        def a_fahrenheit(self):  # método de instancia
            return self.celsius * self.factor_c_f + 32
    
        @classmethod
        def desde_fahrenheit(cls, f):
            c = (f - 32) / cls.factor_c_f
            return cls(c)  # devuelve una nueva instancia
    
        @staticmethod
        def es_temperatura_valida(valor):
            return -273.15 <= valor < 6000
    
    t1 = Temperatura(25)
    print(t1.a_fahrenheit())
    t2 = Temperatura.desde_fahrenheit(98.6)
    print(t2.celsius)
    print(Temperatura.es_temperatura_valida(-500))
    • a_fahrenheit(): usa self → método de instancia.
    • desde_fahrenheit(): usa cls → método de clase (constructor alternativo).
    • es_temperatura_valida(): independiente → método estático.
    💡 Buenas prácticas:

    • Si accedes o modificas datos del objeto → usa self.
    • Si trabajas con la clase en general → usa @classmethod.
    • Si es una utilidad genérica → usa @staticmethod.

    7) Ejercicios rápidos

      1. Crea una clase Circulo con atributo de clase PI = 3.1416 y método de instancia area() que calcule el área a partir del radio.
      2. Añade un @classmethod llamado desde_diametro() que cree un círculo a partir de su diámetro.
      3. Añade un @staticmethod llamado es_valido() que devuelva True si el radio es positivo.
    class Circulo:
        PI = 3.1416
    
        def __init__(self, radio):
            self.radio = radio
    
        def area(self):
            return Circulo.PI * (self.radio ** 2)
    
        @classmethod
        def desde_diametro(cls, diametro):
            return cls(diametro / 2)
    
        @staticmethod
        def es_valido(radio):
            return radio > 0

    8) Tabla resumen

    Tipo de métodoAcceso aDecoradorCaso típico
    InstanciaDatos del objeto(ninguno)Modificar atributos del objeto
    ClaseDatos de clase@classmethodCrear constructores alternativos
    EstáticoNinguno@staticmethodFunciones auxiliares relacionadas
    En la Parte 5 veremos los métodos especiales de Python, como __str__ y __repr__, para representar objetos de forma más legible y profesional.

    Parte 5 — Métodos especiales (__str__, __repr__) y buenas prácticas

    Objetivo: aprender a representar objetos de forma legible con los métodos especiales __str__ y __repr__, y aplicar las mejores prácticas de diseño orientado a objetos en Python.


    1) ¿Qué son los métodos especiales?

    Los métodos especiales (también llamados métodos mágicos o dunder methods, por su doble guion bajo) son los que comienzan y terminan con __, como __init__, __len__ o __str__. Python los usa internamente para definir comportamientos de los objetos. Puedes sobreescribirlos para personalizar cómo se comportan tus clases.

    class Libro:
        def __init__(self, titulo, autor):
            self.titulo = titulo
            self.autor = autor
    
        def __str__(self):
            return f"'{self.titulo}' de {self.autor}"
    
        def __repr__(self):
            return f"Libro(titulo={self.titulo!r}, autor={self.autor!r})"
    
    l = Libro("1984", "George Orwell")
    print(str(l))   # '1984' de George Orwell
    print(repr(l))  # Libro(titulo='1984', autor='George Orwell')

    2) __str__ — Representación legible para el usuario

    Este método define cómo se muestra el objeto cuando se imprime con print() o se convierte con str(). Su objetivo es ser amigable y entendible para el usuario final.

    class Persona:
        def __init__(self, nombre, edad):
            self.nombre = nombre
            self.edad = edad
    
        def __str__(self):
            return f"Persona: {self.nombre}, {self.edad} años"
    
    p = Persona("Ana", 30)
    print(p)  # Persona: Ana, 30 años
    💡 Consejo: __str__ debe ser simple, claro y orientado a presentación, no a depuración.

    3) __repr__ — Representación técnica o de depuración

    El método __repr__ define cómo se muestra el objeto al usarlo en consola o dentro de listas, diccionarios o depuradores. Su objetivo es ser preciso y sin ambigüedad: idealmente, el texto debería poder recrear el objeto.

    class Persona:
        def __init__(self, nombre, edad):
            self.nombre = nombre
            self.edad = edad
    
        def __repr__(self):
            return f"Persona({self.nombre!r}, {self.edad!r})"
    
    p = Persona("Carlos", 25)
    print(repr(p))  # Persona('Carlos', 25)

    Fíjate en el uso de !r en las f-strings: fuerza el formato de repr() sobre los valores.


    4) Diferencias entre __str__ y __repr__

    MétodoPropósitoOrientado aEjemplo típico
    __str__Representación legible para humanosUsuario final«Persona: Ana, 30 años»
    __repr__Representación técnica sin ambigüedadDesarrollador / Depuración«Persona(‘Ana’, 30)»

    5) Cómo interactúan ambos métodos

    • Si solo defines __repr__, Python lo usará también como __str__ por defecto.
    • Si defines ambos, print() usará __str__ y la consola usará __repr__.
    class Producto:
        def __init__(self, nombre, precio):
            self.nombre = nombre
            self.precio = precio
    
        def __repr__(self):
            return f"Producto({self.nombre!r}, {self.precio!r})"
    
    p = Producto("Ratón", 15.99)
    print(p)       # Producto('Ratón', 15.99)
    print([p])     # [Producto('Ratón', 15.99)]

    6) Otros métodos especiales útiles

    Algunos métodos mágicos relacionados que vale la pena conocer:

    MétodoFunciónEjemplo de uso
    __len__Define el resultado de len(obj)len(mi_lista)
    __eq__Compara objetos con ==a == b
    __lt__Ordena con <, >p1 < p2
    __add__Define la suma con +a + b
    class Vector:
        def __init__(self, x, y):
            self.x, self.y = x, y
    
        def __add__(self, other):
            return Vector(self.x + other.x, self.y + other.y)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v1 = Vector(1, 2)
    v2 = Vector(3, 4)
    print(v1 + v2)  # Vector(4, 6)

    7) Buenas prácticas con __str__ y __repr__

    • Siempre devuelve una cadena (str), nunca print() dentro del método.
    • Haz que __repr__ sea lo más completo posible, idealmente reconstruible: eval(repr(obj)) debería funcionar si es viable.
    • Haz que __str__ sea legible y descriptivo.
    • No mezcles lógica pesada o llamadas a red en estos métodos: deben ser rápidos.
    • Cuando sea posible, usa !r en f-strings dentro de __repr__ para garantizar formato técnico.

    8) Caso práctico: sistema de inventario

    Una clase Producto con ambos métodos bien definidos:

    class Producto:
        def __init__(self, codigo, nombre, precio):
            self.codigo = codigo
            self.nombre = nombre
            self.precio = precio
    
        def __str__(self):
            return f"{self.nombre} — {self.precio:.2f} €"
    
        def __repr__(self):
            return f"Producto({self.codigo!r}, {self.nombre!r}, {self.precio!r})"
    
    productos = [
        Producto(101, "Teclado", 29.99),
        Producto(102, "Ratón", 15.99)
    ]
    
    print(productos[0])   # Teclado — 29.99 €
    print(productos)      # [Producto(101, 'Teclado', 29.99), Producto(102, 'Ratón', 15.99)]

    Este patrón se usa en la mayoría de clases del mundo real (modelos, entidades, DTOs, etc.).


    9) Ejercicios propuestos

    1. Implementa una clase Alumno con nombre y nota.
      • __str__: devuelve "Alumno: Ana (Nota: 8.5)"
      • __repr__: devuelve "Alumno('Ana', 8.5)"
    2. Crea una clase Rectangulo con ancho y alto.
      • __repr__ debe devolver un texto que permita reconstruirlo.
      • __str__ debe mostrar su área de forma legible.
    3. Diseña una clase Pedido con atributos cliente y importe.
      • __str__: “Pedido de {cliente} por {importe}€”
      • __repr__: “Pedido({cliente!r}, {importe!r})”

    10) Resumen final del módulo 5.1

    • Una clase define el modelo; los objetos son sus instancias.
    • __init__ inicializa; self representa a la instancia actual.
    • Hay tres tipos de atributos: instancia, clase y propiedad.
    • Hay tres tipos de métodos: instancia, clase (@classmethod) y estático (@staticmethod).
    • __str__ y __repr__ mejoran la legibilidad, la depuración y la calidad del código.
    Conclusión: Dominar las clases, atributos, métodos y sus representaciones te convierte en un desarrollador Python profesional y preparado para construir proyectos limpios, mantenibles y escalables.
    Fin de la Lección 5.1 — Clases y Objetos. Próximo tema: Encapsulación — ampliando el poder de la Programación Orientada a Objetos.
    Nuestra puntuación
    ¡Haz clic para puntuar esta entrada!
    (Votos: 0 Promedio: 0)
    Scroll al inicio