{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ef2c5262",
   "metadata": {},
   "source": [
    "[← Anterior](06_errores_y_excepciones.ipynb) · [Índice](README.md) · [Siguiente →](08_numpy.ipynb)\n",
    "\n",
    "# 07 · Clases y programación orientada a objetos\n",
    "\n",
    "**Audiencia.** Personas que ya han visto algo de programación y quieren repasar Python de forma práctica.  \n",
    "**Prerrequisitos.** Funciones, colecciones, excepciones y anotaciones de tipo básicas.  \n",
    "**Duración orientativa.** 60–90 minutos, incluida la práctica.\n",
    "\n",
    "## Objetivos de aprendizaje\n",
    "\n",
    "- Modelar estado y comportamiento con clases e instancias.\n",
    "- Usar propiedades, métodos de clase y métodos estáticos.\n",
    "- Aplicar herencia, polimorfismo y composición con criterio.\n",
    "- Reducir código repetitivo con `dataclass` y métodos especiales.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bfc99ab5",
   "metadata": {},
   "source": [
    "## Itinerario\n",
    "\n",
    "1. Clases e instancias\n",
    "2. Atributos y métodos\n",
    "3. Propiedades\n",
    "4. Métodos alternativos\n",
    "5. Herencia\n",
    "6. Composición\n",
    "7. Dataclasses\n",
    "8. Práctica\n",
    "\n",
    "> **Cómo trabajar:** ejecuta las celdas en orden, predice el resultado antes de verlo y modifica los ejemplos. Cada bloque termina con un ejercicio, un punto de partida y una solución posible.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51d714f1",
   "metadata": {},
   "source": [
    "## 1. Clases e instancias\n",
    "\n",
    "Una clase define un nuevo tipo; una instancia es un objeto concreto. `__init__` inicializa el estado. `self` es la instancia sobre la que actúa el método y se pasa automáticamente al llamar.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c1ea0212",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.559191Z",
     "iopub.status.busy": "2026-08-02T22:29:54.559072Z",
     "iopub.status.idle": "2026-08-02T22:29:54.563361Z",
     "shell.execute_reply": "2026-08-02T22:29:54.563098Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Alicia 135.5\n"
     ]
    }
   ],
   "source": [
    "class Cuenta:\n",
    "    def __init__(self, titular, saldo=0.0):\n",
    "        self.titular = titular\n",
    "        self.saldo = float(saldo)\n",
    "\n",
    "    def ingresar(self, importe):\n",
    "        if importe <= 0:\n",
    "            raise ValueError(\"el importe debe ser positivo\")\n",
    "        self.saldo += importe\n",
    "        return self.saldo\n",
    "\n",
    "cuenta = Cuenta(\"Alicia\", 100)\n",
    "cuenta.ingresar(35.5)\n",
    "print(cuenta.titular, cuenta.saldo)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b22fe8ba",
   "metadata": {},
   "source": [
    "## 2. Atributos de instancia y de clase\n",
    "\n",
    "Los atributos de instancia pertenecen a cada objeto. Los de clase se comparten y son adecuados para constantes o contadores globales del tipo, no para estado mutable individual.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c070bccf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.564931Z",
     "iopub.status.busy": "2026-08-02T22:29:54.564815Z",
     "iopub.status.idle": "2026-08-02T22:29:54.567427Z",
     "shell.execute_reply": "2026-08-02T22:29:54.567174Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "60.5 2\n"
     ]
    }
   ],
   "source": [
    "class Producto:\n",
    "    impuesto = 0.21\n",
    "    creados = 0\n",
    "\n",
    "    def __init__(self, nombre, precio):\n",
    "        self.nombre = nombre\n",
    "        self.precio = float(precio)\n",
    "        type(self).creados += 1\n",
    "\n",
    "    def precio_final(self):\n",
    "        return self.precio * (1 + self.impuesto)\n",
    "\n",
    "teclado = Producto(\"Teclado\", 50)\n",
    "raton = Producto(\"Ratón\", 20)\n",
    "print(teclado.precio_final(), Producto.creados)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a0c838c",
   "metadata": {},
   "source": [
    "## 3. Encapsulación y propiedades\n",
    "\n",
    "Python se apoya en convenciones: `_nombre` indica uso interno. Una propiedad expone una interfaz de atributo mientras valida o calcula. No hace falta crear getters y setters para todo desde el principio.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "dc52c47c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.568893Z",
     "iopub.status.busy": "2026-08-02T22:29:54.568780Z",
     "iopub.status.idle": "2026-08-02T22:29:54.571385Z",
     "shell.execute_reply": "2026-08-02T22:29:54.571143Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "20.0 68.0\n"
     ]
    }
   ],
   "source": [
    "class Temperatura:\n",
    "    def __init__(self, celsius):\n",
    "        self.celsius = celsius\n",
    "\n",
    "    @property\n",
    "    def celsius(self):\n",
    "        return self._celsius\n",
    "\n",
    "    @celsius.setter\n",
    "    def celsius(self, valor):\n",
    "        valor = float(valor)\n",
    "        if valor < -273.15:\n",
    "            raise ValueError(\"temperatura físicamente imposible\")\n",
    "        self._celsius = valor\n",
    "\n",
    "    @property\n",
    "    def fahrenheit(self):\n",
    "        return self._celsius * 9 / 5 + 32\n",
    "\n",
    "temperatura = Temperatura(20)\n",
    "print(temperatura.celsius, temperatura.fahrenheit)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "354b77e6",
   "metadata": {},
   "source": [
    "## 4. Métodos de clase, estáticos y constructores alternativos\n",
    "\n",
    "Un `classmethod` recibe la clase (`cls`) y suele construir instancias alternativas. Un `staticmethod` pertenece conceptualmente a la clase, pero no necesita ni instancia ni clase.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ca5b00b1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.572760Z",
     "iopub.status.busy": "2026-08-02T22:29:54.572662Z",
     "iopub.status.idle": "2026-08-02T22:29:54.575601Z",
     "shell.execute_reply": "2026-08-02T22:29:54.575346Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(51, 102, 255)\n"
     ]
    }
   ],
   "source": [
    "class Color:\n",
    "    def __init__(self, rojo, verde, azul):\n",
    "        if not all(self.componente_valido(v) for v in (rojo, verde, azul)):\n",
    "            raise ValueError(\"cada componente debe estar entre 0 y 255\")\n",
    "        self.rgb = (rojo, verde, azul)\n",
    "\n",
    "    @staticmethod\n",
    "    def componente_valido(valor):\n",
    "        return isinstance(valor, int) and 0 <= valor <= 255\n",
    "\n",
    "    @classmethod\n",
    "    def desde_hex(cls, codigo):\n",
    "        codigo = codigo.removeprefix(\"#\")\n",
    "        if len(codigo) != 6:\n",
    "            raise ValueError(\"se esperan seis dígitos hexadecimales\")\n",
    "        return cls(*(int(codigo[i:i + 2], 16) for i in (0, 2, 4)))\n",
    "\n",
    "azul = Color.desde_hex(\"#3366FF\")\n",
    "print(azul.rgb)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c17b758",
   "metadata": {},
   "source": [
    "## 5. Métodos especiales\n",
    "\n",
    "Los métodos `__repr__`, `__str__`, `__len__`, `__eq__` y otros integran objetos con la sintaxis de Python. Implementa solo los que tengan semántica natural.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "7d8626d5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.577107Z",
     "iopub.status.busy": "2026-08-02T22:29:54.577003Z",
     "iopub.status.idle": "2026-08-02T22:29:54.579386Z",
     "shell.execute_reply": "2026-08-02T22:29:54.579148Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cesta(productos=['pan', 'leche']) 2 True\n"
     ]
    }
   ],
   "source": [
    "class Cesta:\n",
    "    def __init__(self, productos=None):\n",
    "        self.productos = list(productos or [])\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.productos)\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"Cesta(productos={self.productos!r})\"\n",
    "\n",
    "    def __contains__(self, producto):\n",
    "        return producto in self.productos\n",
    "\n",
    "cesta = Cesta([\"pan\", \"leche\"])\n",
    "print(cesta, len(cesta), \"pan\" in cesta)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a445f9c",
   "metadata": {},
   "source": [
    "## 6. Herencia y polimorfismo\n",
    "\n",
    "La herencia expresa una relación «es un». `super()` delega a la clase base. El polimorfismo permite tratar objetos diferentes mediante la misma operación. Prefiere jerarquías pequeñas.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "27628190",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.580719Z",
     "iopub.status.busy": "2026-08-02T22:29:54.580630Z",
     "iopub.status.idle": "2026-08-02T22:29:54.583217Z",
     "shell.execute_reply": "2026-08-02T22:29:54.582996Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[12, 12.57]\n"
     ]
    }
   ],
   "source": [
    "from math import pi\n",
    "\n",
    "class Figura:\n",
    "    def area(self):\n",
    "        raise NotImplementedError\n",
    "\n",
    "class Rectangulo(Figura):\n",
    "    def __init__(self, ancho, alto):\n",
    "        self.ancho, self.alto = ancho, alto\n",
    "\n",
    "    def area(self):\n",
    "        return self.ancho * self.alto\n",
    "\n",
    "class Circulo(Figura):\n",
    "    def __init__(self, radio):\n",
    "        self.radio = radio\n",
    "\n",
    "    def area(self):\n",
    "        return pi * self.radio ** 2\n",
    "\n",
    "figuras = [Rectangulo(3, 4), Circulo(2)]\n",
    "print([round(figura.area(), 2) for figura in figuras])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c92a299c",
   "metadata": {},
   "source": [
    "## 7. Composición\n",
    "\n",
    "La composición expresa «tiene un» y suele acoplar menos que la herencia. Un objeto delega una parte del trabajo a otro.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "9268558b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.584500Z",
     "iopub.status.busy": "2026-08-02T22:29:54.584411Z",
     "iopub.status.idle": "2026-08-02T22:29:54.586597Z",
     "shell.execute_reply": "2026-08-02T22:29:54.586392Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ejemplo: motor en marcha\n"
     ]
    }
   ],
   "source": [
    "class Motor:\n",
    "    def arrancar(self):\n",
    "        return \"motor en marcha\"\n",
    "\n",
    "class Vehiculo:\n",
    "    def __init__(self, marca, motor):\n",
    "        self.marca = marca\n",
    "        self.motor = motor\n",
    "\n",
    "    def arrancar(self):\n",
    "        return f\"{self.marca}: {self.motor.arrancar()}\"\n",
    "\n",
    "coche = Vehiculo(\"Ejemplo\", Motor())\n",
    "print(coche.arrancar())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2d17480",
   "metadata": {},
   "source": [
    "## 8. `dataclass`\n",
    "\n",
    "Para clases centradas en datos, `@dataclass` genera inicializador, representación e igualdad. `field(default_factory=...)` crea una colección nueva por instancia.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "70d8d6c8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.587887Z",
     "iopub.status.busy": "2026-08-02T22:29:54.587793Z",
     "iopub.status.idle": "2026-08-02T22:29:54.590602Z",
     "shell.execute_reply": "2026-08-02T22:29:54.590392Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Punto(x=3, y=4) 5.0 Equipo(nombre='Datos', miembros=['Ada', 'Grace'])\n"
     ]
    }
   ],
   "source": [
    "from dataclasses import dataclass, field\n",
    "\n",
    "@dataclass(frozen=True, slots=True)\n",
    "class Punto:\n",
    "    x: float\n",
    "    y: float\n",
    "\n",
    "    def distancia_al_origen(self):\n",
    "        return (self.x ** 2 + self.y ** 2) ** 0.5\n",
    "\n",
    "@dataclass\n",
    "class Equipo:\n",
    "    nombre: str\n",
    "    miembros: list[str] = field(default_factory=list)\n",
    "\n",
    "punto = Punto(3, 4)\n",
    "equipo = Equipo(\"Datos\", [\"Ada\", \"Grace\"])\n",
    "print(punto, punto.distancia_al_origen(), equipo)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "296486e7",
   "metadata": {},
   "source": [
    "## Errores habituales\n",
    "\n",
    "- Crear una clase cuando basta una función o un diccionario simple.\n",
    "- Compartir una lista mutable como atributo de clase por accidente.\n",
    "- Hacer jerarquías profundas y frágiles.\n",
    "- Exponer estado inválido sin validar invariantes.\n",
    "- Añadir propiedades triviales que no aportan una interfaz estable.\n",
    "\n",
    "**Extensión opcional:** explora `abc.ABC`, protocolos y gestores de contexto con `__enter__`/`__exit__`.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "817695af",
   "metadata": {},
   "source": [
    "## Práctica · Préstamos de biblioteca\n",
    "\n",
    "Modela `Libro` con `titulo`, `autor` y `prestado`. Debe ofrecer:\n",
    "\n",
    "- `prestar()`, que falla si ya está prestado;\n",
    "- `devolver()`, que falla si no lo está;\n",
    "- una representación legible;\n",
    "- construcción simple mediante `@dataclass`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "80197350",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.591903Z",
     "iopub.status.busy": "2026-08-02T22:29:54.591811Z",
     "iopub.status.idle": "2026-08-02T22:29:54.593778Z",
     "shell.execute_reply": "2026-08-02T22:29:54.593501Z"
    },
    "tags": [
     "ejercicio"
    ]
   },
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass\n",
    "class Libro:\n",
    "    titulo: str\n",
    "    autor: str\n",
    "    prestado: bool = False\n",
    "\n",
    "    def prestar(self):\n",
    "        # TODO: valida y cambia el estado.\n",
    "        pass\n",
    "\n",
    "    def devolver(self):\n",
    "        # TODO: valida y cambia el estado.\n",
    "        pass\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ebc2a7ab",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-02T22:29:54.595039Z",
     "iopub.status.busy": "2026-08-02T22:29:54.594950Z",
     "iopub.status.idle": "2026-08-02T22:29:54.597207Z",
     "shell.execute_reply": "2026-08-02T22:29:54.596994Z"
    },
    "tags": [
     "solucion"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Libro(titulo='El lenguaje de las máquinas', autor='Ejemplo', prestado=True)\n"
     ]
    }
   ],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass\n",
    "class Libro:\n",
    "    titulo: str\n",
    "    autor: str\n",
    "    prestado: bool = False\n",
    "\n",
    "    def prestar(self):\n",
    "        if self.prestado:\n",
    "            raise RuntimeError(\"el libro ya está prestado\")\n",
    "        self.prestado = True\n",
    "\n",
    "    def devolver(self):\n",
    "        if not self.prestado:\n",
    "            raise RuntimeError(\"el libro no estaba prestado\")\n",
    "        self.prestado = False\n",
    "\n",
    "libro = Libro(\"El lenguaje de las máquinas\", \"Ejemplo\")\n",
    "libro.prestar()\n",
    "print(libro)\n",
    "libro.devolver()\n",
    "assert libro.prestado is False\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eb9761da",
   "metadata": {},
   "source": [
    "            ## Comprobación rápida\n",
    "\n",
    "            - [ ] Sé distinguir estado de instancia y estado de clase.\n",
    "- [ ] Puedo elegir entre herencia y composición.\n",
    "- [ ] Uso `dataclass` para modelos de datos sencillos.\n",
    "\n",
    "            [← Anterior](06_errores_y_excepciones.ipynb) · [Volver al índice](README.md) · [Siguiente →](08_numpy.ipynb)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
