{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Objetos, lo justo\n",
    "\n",
    "Por qué df.head() lleva un punto, qué es un método, y cuándo te conviene crear una clase (spoiler: pocas veces).\n",
    "\n",
    "Cuaderno de práctica del capítulo 9 de **Python desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/python-desde-cero/objetos/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"MTcKTUlOSU1BUktFVCBFTCBTT0wKMg==\",\n",
    "    2: \"VHlwZUVycm9yOiAnaW50JyBvYmplY3QgaXMgbm90IGNhbGxhYmxl\",\n",
    "    3: \"PGJ1aWx0LWluIG1ldGhvZCB1cHBlciBvZiBzdHIgb2JqZWN0IGF0IC4uLg==\",\n",
    "    4: \"Qm9kZWdhIFJvc2EgKFBpdXJhKQ==\",\n",
    "    5: \"W0NsaWVudGUoJ0JvZGVnYSBSb3NhJywgJ1BpdXJhJyksIENsaWVudGUoJ1NvbCcsICdMaW1hJyld\",\n",
    "    6: \"Q2xpZW50ZShub21icmU9J0JvZGVnYSBSb3NhJywgY2l1ZGFkPSdQaXVyYScsIGFjdGl2bz1UcnVlKQpDbGllbnRlKG5vbWJyZT0nU29sJywgY2l1ZGFkPSdMaW1hJywgYWN0aXZvPUZhbHNlKQ==\",\n",
    "    7: \"VHlwZUVycm9yOiBQZWRpZG8udG90YWwoKSB0YWtlcyAwIHBvc2l0aW9uYWwgYXJndW1lbnRzIGJ1dCAxIHdhcyBnaXZlbg==\",\n",
    "    8: \"NjM1LjIKNjM1LjI=\",\n",
    "    9: \"ZW4gbGEgbGlzdGE6IDMKc2luIHJlcGV0aXIgOiAyCiAgRGlzdHJpYnVpZG9yYSBMaW1hCiAgUm9zaXRh\",\n",
    "    10: \"cHJlY2lvIHBvciB1bmlkYWQ6IDg1LjAKcmVjaGF6YWRhOiBtb250byBuZWdhdGl2bzogLTUwLjA=\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Hola! Este capítulo es corto a propósito"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te lo digo de una: **para analizar datos necesitas entender objetos, no\n",
    "diseñarlos** 🌸\n",
    "\n",
    "He visto cursos que meten tres semanas de herencia y polimorfismo antes de\n",
    "tocar un CSV, y la gente sale creyendo que Python es dificilísimo. No lo es.\n",
    "\n",
    "Lo que sí necesitas es entender por qué escribes `df.head()` con\n",
    "punto y con paréntesis, y por qué `df.shape` lleva punto pero no\n",
    "paréntesis. Vamos a eso.\n",
    "\n",
    "Así que la pregunta de este capítulo es chiquita: **¿por qué unas cosas llevan paréntesis y otras no?** Contéstala y ya sabes lo único que hace falta saber de objetos para trabajar con datos 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Todo lo que has usado ya era un objeto"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "texto = 'Minimarket El Sol'\n",
    "\n",
    "print(type(texto))\n",
    "print(texto.upper())\n",
    "print(texto.count('l'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese `texto` no es solo una cadena de letras: es un objeto de tipo\n",
    "`str` que viene con funciones pegadas. Esas funciones pegadas se\n",
    "llaman **métodos**, y se llaman con el punto.\n",
    "\n",
    "La lista también:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas = [480.37, 154.83]\n",
    "\n",
    "ventas.append(2480.84)\n",
    "print(ventas)\n",
    "print(ventas.count(154.83))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Atributo o método, que es la duda de todos\n",
    "\n",
    "Esta es la pregunta que quería resolverte con este capítulo:\n",
    "\n",
    "- 📊 **Atributo**: un dato que el objeto guarda. Se lee\n",
    "**sin paréntesis**.\n",
    "\n",
    "- ⚙️ **Método**: una acción que el objeto sabe hacer. Se llama\n",
    "**con paréntesis**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import date\n",
    "\n",
    "hoy = date(2026, 8, 15)\n",
    "\n",
    "print(hoy.year)          # atributo: un dato guardado\n",
    "print(hoy.month)\n",
    "print(hoy.weekday())     # metodo: calcula algo y lo devuelve\n",
    "print(hoy.isoformat())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y esto es lo que pasa si te equivocas, que te va a pasar:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    print(hoy.year())\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "TypeError: 'int' object is not callable\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo: `hoy.year` es un `int`, o sea el número 2026, y\n",
    "le pusiste paréntesis como si fuera una función. \"No es llamable\" significa\n",
    "justo eso 🙃\n",
    "\n",
    "Al revés también avisa:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(hoy.weekday)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No revienta, pero mira lo que imprimió: te describió el método en vez de\n",
    "ejecutarlo. **Si tu salida dice \"method\" en alguna parte, te faltaron los\n",
    "paréntesis.** Guárdate ese truco, que en pandas lo vas a usar 💜\n",
    "\n",
    "Cuando dudes, `dir()` te lista todo lo que tiene un objeto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print([n for n in dir(hoy) if not n.startswith('_')])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese filtro del guion bajo saca los internos, que no te interesan por ahora."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Crear tu propia clase"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una clase es el molde y un objeto es lo que sale del molde. Vamos con un\n",
    "ejemplo que sí tiene sentido."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Venta:\n",
    "    def __init__(self, cliente, ciudad, monto, unidades):\n",
    "        self.cliente = cliente\n",
    "        self.ciudad = ciudad\n",
    "        self.monto = monto\n",
    "        self.unidades = unidades\n",
    "\n",
    "    def precio_unitario(self):\n",
    "        return round(self.monto / self.unidades, 2)\n",
    "\n",
    "    def con_igv(self, igv=0.18):\n",
    "        return round(self.monto * (1 + igv), 2)\n",
    "\n",
    "v = Venta('C0045', 'Trujillo', 480.37, 10)\n",
    "\n",
    "print(v.cliente)\n",
    "print(v.precio_unitario())\n",
    "print(v.con_igv())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres cosas y ya sabes leer cualquier clase:\n",
    "\n",
    "- 🏗️ `__init__` es lo que corre al crear el objeto. Ahí se guardan\n",
    "los datos.\n",
    "\n",
    "- 👉 `self` es \"este objeto en particular\". Va como primer argumento\n",
    "de todos los métodos y **nunca se pasa al llamarlos**: Python lo\n",
    "pone solo.\n",
    "\n",
    "- 💾 `self.cliente = cliente` guarda el dato dentro del objeto para\n",
    "que siga ahí después.\n",
    "\n",
    "Si te olvidas del `self`, pasa esto:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    class Mal:\n",
    "        def __init__(self, monto):\n",
    "            self.monto = monto\n",
    "\n",
    "        def doble():\n",
    "            return 0\n",
    "\n",
    "    Mal(100).doble()\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "TypeError: Mal.doble() takes 0 positional arguments but 1 was given\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python le pasó el objeto como primer argumento y el método no lo esperaba. Ese\n",
    "\"1 was given\" cuando tú no diste ninguno es la firma del `self`\n",
    "olvidado 😄\n",
    "\n",
    "### Que se imprima bonito"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(v)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Feo, ¿no? Se arregla con `__repr__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Venta:\n",
    "    def __init__(self, cliente, ciudad, monto):\n",
    "        self.cliente = cliente\n",
    "        self.ciudad = ciudad\n",
    "        self.monto = monto\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f'Venta({self.cliente}, {self.ciudad}, S/{self.monto})'\n",
    "\n",
    "print(Venta('C0045', 'Trujillo', 480.37))\n",
    "print([Venta('C0325', 'Lima', 524.9), Venta('C0260', 'Cusco', 154.83)])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en la segunda línea: al imprimir una lista de objetos, cada uno usa su\n",
    "`__repr__`. Eso hace que depurar sea infinitamente más cómodo 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## dataclass: lo mismo con mucho menos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la clase es solo para guardar datos, hay un atajo que te ahorra escribir el\n",
    "`__init__` y el `__repr__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass\n",
    "class Venta:\n",
    "    cliente: str\n",
    "    ciudad: str\n",
    "    monto: float\n",
    "    unidades: int = 1\n",
    "\n",
    "    def precio_unitario(self):\n",
    "        return round(self.monto / self.unidades, 2)\n",
    "\n",
    "v = Venta('C0045', 'Trujillo', 480.37, 10)\n",
    "print(v)\n",
    "print(v.precio_unitario())\n",
    "print(Venta('C0325', 'Lima', 524.9))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese `@dataclass` de arriba es un decorador, o sea algo que le\n",
    "agrega comportamiento a la clase. Escribió el `__init__`, el\n",
    "`__repr__` y la comparación por ti.\n",
    "\n",
    "Y esos `: str` y `: float` son anotaciones de tipo. No\n",
    "obligan nada en tiempo de ejecución, pero tu editor las lee y te avisa si metes\n",
    "un texto donde iba un número 💜"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la pregunta importante: ¿te hace falta una clase?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi siempre no. De verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Situación | Qué usar |\n",
    "|---|---|\n",
    "| Una fila de datos suelta | Un diccionario |\n",
    "| Muchas filas con las mismas columnas | Un DataFrame de pandas |\n",
    "| Una transformación | Una función |\n",
    "| Datos y comportamiento que van SIEMPRE juntos | Ahí sí, una clase |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira estas dos formas de hacer lo mismo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "venta_dict = {'cliente': 'C0045', 'monto': 480.37, 'unidades': 10}\n",
    "print(round(venta_dict['monto'] / venta_dict['unidades'], 2))\n",
    "\n",
    "venta_obj = Venta('C0045', 'Trujillo', 480.37, 10)\n",
    "print(venta_obj.precio_unitario())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para una fila, el diccionario gana por simple. Para tres mil filas,\n",
    "**ninguna de las dos**: ahí va pandas, que es el capítulo 12.\n",
    "\n",
    "Mi regla después de varios proyectos: **escribe funciones hasta que te\n",
    "duela, y recién ahí piensa en una clase**. La clase se justifica cuando\n",
    "llevas cinco funciones que reciben siempre los mismos tres argumentos. Ahí esos\n",
    "tres argumentos querían ser un objeto 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Que dos objetos iguales se reconozcan"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta es la que más rabia da la primera vez 😤"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Simple:\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "\n",
    "a = Simple('Rosita')\n",
    "b = Simple('Rosita')\n",
    "print('con clase normal, a == b ->', a == b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos objetos con exactamente el mismo contenido y Python dice que no son\n",
    "iguales. No está roto: por defecto, comparar dos objetos pregunta **si son\n",
    "el mismo objeto**, no si tienen lo mismo dentro.\n",
    "\n",
    "Y eso rompe cosas que ni te imaginas: buscar en una lista, quitar duplicados\n",
    "con un conjunto, comparar el antes y el después de un cálculo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass\n",
    "class ConDataclass:\n",
    "    nombre: str\n",
    "\n",
    "c = ConDataclass('Rosita')\n",
    "d = ConDataclass('Rosita')\n",
    "print('con dataclass,    c == d ->', c == d)\n",
    "print('y buscando en una lista  ->', ConDataclass('Rosita') in [c])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El `@dataclass` escribe ese `==` por ti, y es la\n",
    "segunda razón para usarlo aunque la primera ya fuera suficiente 🎁"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error que hasta las dataclass te marcan"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si le pones una lista vacía como valor por defecto a un atributo, Python te\n",
    "para en seco."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    @dataclass\n",
    "    class Malo:\n",
    "        compras: list = []\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "ValueError: mutable default <class 'list'> for field compras is not allowed: use default_factory\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese error es un regalo, y es el mismo problema del capítulo\n",
    "6: el valor por defecto se crea **una sola vez**, al\n",
    "definir la clase. Sin ese aviso, todos los clientes que crearas compartirían la\n",
    "misma lista de compras.\n",
    "\n",
    "La forma correcta lo dice el propio mensaje:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import field\n",
    "\n",
    "@dataclass\n",
    "class Bueno:\n",
    "    compras: list = field(default_factory=list)\n",
    "\n",
    "x = Bueno()\n",
    "y = Bueno()\n",
    "x.compras.append('venta 1')\n",
    "print('y.compras ->', y.compras)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`default_factory=list` significa \"llama a `list()` cada\n",
    "vez que crees uno\". Por eso `y` nace con su lista vacía y no con la\n",
    "de `x` 🧺\n",
    "\n",
    "Fíjate en la diferencia con el capítulo 6: allí el mismo\n",
    "error se cuela en silencio y aquí Python lo caza. Cuando una librería te obliga\n",
    "a hacerlo bien, agradécelo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y sin dataclass, sin aviso ninguno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Arriba te dije que sin ese error todos los clientes compartirían la misma\n",
    "lista. Ahora te lo enseño pasando de verdad, porque en una clase normal\n",
    "**nadie te avisa** 😖"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClienteMal:\n",
    "    compras = []\n",
    "\n",
    "    def __init__(self, codigo):\n",
    "        self.codigo = codigo\n",
    "\n",
    "    def compra(self, monto):\n",
    "        self.compras.append(monto)\n",
    "\n",
    "\n",
    "a = ClienteMal('C0045')\n",
    "b = ClienteMal('C0325')\n",
    "a.compra(480.37)\n",
    "b.compra(154.83)\n",
    "\n",
    "print('compras de a:', a.compras)\n",
    "print('compras de b:', b.compras)\n",
    "print('es la misma lista:', a.compras is b.compras)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos clientes distintos, cada uno con una compra, y los dos ven las dos 🫠\n",
    "\n",
    "Lo que pasó es que `compras = []` está escrito\n",
    "**en la clase**, no dentro de `__init__`. Se creó una sola\n",
    "vez, cuando Python leyó el archivo, y todos los objetos que nazcan de esa clase\n",
    "apuntan a esa misma lista.\n",
    "\n",
    "La diferencia es exactamente esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Dónde lo escribes | Qué es | Quién lo tiene |\n",
    "|---|---|---|\n",
    "| Fuera de `__init__` | Atributo de clase | Uno solo, compartido por todos |\n",
    "| Dentro, con `self.` | Atributo de instancia | Uno por objeto |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se arregla moviendo la línea adentro: `self.compras = []` dentro de\n",
    "`__init__`. Ahí se crea una lista nueva en cada objeto, que es lo que\n",
    "querías 🩹\n",
    "\n",
    "Y por eso el `@dataclass` te para en seco en vez de dejarte hacer\n",
    "esto: te está ahorrando una tarde entera de mirar por qué a un cliente le aparecen\n",
    "las compras de otro.\n",
    "\n",
    "Ojo, que un atributo de clase no es malo: es perfecto para cosas que de verdad\n",
    "son iguales para todos, tipo `IGV = 0.18` o una lista de nombres de\n",
    "columnas que nadie va a modificar. Lo peligroso es cuando es\n",
    "**mutable** y alguien lo toca."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Otra forma de crear el objeto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi siempre los datos no te llegan como argumentos bonitos: te llegan como\n",
    "una fila de un CSV. Y para eso está `@classmethod` 🏗️"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass, field\n",
    "\n",
    "\n",
    "@dataclass\n",
    "class ClienteBien:\n",
    "    codigo: str\n",
    "    compras: list = field(default_factory=list)\n",
    "\n",
    "    @classmethod\n",
    "    def desde_fila(cls, fila):\n",
    "        codigo, monto = fila.split(';')\n",
    "        return cls(codigo.strip(), [float(monto)])\n",
    "\n",
    "    @property\n",
    "    def total(self):\n",
    "        return round(sum(self.compras), 2)\n",
    "\n",
    "\n",
    "c = ClienteBien.desde_fila('C0045; 480.37')\n",
    "d = ClienteBien('C0325')\n",
    "c.compras.append(120.0)\n",
    "\n",
    "print(c)\n",
    "print(d)\n",
    "print('total de c      :', c.total)\n",
    "print('comparten lista :', c.compras is d.compras)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres cosas ahí que vale la pena separar 🧩\n",
    "\n",
    "**El `@classmethod`** es un constructor alternativo.\n",
    "Recibe `cls`, que es la clase misma, en vez de `self`, y\n",
    "devuelve un objeto nuevo. Lo que consigues es que la lógica de \"cómo se lee una\n",
    "fila\" viva junto a la clase y no repartida por diez sitios.\n",
    "\n",
    "**El `@property`** es un método que se usa como si\n",
    "fuera un atributo: fíjate en que escribí `c.total` sin paréntesis. Para\n",
    "lo que se calcula a partir de lo que ya tienes, queda mucho más natural.\n",
    "\n",
    "**Y el `False` del final** confirma que\n",
    "`default_factory` funcionó: cada cliente tiene su propia lista, al\n",
    "revés que en la clase de arriba 💜\n",
    "\n",
    "Un patrón que uso mucho con esto: `desde_fila` para leer,\n",
    "`@property` para lo calculado, y una función suelta que recorre el CSV\n",
    "y devuelve la lista de objetos. Con eso, el resto del programa deja de saber cómo\n",
    "era el archivo 🗂️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Herencia, que aquí se usa menos de lo que parece"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Heredar es decir \"esto es como aquello, pero con algo distinto\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@dataclass\n",
    "class Cliente:\n",
    "    nombre: str\n",
    "    segmento: str\n",
    "\n",
    "    def descuento(self):\n",
    "        return 0.0\n",
    "\n",
    "    def ficha(self):\n",
    "        return f'{self.nombre} ({self.segmento}) descuento {self.descuento():.0%}'\n",
    "\n",
    "@dataclass\n",
    "class Mayorista(Cliente):\n",
    "    segmento: str = 'Mayorista'\n",
    "\n",
    "    def descuento(self):\n",
    "        return 0.15\n",
    "\n",
    "for c in [Cliente('Bodega Rosita', 'Bodega'), Mayorista('Distribuidora Lima')]:\n",
    "    print(c.ficha())\n",
    "\n",
    "print('Mayorista es un Cliente?', isinstance(Mayorista('X'), Cliente))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira lo que pasó: `ficha` se escribió una sola vez, en\n",
    "`Cliente`, y cuando la llama un `Mayorista` usa\n",
    "**su** descuento. Eso se llama polimorfismo y suena mucho peor de\n",
    "lo que es 🎭\n",
    "\n",
    "Ahora la parte honesta, que casi ningún tutorial dice: **en análisis de\n",
    "datos vas a heredar poquísimo**. La herencia sirve cuando tienes varias\n",
    "cosas que se parecen mucho y se comportan distinto en un detalle. Un análisis\n",
    "normal tiene funciones y DataFrames, no jerarquías de clases.\n",
    "\n",
    "Dónde sí la vas a ver: heredando de algo que ya existe, como cuando escribes\n",
    "un transformador propio para scikit-learn. Ahí heredas porque la librería te lo\n",
    "pide, no porque tú lo diseñaras así 🔧"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Atributo o método\n",
    "\n",
    "De un texto, saca su largo, su versión en mayúsculas y\n",
    "cuántas veces aparece una letra. Di cuál de las tres es un atributo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Provoca el error de los paréntesis\n",
    "\n",
    "Con una fecha, llama a un atributo como si fuera método."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El método sin llamar\n",
    "\n",
    "Imprime un método sin los paréntesis y aprende a reconocer\n",
    "la salida."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Tu primera clase\n",
    "\n",
    "Crea una clase `Cliente` con nombre y ciudad, y\n",
    "un método que devuelva una etiqueta tipo \"Nombre (Ciudad)\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Que se imprima bien\n",
    "\n",
    "Agrégale `__repr__` a esa clase e imprime una\n",
    "lista de dos clientes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Con dataclass\n",
    "\n",
    "Reescribe la clase anterior como `dataclass` y\n",
    "compara cuánto código te ahorraste."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El self olvidado\n",
    "\n",
    "Escribe un método sin `self` a propósito y lee el\n",
    "mensaje de error con calma."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. Diccionario o clase\n",
    "\n",
    "Guarda tres ventas de las dos formas y calcula el total.\n",
    "Después decide cuál te gusta más para este caso."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 8\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9. Dos clientes que deberían ser el mismo\n",
    "\n",
    "Quita los duplicados de una lista de clientes. Con una\n",
    "clase normal no se puede; con dataclass sí, y hace falta una línea más."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 9\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10. Una clase que se defiende sola\n",
    "\n",
    "Haz que la clase compruebe sus propios datos al crearse,\n",
    "en vez de confiar en quien la llama."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 10\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La lista que todos tus clientes comparten sin saberlo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes de cerrar, la trampa de clases por excelencia. Funciona perfecto mientras pruebas con un objeto, y por eso llega tan lejos antes de que alguien la vea 😳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Creas una clase para tus clientes con una lista de compras dentro, que es lo natural. Registras una compra de Ana y miras a Beto.\n",
    "\n",
    "```\n",
    "class Cliente:\n",
    "    compras = []\n",
    "\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "\n",
    "ana = Cliente('Ana')\n",
    "beto = Cliente('Beto')\n",
    "ana.compras.append('arroz')\n",
    "\n",
    "print(beto.compras)   # ['arroz']\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "¿Cuándo te conviene escribir una clase en un proyecto de datos?\n",
    "\n",
    "a) Cuando tienes datos y funciones que van siempre juntos\n",
    "\n",
    "b) Siempre, porque es la forma profesional de programar\n",
    "\n",
    "c) Cuando el archivo pasa de doscientas líneas\n",
    "\n",
    "d) Nunca, en análisis de datos no hacen falta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🔵 En Python casi todo es un objeto, y por eso todo se usa con puntos.\n",
    "\n",
    "- 📊 Atributo sin paréntesis, método con paréntesis.\n",
    "\n",
    "- 🔎 Si tu salida dice \"method\", te faltaron los paréntesis.\n",
    "\n",
    "- 👉 `self` es este objeto, va primero en cada método y no se pasa\n",
    "al llamar.\n",
    "\n",
    "- ⚡ `@dataclass` te escribe el `__init__` y el\n",
    "`__repr__`.\n",
    "\n",
    "- 🤏 Antes de crear una clase, pregúntate si un diccionario o una función\n",
    "bastan. Casi siempre bastan.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para datos hace falta entender objetos, no diseñarlos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso de rechazar un dato imposible al entrar, en vez de descubrirlo tres\n",
    "pantallas más abajo, en una base de datos tiene nombre y se llama\n",
    "restricción. Está en el [libro de SQL](https://missyera.com/guias/sql-desde-cero/),\n",
    "y es la misma idea escrita en otro sitio 🛡️\n",
    "\n",
    "En el capítulo 10 abrimos archivos de verdad: CSV, Excel, JSON, y la\n",
    "codificación, que es la razón por la que a veces ves \"Ã¡\" donde debería haber una\n",
    "á.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 9 de **Python desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/python-desde-cero/objetos/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
