{
 "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'> Actualizados el 2026-1.</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Variables de clases y de instancia](#Variables-de-clases-y-de-instancia)\n",
    "2. [Encapsulamiento](#Encapsulamiento)\n",
    "    1. [Encapsulamiento e interfaces](#Encapsulamiento-e-interfaces)\n",
    "    2. [Encapsulamiento en Python](#Encapsulamiento-en-Python)\n",
    "3. [Aprender más](#Aprender-más)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Variables de clases y de instancia\n",
    "\n",
    "Muchas veces, en OOP, necesitamos tener un valor que sea compartido por todas las instancias de una clase, pero que esta a su vez sea una variable propia de la clase. Para esto, podemos definir una variable de clase, simplemente declarándola dentro de su definición. Luego, para utilizarla basta llamarla como si fuera un atributo de la clase. Supongamos que nos piden implementar el sistema registro de personas en el registro civil. Sabemos que todas las personas tienen un RUN (o RUT), y que este es único (dos personas no pueden tener un mismo RUN). Es claro que podríamos implementar una clase `Persona` que tenga un atributo `run`, pero ¿cómo nos aseguramos que todas las personas tengan un `run` único? En términos de programación,  ¿cómo podemos hacer que cada instancia de `Persona` tenga un `run` único? Para esto podemos utilizar una variable de clase. A continuación, se muestra un ejemplo de cómo podríamos implementar esto en Python, utilizando variables de clase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hola, soy Pablo y mi run es 1\n",
      "Hola, soy Dani y mi run es 2\n",
      "Hola, soy Fran y mi run es 3\n",
      "Hola, soy Lucas y mi run es 4\n",
      "Hola, soy Paqui y mi run es 5\n"
     ]
    }
   ],
   "source": [
    "class Persona:\n",
    "    run = 1\n",
    "    \n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "        self.run = Persona.run\n",
    "        Persona.run += 1\n",
    "    \n",
    "    def presentarse(self):\n",
    "        print(f'Hola, soy {self.nombre} y mi run es {self.run}')\n",
    "\n",
    "p1 = Persona('Jaime')\n",
    "p2 = Persona('Pablo')\n",
    "p3 = Persona('Toño')\n",
    "p4 = Persona('Tama')\n",
    "p5 = Persona('Dani')\n",
    "\n",
    "p1.presentarse()\n",
    "p2.presentarse()\n",
    "p3.presentarse()\n",
    "p4.presentarse()\n",
    "p5.presentarse()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, cada persona que se registre tendrá un run único. El run de cada instancia pasa a ser propio de la instancia, y el valor del siguiente run quedará guardado en la variable de clase."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Encapsulamiento\n",
    "\n",
    "Una característica muy favorecida en OOP es el **encapsulamiento**. El encapsulamiento se refiere al ocultamiento de los atributos de un objeto de manera que éstos sólo puedan ser modificados mediante los métodos que el programador defina. Esto evita que un objeto que interactúa con otro pueda observar o modificar elementos que sean internos al funcionamiento del objeto, y obliga a que toda la interacción sea de la manera que el programador la definió, contribuyendo a reducir la ocurrencia de errores.\n",
    "\n",
    "Usemos un objeto clase `Auto` para ejemplificar este encapsulamiento: \n",
    "\n",
    ">    Un `Auto` tiene atributos `color` y `modelo` que son interesantes para un objeto de la clase `Persona` que quiera interactuar con él. Sin embargo, también puede haber atributos como `disco_de_embrague`, o `palanca_de_cambios`, que también son atributos de `Auto`, pero son internos a la construcción y al funcionamiento de un objeto de clase `Auto`. Otros objetos de clase `Auto`, o de otras clases como `Persona` no necesitan interactuar con ellos, o al menos no directamente. Los atributos `disco_de_embrague` y `palanca_de_cambios` están **encapsulados** dentro de la clase `Auto`.\n",
    "\n",
    "\n",
    "\n",
    "El encapsulamiento nos ayudará a alcanzar un mejor nivel de abstracción en el modelamiento de nuestros programas al definir qué atributos de un objeto son de interés para otros objetos y cuáles atributos son de interés únicamente para el comportamiento interno del objeto y, por lo tanto, deberían permanecer ocultos o _encapsulados_ dentro del objeto. Veremos que un correcto uso del encapsulamiento de atributos lleva a un código más limpio.\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Encapsulamiento e interfaces\n",
    "\n",
    "En programación, una [interfaz](https://es.wikipedia.org/wiki/Interfaz_(programaci%C3%B3n)) es una _fachada_ para proteger la implementación de una clase e interactuar con otros objetos. La interfaz define el **conjunto de atributos y métodos** de un objeto que son _expuestos_ u ofrecidos por la clase para poder interactuar con otros objetos.\n",
    "\n",
    "Utilizan el ejemplo de la clase `Auto`:\n",
    "\n",
    "> Una interfaz que puede ser ofrecida a un objeto de clase `Conductor` puede incluir atributos como `marca` y `modelo` y `año`, y métodos como `conducir`, `vender`, ó `pintar`. No queremos que un objeto de clase `Conductor` pueda modificar directamente el atributo `kilometraje`, sino que sólo lo puede hacer utilizando el método `conducir`. Por otro lado, si consideramos la interacción con un objeto de clase `Mecánico`, podríamos pensar en una interfaz con atributos como `nivel_de_aceite`, y métodos como `abrir_capot`, `cambiar_vidrios` o `cambiar_aceite`. \n",
    "\n",
    "El nivel de detalle de la interfaz se denomina **abstracción**. En el siguiente ejemplo, todos los atributos `marca`, `año`, `kilometraje` y los métodos `conducir`, `pintar`, `vender` siguen siendo parte de la clase `Auto`. Sin embargo, hemos definido una **interfaz** con cierto nivel de abstracción para interactuar con la clase `Conductor`, ocultando o abstrayendo al `Conductor` de detalles internos del `Auto`, por ejemplo, esa interfaz no incluye el atributo `kilometraje` para que así el `Conductor` no lo modifique.\n",
    "\n",
    "![](data/OOP_interfaz.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Encapsulamiento en Python\n",
    "\n",
    "En lenguajes tradicionales que usan OOP, como por ejemplo Java y C#, es posible definir atributos o métodos que pueden ser accedidos desde fuera del objeto (*públicos*), y otros que sólo pueden ser utilizados internamente (*privados*). Esta característica es usada para implementar **encapsulamiento**. Al declarar algunos atributos o métodos como *privados*, estos prohibiendo que puedan ser invocados desde código externo a la clase, y el lenguaje evita que el programador viole estas reglas.\n",
    "\n",
    "En Python esta diferencia no existe, y **todos los atributos y métodos de un objeto son públicos**. Esto complicaría la implementación del encapsulamiento en Python. Sin embargo, existe una convención que permite _sugerir_ que un método o atributo es de uso únicamente interno. Esto se hace agregando un caracter _underscore_ (`_`) al inicio del atributo o método, como en el siguiente ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Estoy convirtiendo una señal eléctrica en la imagen que estás viendo.\n",
      "Televisor Samsung en canal 5\n"
     ]
    }
   ],
   "source": [
    "class Televisor:\n",
    "    \"\"\"\n",
    "    Clase que modela un televisor con encendido/apagado,\n",
    "    cambio de canal y decodificación de imagen.\n",
    "\n",
    "    Atributos:\n",
    "        pulgadas (int): Tamaño del televisor en pulgadas.\n",
    "        marca (str): Marca del televisor.\n",
    "        encendido (bool): Estado del televisor, True si está encendido.\n",
    "        canal_actual (int): Canal que se está reproduciendo actualmente.\n",
    "        _clave (str): Clave interna del televisor (privada).\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, pulgadas, marca):\n",
    "        \"\"\"\n",
    "        Inicializa un televisor con tamaño, marca y estado por defecto.\n",
    "        \n",
    "        Args:\n",
    "            pulgadas (int): Tamaño del televisor en pulgadas.\n",
    "            marca (str): Marca del televisor.\n",
    "        \"\"\"\n",
    "        self.pulgadas = pulgadas\n",
    "        self.marca = marca\n",
    "        self.encendido = False\n",
    "        self.canal_actual = 0\n",
    "        self._clave = \"tv123\"  # Atributo privado, no se debe acceder fuera de la clase\n",
    "\n",
    "    def encender(self):\n",
    "        \"\"\"Enciende el televisor.\"\"\"\n",
    "        self.encendido = True\n",
    "\n",
    "    def apagar(self):\n",
    "        \"\"\"Apaga el televisor.\"\"\"\n",
    "        self.encendido = False\n",
    "\n",
    "    def cambiar_canal(self, nuevo_canal):\n",
    "        \"\"\"\n",
    "        Cambia el canal actual del televisor y decodifica la imagen.\n",
    "\n",
    "        Args:\n",
    "            nuevo_canal (int): Número del nuevo canal.\n",
    "        \"\"\"\n",
    "        self._decodificar_imagen()\n",
    "        self.canal_actual = nuevo_canal\n",
    "\n",
    "    def _decodificar_imagen(self) -> None:\n",
    "        \"\"\"\n",
    "        Método privado que simula la decodificación de la señal\n",
    "        eléctrica a imagen visible. No debe ser llamado desde fuera de la clase.\n",
    "        \"\"\"\n",
    "        print(\"Estoy convirtiendo una señal eléctrica en la imagen que estás viendo.\")\n",
    "\n",
    "\n",
    "# Ejemplo de uso\n",
    "tv = Televisor(55, \"Samsung\")\n",
    "tv.encender()\n",
    "tv.cambiar_canal(5)\n",
    "print(f\"Televisor {tv.marca} en canal {tv.canal_actual}\")\n",
    "tv.apagar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, podemos notar que la clase `Televisor` tiene los métodos `encender`, `apagar`, `cambiar_canal` y `_decodificar_imagen`. Digamos que queremos crear objetos de la clase `Televisor`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "televisor1 = Televisor(17, 'zony')\n",
    "televisor2 = Televisor(21, 'zamsung')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Estos televisores que hemos creado deberían poder ser encendidos y apagados. También deberíamos poder cambiar el canal.\n",
    "Pero no necesitamos decirle al televisor que decodifique la imagen. Ésta es una operación que se realiza automáticamente, cada vez que se cambia el canal. Como el método `_decodificar_imagen` empieza con *underscore*, **por convención** éste no debe ser llamado fuera de la clase. Lo mismo ocurre con el atributo `_clave`, que es un parámetro interno del televisor. Sin embargo, como todo esto sólo una convención, **aún podemos acceder a ellos directamente**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Estoy convirtiendo una señal eléctrica en la imagen que estás viendo.\n",
      "No debería poder leer que la clave es tv123\n"
     ]
    }
   ],
   "source": [
    "televisor1._decodificar_imagen()      ## Esto no debería hacerlo, pero lo estoy haciendo igual :(\n",
    "print(f\"No debería poder leer que la clave es {televisor1._clave}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### (Bonus) Atributos Privados en Python\n",
    "\n",
    "Si bien fijamos una convención para definir atributos privados anteponiéndole un guión bajo, pareciera que esta convención no hace realmente ninguna diferencia para el intérprete de Python (aun que sí para nuestra propia comprensión del código). Si queremos (casi) realmente tener atributos y métodos que no puedan ser llamados directamente, podemos iniciar con _double underscore_ como en el siguiente ejemplo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Televisor:\n",
    "    ''' Clase que modela un televisor.\n",
    "    '''\n",
    "    \n",
    "    def __init__(self, pulgadas, marca):\n",
    "        self.pulgadas = pulgadas\n",
    "        self.marca = marca\n",
    "        self.encendido = False\n",
    "        self.canal_actual = 0\n",
    "        self._clave = \"tv123\"\n",
    "        self.__clavesecreta = \"tv456\"\n",
    "        \n",
    "    def encender(self):\n",
    "        self.encendido = True\n",
    "        \n",
    "    def apagar(self):\n",
    "        self.encendido = False\n",
    "        \n",
    "    def cambiar_canal(self, nuevo_canal):\n",
    "        self._decodificar_imagen()\n",
    "        self.canal_actual = nuevo_canal\n",
    "        \n",
    "    def _decodificar_imagen(self):\n",
    "        print(\"Estoy convirtiendo una señal eléctrica en la imagen que estás viendo.\")\n",
    "        \n",
    "    def __mostrar_canal_prohibido(self):\n",
    "        print(\"Esto permite ver el canal del curling, pero usted no debe saberlo.\")\n",
    "\n",
    "televisor1 = Televisor(17, 'zony')\n",
    "televisor2 = Televisor(21, 'zamsung')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y entonces si queremos acceder a estos elementos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Televisor' object has no attribute '__clavesecreta'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[6], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mtelevisor1\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__clavesecreta\u001b[49m)\n",
      "\u001b[0;31mAttributeError\u001b[0m: 'Televisor' object has no attribute '__clavesecreta'"
     ]
    }
   ],
   "source": [
    "print(televisor1.__clavesecreta)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Televisor' object has no attribute '__mostrar_canal_prohibido'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[7], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtelevisor1\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__mostrar_canal_prohibido\u001b[49m()\n",
      "\u001b[0;31mAttributeError\u001b[0m: 'Televisor' object has no attribute '__mostrar_canal_prohibido'"
     ]
    }
   ],
   "source": [
    "televisor1.__mostrar_canal_prohibido()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos ver que, a pesar que los atributos existen, Python pareciera ser incapaz de encontrarlos y nos arroja un error. La verdad es que todo esto es un **truco** de la implementación de Python (y Python tiene muchos) para proveer algo similar a los atributos y métodos privados. Cuando un atributo o método empieza con *doble underscore*, Python reemplaza internamente sus nombres por `_NombreDeLaClase__atributo_o_metodo_secreto`, y por lo tanto podemos ser más astutos y escribir:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Esto permite ver el canal del curling, pero usted no debe saberlo.\n",
      "Ahora sí puedo ver que la clave secreta es tv456\n"
     ]
    }
   ],
   "source": [
    "televisor1._Televisor__mostrar_canal_prohibido()\n",
    "print(f\"Ahora sí puedo ver que la clave secreta es {televisor1._Televisor__clavesecreta}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este truco se conoce como _name mangling_. No ocurre, sin embargo, cuando el nombre del método termina también con *doble underscore*, por lo cual sí podemos llamar directamente métodos como `televisor1.__str__()`. Estas características son, en cualquier caso, exclusivas de Python y su objetivo es disminuir la posibilidad de errores por parte del programador al proveer algo que simula la existencia de atributos y métodos privados en un lenguaje que por diseño no los tiene."
   ]
  }
 ],
 "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.11.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
