{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'> Material creado en 2019-2 por Equipo Docente IIC2233. Modificado al 2026-1 por Equipo Docente IIC2233</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [¿Qué es modularización?](#¿Qué-es-modularización?)\n",
    "2. [Importación en Python](#Importación-en-Python)\n",
    "3. [Sentencias de importación](#Sentencias-de-importación)\n",
    "4. [¿Y por qué usar módulos?](#¿Y-por-qué-usar-módulos?)\n",
    "5. [Abstracción de componentes](#Abstracción-de-componentes)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Qué es modularización?\n",
    "\n",
    "Programación modular es una técnica de diseño de software que se basa en separar el código de un programa en componentes lógicos llamados **módulos**, cada uno con funcionalidades independientes. En términos muy simples, es separar de forma ordenada el código de un programa en múltiples archivos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Importación en Python\n",
    "\n",
    "Python está orientado a generar código modular. Cada archivo con extensión `.py` que tiene sentencias y definiciones es un módulo en Python. Con esto, se permite importar definiciones de un módulo en otro mediante la palabra reservada `import`. Esto permite importar variables, funciones, clases y cualquier otro tipo de definición creada.\n",
    "\n",
    "Por ejemplo, junto a estos archivos podrás encontrar un módulo llamado `ejemplo.py`, que tiene la definición de una variable, una función y una clase. Revisa ese archivo y luego continúa leyendo. Mediante `import` podemos ingresar todas esas definiciones y utilizarlas en este documento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ejemplo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "10"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ejemplo.mi_variable"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "¡Hola!\n"
     ]
    }
   ],
   "source": [
    "ejemplo.saludar()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'42'"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "instancia = ejemplo.MiClase(\"42\")\n",
    "instancia.argumento"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ¿Cómo se ejecuta la importación?\n",
    "\n",
    "Al ejecutar un archivo de extensión `.py` las líneas se ejecutan en orden, una línea a la vez. Cuando se llega a una sentencia `import` Python: **ejecuta** el archivo en referencia completo, creando variables y creando definiciones; luego vuelve al archivo original a continuar con el resto de las sentencias. En este último, se puede utilizar el código ejecutado en el módulo importado, pues ya fue ejecutado.\n",
    "\n",
    "Esto se cumple para importaciones en cadena. Digamos que un archivo `a.py` importa a `b.py`, el cual a su vez importa a `c.py`. Si ejecutamos `a.py`:\n",
    "1. Al llegar a la sentencia `import b` en `a.py`, se comienza ejecutando `b.py`\n",
    "2. Al llegar a la sentencia `import c` en `b.py`, se comienza ejecutando `c.py`\n",
    "3. Si hay más importaciones en `c.py`, se ejecuta sucesivamente de la misma forma esos módulos. Luego se ejecuta el resto de `c.py`\n",
    "4. Se ejecuta el resto de `b.py`\n",
    "5. Se ejecuta el resto de `a.py`\n",
    "\n",
    "Python verifica los archivos que ya han sido importados en el contexto del programa antes de ejecutar un `import`. Es por esto que al formar un ciclo de importaciones, estas funcionan correctamente, ya que Python ignora los módulos que ya han sido importados. En el ejemplo anterior, si `c.py` posee una sentencia `import a`, Python no ejecuta nuevamente el `import a`, pues, ya se ha iniciado un `import` de `a.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sentencias de importación\n",
    "\n",
    "Hasta el momento vimos una sola forma de importar un módulo, pero hay varias formas de hacerlo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importación completa\n",
    "\n",
    "Es la mostrada anteriormente y es de la forma:\n",
    "\n",
    "```python\n",
    "import modulo\n",
    "```\n",
    "\n",
    "Esto provee una nueva variable con el nombre del módulo que contiene todas las definiciones importadas. Para acceder a las definiciones del módulo, se acceden mediante esta variable, punto (`.`) y el nombre de la definición. Es similar a acceder a atributos o métodos de la instancia de una clase:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n",
      "¡Hola!\n"
     ]
    }
   ],
   "source": [
    "import ejemplo\n",
    "\n",
    "\n",
    "print(ejemplo.mi_variable)\n",
    "ejemplo.saludar()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importación completa con un alias\n",
    "\n",
    "```python\n",
    "import modulo as alias\n",
    "```\n",
    "\n",
    "Funciona de igual forma a la anterior, pero es posible cambiar el nombre de la variable que contiene las definiciones del módulo. Esta opción es conveniente cuando el nombre puede generar confusión con otras definiciones del archivo en ejecución. También es conveniente cuando el nombre del módulo importado es muy largo y se quiere reducir."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n",
      "¡Hola!\n",
      "Otra variable\n"
     ]
    }
   ],
   "source": [
    "import ejemplo as ej\n",
    "\n",
    "\n",
    "print(ej.mi_variable)\n",
    "ej.saludar()\n",
    "\n",
    "ejemplo = \"Otra variable\"\n",
    "print(ejemplo)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importación parcial\n",
    "\n",
    "```python\n",
    "from modulo import variable, funcion, Clase\n",
    "```\n",
    "\n",
    "Este tipo de sentencia permite ahorrarse completamente utilizar una variable para acceder a los contenidos del módulo y permite utilizar directamente los nombres de esas definiciones en el archivo en ejecución. Se llama parcial, porque a su vez permite elegir qué definiciones se están importando y cuáles no:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n",
      "23\n"
     ]
    }
   ],
   "source": [
    "from ejemplo import mi_variable, MiClase\n",
    "\n",
    "\n",
    "print(mi_variable)\n",
    "instancia = MiClase(23)\n",
    "print(instancia.argumento)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'saludar' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43msaludar\u001b[49m()  \u001b[38;5;66;03m# No se importó en la sentencia, así que no está disponible para usarse\u001b[39;00m\n",
      "\u001b[31mNameError\u001b[39m: name 'saludar' is not defined"
     ]
    }
   ],
   "source": [
    "saludar()  # No se importó en la sentencia, así que no está disponible para usarse"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importación de un archivo dentro de una carpeta\n",
    "\n",
    "```python\n",
    "import carpeta_con_modulo.modulo\n",
    "import carpeta_con_modulo.modulo as modulo\n",
    "from carpeta_con_modulo import modulo\n",
    "from carpeta_con_modulo.modulo import funcion\n",
    "```\n",
    "\n",
    "A veces, el tamaño de nuestro programa aumenta, y con este el número de archivos y módulos auxiliares que necesitamos. Por temas de orden, podemos crear subcarpetas en nuestro proyecto y ordenar los módulos auxiliares dentro de ellas. En estos casos, si queremos importar utilidades desde un módulo que se encuentra dentro de una subcarpeta, debemos seguir el formato `nombre_carpeta.nombre_modulo` para acceder a este. \n",
    "\n",
    "Tal como en el ejemplo, podemos combinar esto con cualquiera de las tres formas de importación que mencionamos antes. Suponiendo que junto al archivo que queremos ejecutar, tenemos una carpeta llamada `carpeta_con_modulo` donde dentro existe un archivo llamado `modulo.py`, podemos importar de las siguientes formas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sopaipillas\n"
     ]
    }
   ],
   "source": [
    "import carpeta_con_modulo.modulo\n",
    "\n",
    "print(carpeta_con_modulo.modulo.variable)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sopaipillas\n"
     ]
    }
   ],
   "source": [
    "import carpeta_con_modulo.modulo as modulo\n",
    "\n",
    "print(modulo.variable)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sopaipillas\n"
     ]
    }
   ],
   "source": [
    "from carpeta_con_modulo import modulo\n",
    "\n",
    "print(modulo.variable)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sopaipillas\n"
     ]
    }
   ],
   "source": [
    "from carpeta_con_modulo.modulo import variable\n",
    "\n",
    "print(variable)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importación completa sin referencia al módulo ❌❌❌\n",
    "\n",
    "```python\n",
    "from modulo import *\n",
    "```\n",
    "\n",
    "Similar a la forma anterior, permite importar definiciones desde un módulo sin la necesidad de usar una variable para almacenarlos todos, pero el uso de `*` especifica que se desean importar **todas** las definiciones. El uso de estas sentencias se considera una **mala práctica**, ya que importa definiciones sin saber cuántas o cuáles son, y potencialmente importa nombres de definiciones que pueden coincidir con nombres de tu programa, sobrescribiendo definiciones y obteniendo resultados inesperados. Siempre se prefiere tener algún tipo de referencia al módulo de donde proviene una definición, o en su defecto definir, mediante importación parcial, cuáles son aquellas importadas. Esto hace que el código ajeno sea más legible y fácil de entender. \n",
    "\n",
    "A lo largo del curso IIC2233 **penalizaremos el uso de esta práctica**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Variable `__name__`\n",
    "\n",
    "Al tener múltiples módulos que se importan unos entre ellos, con alias o no, a veces es necesario identificar módulos por sus nombres, e identificar si el módulo está siendo importado o directamente ejecutado. Aquí entra la variable global `__name__`, cuyo valor cambia en el contexto de cada módulo. En general, si se accede a esa variable dentro de un módulo, esta toma como valor (en forma de _string_) el nombre del módulo. Por ejemplo, importando con o sin alias, se puede ver el nombre de módulo accediendo a esta variable:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'ejemplo'"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import ejemplo\n",
    "\n",
    "\n",
    "ejemplo.__name__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'ejemplo'"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import ejemplo as ej\n",
    "\n",
    "\n",
    "ej.__name__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hay una única excepción a la regla. Cuando se accede a `__name__` desde el módulo en que se inició la ejecución. Esto es, desde un módulo que no está siendo importado por otro módulo, entonces la variable `__name__` toma el valor `\"__main__\"`. Por ejemplo, al acceder a `__name__` en este documento obtenemos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'__main__'"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "__name__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el módulo adjunto `ejemplo2.py` se guarda el nombre de `__name__` en otra variable para ver su valor, y además se agrega una sentencia condicional que se ejecuta si es el módulo principal. Al importar el módulo, vemos que no entra al condicional, ya que no imprime nada, pero vemos que el nombre almacenado es efectivamente `\"ejemplo2\"`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'ejemplo2'"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import ejemplo2\n",
    "\n",
    "\n",
    "ejemplo2.mi_nombre"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En cambio, si ejecutamos ese condicional aquí, en el \"módulo\" principal:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Soy el módulo principal\n"
     ]
    }
   ],
   "source": [
    "if __name__ == \"__main__\":\n",
    "    print(\"Soy el módulo principal\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sentencias como la anterior suelen encontrarse en módulos que solo contienen definiciones y cuyo único propósito es ser importados por otros módulos. Para el extraño caso en que alguien ejecute ese módulo como el programa principal, se deja código dentro de la instrucción condicional, con el objetivo de mostrar formas de utilizar el módulo al ser importado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Y por qué usar módulos?\n",
    "\n",
    "Al escribir pequeños programas es común escribirlos en un solo archivo, pero a medida que un programa crece en extensión y complejidad, esta alternativa no escala bien. El archivo se hace cada vez más largo y más difícil de organizar, y como consecuencia, se hace difícil de leer. Entender dicho programa no solo sería una ardua tarea para un tercero que no conoce previamente el código, sino que también lo es para el mismo autor del programa."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por ejemplo, imaginemos que tenemos la tarea de crear un programa que, dado un archivo de contactos de usuarios, envíe un correo a cada uno con el contenido de otro archivo en formato PDF. Podemos identificar varias subtareas que realizar para crear este programa:\n",
    "\n",
    "- Comprobar que el archivo de lista de contactos existe\n",
    "- Identificar cuál es el formato del archivo de lista de contactos\n",
    "- Leer el archivo de lista de contactos\n",
    "- Transformar el contenido del archivo a una representación interna del programa\n",
    "- Eliminar posibles contactos con datos faltantes\n",
    "- Identificar el nombre y correo de cada usuario\n",
    "- Comprobar que los correos son válidos\n",
    "- Comprobar que el archivo PDF existe y es de ese formato\n",
    "- Leer el archivo PDF\n",
    "- Extraer el contenido del archivo PDF a una representación interna del programa\n",
    "- Enviar el contenido por correo a usuarios"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un primer acercamiento para resolver el problema sería realizar una función dedicada a cada una de estas subtareas. Al nivel de detalle que se mostró, ya habrían muchas funciones en un mismo archivo. Pero si lo pensamos, varias de estas funciones podrían agruparse por el tipo de tareas que realizan:\n",
    "\n",
    "- Funciones para extraer y procesar la lista de usuarios:\n",
    "    - Comprobar que el archivo de lista de contactos existe\n",
    "    - Identificar cuál es el formato del archivo de lista de contactos\n",
    "    - Leer el archivo de lista de contactos\n",
    "    - Transformar el contenido del archivo a una representación interna del programa\n",
    "    - Eliminar posibles contactos con datos faltantes\n",
    "    - Identificar el nombre y correo de cada usuario\n",
    "- Funciones para extraer el contenido de un archivo PDF:\n",
    "    - Comprobar que el archivo PDF existe y es de ese formato\n",
    "    - Leer el archivo PDF\n",
    "    - Extraer el contenido del archivo PDF a una representación interna del programa\n",
    "- Funciones para enviar correos:\n",
    "    - Comprobar que los correos son válidos\n",
    "    - Enviar el contenido por correo a usuarios\n",
    "\n",
    "Es más, estas categorías de funciones son independientes entre sí: ninguna depende de una funcionalidad de la otra. Esto nos indica que una manera de organizar el código del programa es separando las funcionalidades en tres módulos: uno de contactos, uno de extracción de PDF y otro de emails, cada uno escrito en un archivo separado: `contactos.py`, `pdf.py` e `emails.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Modularizar código provee varias ventajas. Primero: mejor organización y legibilidad de código. Esto trae como consecuencia otras ventajas: mayor facilidad de *debuggeo* y de inspección de código. Cuando un error aparece o se quiere buscar una función particular, es más fácil encontrar el origen si se encuentra en un módulo separado. \n",
    "\n",
    "También permite reutilizar código fácilmente. El tener módulos completamente agnósticos al contexto donde se usa inicialmente, permite que sea más fácil usarlo en otro programa. Si el módulo de lectura de archivos PDF del ejemplo anterior es capaz de leer cualquier archivo PDF que se le entregue, entonces ese módulo puede ser completamente reutilizado por otros programas, distintos al del ejemplo, que necesiten esa misma funcionalidad"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Abstracción de componentes\n",
    "\n",
    "El concepto de modularización, además de organizar mejor el código de un programa, permite un mejor proceso de conceptualización del flujo de éste. Al identificar distintos módulos, y las dependencias entre ellos, se permite la abstracción de los detalles de cada módulo y permite enfocarse en el funcionamiento general del programa. \n",
    "\n",
    "Cada módulo pasa a ser una caja negra que provee funcionalidades. Un ejemplo tangible es una radio: es un objeto con perillas y botones que nos permite encenderla, cambiar la estación, cambiar el volumen y apagarla. Internamente, no sabemos como funciona y como logra todas esas funciones, y no nos importa. El trabajar con módulos permite un trabajo similar. Al importar las funcionalidades de un módulo en un programa, solo nos importa **lo que hacen** esas funcionalidades, y **no el cómo lo hacen**. \n",
    "\n",
    "```python\n",
    "import radio\n",
    "\n",
    "\n",
    "radio.encender()\n",
    "radio.cambiar_estacion(99.3)\n",
    "radio.subir_volumen(10)\n",
    "radio.apagar()\n",
    "```"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un ejemplo de código **real**, es la librería (y módulo) `random` de Python. En ella podemos encontrar varias funciones que permiten generar elementos aleatorios. ¿Cómo lo hace? No se sabe, a menos que se lea en detalle su código. Esto permite utilizarlo en cualquier contexto donde necesite aleatoriedad, sin la preocupación de implementar efectivamente esa funcionalidad."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "9\n",
      "a\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "\n",
    "\n",
    "print(random.randint(1, 10))\n",
    "print(random.choice([\"a\", \"b\", \"c\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pensar en módulos permite separar las tareas de un programa en categorías y tipos; permite crear el mapa general de un programa de forma más sencilla y sin preocupación de los detalles. Hoy en día, la modularización es un concepto pilar del desarrollo de software. Pero este concepto no siempre fue obvio, en el siguiente [video](https://youtu.be/_jTc1BTFdIo), Barbara Liskov, ganadora del Premio Turing 2008, habla sobre los tiempos en que este concepto recién surgió y conecta la idea de modularización con la programación orientada a objetos (uno de los contenidos del curso que veremos próximamente)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "3.12.9",
   "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.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
