Unidad III · Tema 3.2 Disponible

Aspectos de implementación

Clases y objetos, ciclo de vida, atributos y métodos, y modificadores de acceso en Python, Java y C++.

Objetivo de aprendizaje

Al concluir este tema, el estudiante será capaz de:

  1. Describir qué ocurre en la memoria al crear un objeto (clase, instancia, constructor y self).
  2. Explicar el ciclo de vida de un objeto y comparar cómo lo administran Python, Java y C++ (conteo de referencias, recolector de basura, destrucción por ámbito).
  3. Diferenciar los atributos de clase de los de instancia y evitar la trampa del atributo mutable compartido.
  4. Distinguir los métodos de instancia, de clase y estáticos, y elegir el adecuado.
  5. Comparar los modificadores de acceso de Python, Java y C++, y usar propiedades para calcular o proteger atributos.

Ruta de trabajo del tema

Etapa Contenido Producto del estudiante
1 Planteamiento inicial Hipótesis sobre una lista compartida
2 Clase e instancia: qué ocurre al crear un objeto Descripción de los cuatro pasos
3 Ciclo de vida: constructores y destructores Predicción de cuándo se libera un objeto
4 Atributos de clase y de instancia Regla para evitar la trampa
5 Métodos de instancia, de clase y estáticos Criterio de elección
6 Modificadores de acceso y propiedades Clasificación en tres lenguajes
7 Verificación y reformulación Cuestionario y respuesta revisada

1. Planteamiento inicial

Considere el siguiente programa. Cada socio de la biblioteca guarda la lista de sus préstamos.

class Socio:
    prestamos = []

    def __init__(self, nombre):
        self.nombre = nombre

    def pedir(self, titulo):
        self.prestamos.append(titulo)


a = Socio("Ana")
b = Socio("Beto")
a.pedir("Fisica I")
b.pedir("Calculo")
print(a.prestamos)

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Esta hipótesis se retomará en la sección 4.


2. Clase e instancia: qué ocurre al crear un objeto

Definición — Clase e instancia: la clase es una plantilla que describe cómo serán los objetos (sus atributos y métodos). Una instancia es un objeto concreto creado a partir de ella. En Python, la clase es también un objeto: existe en la memoria, tiene un nombre ligado a ella y guarda sus métodos y atributos.

Cuando el programa ejecuta la sentencia class Socio: ..., no se crea ningún socio: se crea un objeto clase y se liga a él el nombre Socio. Esto puede observarse en la corrida: el cuerpo de la clase se ejecuta en un marco propio y, al terminar, sus nombres (métodos y atributos de clase) quedan guardados dentro del objeto clase.

Una llamada como Socio("Ana") ejecuta cuatro pasos:

Paso Qué ocurre Dónde se observa
1 Se busca el nombre Socio y se obtiene el objeto clase Marco global
2 Se crea un objeto nuevo, vacío, ligado a su clase Aparece una tarjeta de objeto sin atributos
3 Se ejecuta __init__ con el objeto nuevo como self y los argumentos Marco Socio.__init__ en la pila
4 Se devuelve el objeto y se liga al nombre de la izquierda El nombre apunta al objeto
class Socio:
    total = 0                      # atributo de CLASE: vive en el objeto clase

    def __init__(self, nombre):    # constructor: recibe el objeto recién creado en "self"
        self.nombre = nombre       # atributo de INSTANCIA: vive en el objeto de este socio
        Socio.total += 1           # se modifica el atributo de la clase


a = Socio("Ana")                   # pasos 1 a 4
b = Socio("Beto")
print(Socio.total, a.total, b.total)
a.total = 99                       # crea un atributo de instancia que OCULTA al de la clase
print(Socio.total, a.total, b.total)

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Active «Predecir la salida» y compare con el programa. En el panel de búsqueda de nombres observe dónde encuentra Python cada atributo.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Error común: creer que a.total = 99 cambia el contador de todos los socios. Asignar por medio de una instancia siempre crea o modifica el atributo de la instancia.


3. Ciclo de vida: constructores y destructores

Definición — Ciclo de vida de un objeto: etapas por las que pasa un objeto: construcción (se reserva y se inicializa), uso y destrucción (se libera su memoria y los recursos que posea). El constructor inicializa el objeto; el destructor (cuando existe) se ejecuta antes de que el objeto desaparezca.

En el tema 2.2 se estudió que la memoria se administra por conteo de referencias o por recolección de basura. Los objetos siguen exactamente esas reglas. En Python (CPython), cuando el conteo de un objeto llega a cero, el objeto se libera y se ejecuta su método __del__, si lo tiene.

class Conexion:
    def __init__(self, nombre):
        self.nombre = nombre
        print("abre", nombre)              # constructor: adquiere el recurso

    def __del__(self):
        print("cierra", self.nombre)       # destructor: libera el recurso


a = Conexion("A")                          # conteo de referencias: 1
b = a                                      # conteo: 2 (dos nombres, un objeto)
a = None                                   # conteo: 1 (el objeto sigue vivo)
print("A sigue viva")
b = None                                   # conteo: 0 -> se ejecuta __del__
print("fin")

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Active «Predecir la salida» y anticipe el orden de los mensajes.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Los ciclos de referencias

Si dos objetos se referencian entre sí, el conteo de cada uno nunca llega a cero aunque el programa ya no pueda alcanzarlos. Python complementa el conteo con un recolector de ciclos; el modelo de esta página, que solo usa conteo, muestra el problema.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Python, Java y C++ frente a frente

Aspecto Python (CPython) Java C++
Constructor __init__ (inicializa; el objeto ya fue creado) Constructor con el nombre de la clase Constructor con el nombre de la clase
Destructor __del__ (momento ligado al conteo; ejecución al salir del intérprete no garantizada) No hay destructor fiable; finalize está obsoleto Destructor ~Clase(), determinista
Quién libera Conteo de referencias y recolector de ciclos Recolector de basura de la JVM Al salir del ámbito (objetos locales) o manualmente (delete)
Recursos (archivos, conexiones) Administrador de contexto with try-with-resources RAII: el destructor libera el recurso

Definición — RAII: idioma de C++ (Resource Acquisition Is Initialization): un recurso se adquiere en el constructor de un objeto local y se libera en su destructor, de modo que el recurso se libera automáticamente al salir del bloque, incluso si ocurre un error.

Clasifique el mecanismo que se aplica en cada fragmento. Los fragmentos de Java y C++ son afirmaciones didácticas; no se ejecutan.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Importante: que CPython libere el objeto en cuanto su conteo llega a cero es un detalle de implementación de CPython, no una garantía del lenguaje Python. Otras implementaciones (por ejemplo PyPy) liberan con otra estrategia. Para recursos que deben cerrarse, se utiliza with, no __del__.


4. Atributos de clase y de instancia

Retome la pregunta del inicio. En el programa del socio, prestamos = [] está en el cuerpo de la clase: la lista se crea una vez, cuando se ejecuta class. Los objetos a y b no tienen un atributo prestamos propio; cuando se evalúa self.prestamos, Python no lo encuentra en el objeto y lo busca en la clase. Ambos encuentran la misma lista, y append la muta.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

La corrección es crear la lista dentro del constructor, de modo que cada objeto tenga la suya:

class Socio:
    def __init__(self, nombre):
        self.nombre = nombre
        self.prestamos = []        # atributo de INSTANCIA: una lista nueva por cada socio

Regla práctica: los atributos de clase son adecuados para constantes y valores compartidos inmutables (LIMITE = 3, PLAZO = 7). Los atributos que cada objeto debe tener por separado, sobre todo si son mutables, se crean en __init__.

Error común: self.n += 1 sobre un atributo de clase. Equivale a self.n = self.n + 1: lee el valor de la clase y crea un atributo de instancia con el resultado. El contador de la clase no cambia. Para incrementarlo se escribe Socio.n += 1.


5. Métodos de instancia, de clase y estáticos

Clase de método Recibe Se declara con Sirve para
De instancia El objeto (self) def metodo(self, ...) Operar sobre el estado de un objeto concreto
De clase La clase (cls) @classmethod Operaciones de la clase en su conjunto; constructores alternativos
Estático Nada @staticmethod Una función relacionada con la clase que no necesita ni objeto ni clase
class Socio:
    def __init__(self, matricula, nombre):
        self.matricula = matricula
        self.nombre = nombre

    def saludar(self):                            # de INSTANCIA: usa self
        return "Hola, " + self.nombre

    @classmethod
    def desde_texto(cls, texto):                  # de CLASE: recibe la clase en "cls"
        partes = texto.split(",")
        return cls(int(partes[0]), partes[1])     # construye una instancia: constructor alternativo

    @staticmethod
    def matricula_valida(m):                      # ESTÁTICO: no recibe ni self ni cls
        return m > 2000000


s = Socio.desde_texto("2024001,Ana")              # se invoca sobre la clase
print(s.saludar())                                # se invoca sobre la instancia
print(Socio.matricula_valida(s.matricula))

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Nota: en Java y en C++ existen los equivalentes: los miembros declarados con static pertenecen a la clase y no a cada objeto. Java no tiene un equivalente directo de @classmethod; los métodos estáticos de fábrica (static Socio desdeTexto(String t)) cumplen ese papel.


6. Modificadores de acceso y propiedades

Definición — Modificador de acceso: declaración que indica desde qué código puede utilizarse un miembro de una clase. Los habituales son público (cualquiera), protegido (la clase y sus subclases) y privado (solo la clase).

Nivel Python Java C++
Público Cualquier nombre sin guion bajo inicial public public
Protegido Convención: un guion bajo inicial (_x) protected (también acceso por paquete) protected
Privado Convención reforzada: doble guion bajo (__x, name mangling) private private (por omisión en class)
Quién lo verifica Nadie: es disciplina del programador El compilador El compilador

La diferencia esencial es quién hace cumplir la regla. En Java y C++ un acceso indebido no compila. En Python, la regla es de convención: el doble guion bajo transforma el nombre (__pin se almacena como _Cuenta__pin) para evitar colisiones y accesos accidentales, pero basta conocer la regla para acceder.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Los fragmentos siguientes están en tres lenguajes. Determine qué ocurre con cada uno. Los de Java y C++ son afirmaciones didácticas; no se ejecutan.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Propiedades: atributos que ejecutan código

Una propiedad se lee y se escribe como un atributo, pero ejecuta un método. Permite (1) calcular un valor a partir de otros, sin almacenarlo, y (2) validar al asignar, sin cambiar la sintaxis de quien usa la clase.

class Prestamo:
    MULTA_DIA = 5

    def __init__(self, vence):
        self.vence = vence
        self.hoy = vence

    @property
    def retraso(self):                     # se calcula cada vez que se lee
        return max(0, self.hoy - self.vence)

    @property
    def multa(self):                       # usa otra propiedad
        return self.retraso * Prestamo.MULTA_DIA


p = Prestamo(7)
p.hoy = 12
print(p.retraso, p.multa)                  # 5 25
p.hoy = 5
print(p.retraso, p.multa)                  # 0 0

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Error común: guardar como atributo algo que puede calcularse. Si se guardara multa como dato, habría que acordarse de actualizarla cada vez que cambie hoy o vence.


7. Verificación y reformulación

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.


8. Libreta de trabajo

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.


9. Preguntas de reflexión

  1. Explique con el modelo de objetos y referencias por qué dos socios creados con la clase de la sección 1 comparten la lista de préstamos. ¿Qué cambiaría si prestamos se asignara con una tupla en lugar de una lista?
  2. ¿Qué ventajas tiene la destrucción determinista de C++ frente a la recolección de basura de Java? ¿Qué costo tiene? Relacione su respuesta con los criterios de la pragmática (tema 1.2.3).
  3. Si Python no verifica el acceso a los atributos «privados», ¿para qué sirve el name mangling? ¿Qué problema concreto evita?
  4. Proponga una clase de su dominio en la que un método de clase sea un constructor alternativo útil.
  5. ¿En qué situaciones no debería usarse una propiedad calculada? Considere el costo del cálculo y los efectos secundarios.

10. Lecturas recomendadas

  • Sebesta, R. (2015). Concepts of programming languages (11th ed.), capítulo sobre soporte para la programación orientada a objetos (elementos de clases, constructores y destructores, control de acceso). Pearson.
  • Python Software Foundation. The Python Language Reference, sección «Data model» (__init__, __del__, atributos de clase y de instancia). Acceso libre en docs.python.org.
  • Python Software Foundation. The Python Tutorial, secciones «Classes» y «Private Variables». Acceso libre en docs.python.org.
  • Stroustrup, B. (2013). The C++ programming language (4th ed.). Addison-Wesley. (Constructores, destructores y RAII.)