Objetivo de aprendizaje
Al concluir este tema, el estudiante será capaz de:
- Describir un programa orientado a objetos con los términos objeto (identidad, estado y comportamiento), clase y mensaje.
- Distinguir abstracción (qué se expone) de encapsulamiento (cómo se protege) y reconocer la interfaz de una clase.
- Aplicar el encapsulamiento para conservar los invariantes de un objeto.
- Explicar la herencia y la búsqueda de métodos en una jerarquía (MRO), y distinguir «es un» de «tiene un».
- Predecir qué implementación se ejecuta cuando varios objetos responden a la misma llamada (polimorfismo de subtipo).
Ruta de trabajo del tema
| Etapa | Contenido | Producto del estudiante |
|---|---|---|
| 1 | Planteamiento inicial | Hipótesis escrita |
| 2 | Objetos: identidad, estado y comportamiento | Descripción de un programa con objetos |
| 3 | Abstracción: interfaz e implementación | Distinción entre «qué» y «cómo» |
| 4 | Encapsulamiento: proteger los invariantes | Regla propia sobre quién valida |
| 5 | Herencia y búsqueda de métodos | Predicción del método que se ejecuta |
| 6 | Polimorfismo y despacho dinámico | Predicción con una colección heterogénea |
| 7 | Los cuatro conceptos juntos | Clasificación de situaciones |
| 8 | Python, Java y C++ frente a frente | Comparación de las decisiones de cada lenguaje |
| 9 | Verificación y reformulación | Cuestionario y respuesta revisada |
1. Planteamiento inicial
En el tema 3.0 se modeló la biblioteca con un material genérico. La biblioteca, sin embargo, presta cosas distintas: los libros por siete días, las revistas por tres y las tesis solo para consulta en sala. Se necesita un programa que, dado cualquier material, indique cuántos días puede prestarse.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Esta hipótesis se retomará al final de la sección 6.
2. Objetos: identidad, estado y comportamiento
Definición — Objeto: entidad que tiene identidad (es distinguible de cualquier otra, aunque tenga los mismos datos), estado (los valores de sus atributos en un momento dado) y comportamiento (las operaciones que sabe realizar, los métodos). Los objetos colaboran enviándose mensajes: invocar
libro.prestar()es enviar el mensaje «prestar» al objetolibro.
La idea de objeto no es nueva para quien estudió la Unidad II: es el mismo «objeto» del modelo de nombres y objetos del tema 2.1. La diferencia es que ahora el programador define el tipo del objeto: la clase.
| Concepto de la Unidad II | Su significado en este tema |
|---|---|
| Un nombre se liga a un objeto | a = Libro("Fisica I") liga el nombre a a un objeto de la clase Libro |
Igualdad (==) frente a identidad (is) |
Dos libros con el mismo título son objetos distintos; sin más información, == compara identidad |
| Alias | c = a no crea otro libro: el estado es compartido |
| Marco de activación | Cada llamada a un método crea uno, con self ligado al objeto receptor |
Ejecute el programa y compare las etiquetas @n de los objetos: observe cuántos libros existen y qué nombres comparten uno.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Error común: creer que dos objetos con los mismos datos son «el mismo». Son objetos distintos con estados iguales (en un momento dado), que pueden diverger después.
3. Abstracción: interfaz e implementación
Definición — Abstracción: resaltar los rasgos esenciales de un concepto y omitir los detalles irrelevantes. En una clase, la abstracción define su interfaz: el conjunto de operaciones que se ofrecen a quien la usa. Todo lo demás es implementación y puede cambiar sin que nadie lo note.
En el tema 3.0 la abstracción se utilizó para decidir qué datos modelar; aquí se usa para decidir qué ofrece una clase. Observe el siguiente ejemplo: dos catálogos distintos ofrecen la misma interfaz (agregar y existe) y una función que solo conoce la interfaz funciona con ambos.
# Primera implementación: guarda los títulos en una lista.
class CatalogoEnLista:
def __init__(self):
self._titulos = [] # detalle interno: una lista
def agregar(self, titulo):
self._titulos.append(titulo)
def existe(self, titulo):
return titulo in self._titulos
# Segunda implementación: guarda los títulos en un diccionario.
class CatalogoEnDiccionario:
def __init__(self):
self._titulos = {} # detalle interno: un diccionario
def agregar(self, titulo):
self._titulos[titulo] = True
def existe(self, titulo):
return titulo in self._titulos
# Esta función solo conoce la INTERFAZ (agregar, existe), no la implementación.
def cargar(catalogo):
catalogo.agregar("Fisica I")
catalogo.agregar("Calculo")
return catalogo.existe("Calculo")
print(cargar(CatalogoEnLista()))
print(cargar(CatalogoEnDiccionario()))
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Error común: confundir abstracción con encapsulamiento. La abstracción decide qué se ofrece; el encapsulamiento decide cómo se protege lo interno. Son decisiones distintas y complementarias.
4. Encapsulamiento: proteger los invariantes
Definición — Encapsulamiento: reunir en la clase los datos y las operaciones que los modifican, y restringir el acceso directo al estado interno, de modo que solo pueda cambiar mediante la interfaz. Su propósito es conservar los invariantes del objeto.
Un invariante es una condición que debe cumplirse siempre. Para un material, un invariante razonable es que las copias disponibles nunca sean negativas. Observe qué ocurre cuando nada protege el estado.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
La solución es doble: (1) el estado se vuelve interno (no se modifica desde fuera) y (2) la clase ofrece operaciones que validan antes de modificarlo.
class Material:
def __init__(self, titulo, copias):
self.titulo = titulo
self.__copias = copias # atributo interno (doble guion bajo)
@property
def copias(self): # lectura permitida, como si fuera un atributo
return self.__copias
def prestar(self):
if self.__copias == 0: # se protege el invariante: nunca menos de cero
raise ValueError("No quedan copias")
self.__copias = self.__copias - 1
m = Material("Fisica I", 1)
m.prestar()
try:
m.prestar() # viola el invariante: se rechaza
except ValueError as e:
print("Rechazado:", e)
try:
m.copias = -40 # la propiedad no tiene setter: se rechaza
except AttributeError:
print("No se puede asignar directamente")
print(m.copias)
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Nota: en Python el encapsulamiento se apoya en convenciones (el guion bajo) y en propiedades; el lenguaje no impide el acceso de manera absoluta. En Java y C++, el compilador sí lo verifica. Este contraste se estudia en el tema 3.2.
Error común: pensar que encapsular es ocultar por ocultar. Si un atributo no tiene reglas, protegerlo con métodos de acceso triviales solo agrega código. El valor del encapsulamiento está en proteger invariantes.
5. Herencia y búsqueda de métodos
Definición — Herencia: mecanismo por el cual una clase (la subclase) reutiliza y especializa el comportamiento de otra (la superclase). Expresa una relación «es un»: un libro es un material.
La herencia permite escribir una vez lo común (título y código) y que cada subclase agregue lo propio (autor, número). La subclase puede sobrescribir un método para cambiar su comportamiento y reutilizar el de la superclase con super().
class Material: # superclase: lo común a todo material
def __init__(self, codigo, titulo):
self.codigo = codigo
self.titulo = titulo
def describir(self):
return self.titulo + " (" + str(self.codigo) + ")"
class Libro(Material): # Libro ES UN Material
def __init__(self, codigo, titulo, autor):
super().__init__(codigo, titulo) # reutiliza el constructor de Material
self.autor = autor # y agrega lo propio
class Revista(Material): # Revista ES UN Material
def __init__(self, codigo, titulo, numero):
super().__init__(codigo, titulo)
self.numero = numero
def describir(self): # SOBRESCRIBE el método de Material
return super().describir() + " n." + str(self.numero) # y reutiliza el original
l = Libro(101, "Fisica I", "Resnick")
r = Revista(202, "Ciencia Hoy", 15)
print(l.describir()) # se encuentra en Material
print(r.describir()) # se encuentra en Revista
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Definición — Resolución de métodos (MRO): orden en que Python busca un atributo o un método. Primero se busca en el objeto, luego en su clase y después en cada clase base, en el orden del MRO. Se ejecuta la primera coincidencia. Puede consultarse el orden de una clase con el nombre de la clase seguido de
.mro()en un intérprete real.
Active en la corrida siguiente «Predecir el método que se ejecuta». Antes de cada llamada, escriba qué implementación espera (por ejemplo, Material.describir).
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Herencia múltiple
Python permite que una clase herede de varias. El orden de las superclases en la declaración determina el MRO, y con él, qué método se ejecuta.
class A:
def f(self):
return "A"
class B(A):
def f(self):
return "B" + super().f()
class C(A):
def f(self):
return "C" + super().f()
class D(B, C):
def f(self):
return "D" + super().f()
print(D().f()) # DBCA
El MRO de D es D → B → C → A → object. Observe que super() no significa «la superclase» sino «la siguiente clase del MRO»: por eso la llamada de B continúa en C y no directamente en A.
Error común: usar herencia para reutilizar código cuando la relación «es un» no es verdadera. Una
Bibliotecaque hereda deListapara reutilizarappendafirma que «una biblioteca es una lista». Cuando solo se necesita usar otra clase, se utiliza composición (un atributo que guarda el objeto), tema que se retoma en 3.3.
6. Polimorfismo y despacho dinámico
Definición — Polimorfismo de subtipo: capacidad de tratar objetos de distintas clases de manera uniforme porque comparten una interfaz, y de que cada uno responda con su propia implementación. La implementación que se ejecuta depende del tipo real del objeto en ejecución (despacho dinámico), no de cómo esté escrito el código que la invoca.
Retome la pregunta del inicio. Con herencia, el programa que calcula plazos no necesita conocer los tipos concretos.
class Material:
def plazo(self):
return 5 # plazo por omisión
class Libro(Material):
def plazo(self):
return 7
class Revista(Material):
def plazo(self):
return 3
class Tesis(Material): # no redefine plazo: hereda el de Material
pass
acervo = [Libro(), Revista(), Tesis(), Material()]
total = 0
for m in acervo: # la MISMA línea de código en cada vuelta...
print(type(m).__name__, m.plazo()) # ...ejecuta la implementación de la clase real
total = total + m.plazo()
print(total)
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Active «Predecir el método que se ejecuta» y anticipe, en cada vuelta, qué implementación corresponde.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Definición — Principio abierto/cerrado: el código debe estar abierto a la extensión (se pueden agregar tipos nuevos) y cerrado a la modificación (agregarlos no obliga a cambiar el código que ya funciona). El polimorfismo es el mecanismo que lo hace posible.
7. Los cuatro conceptos juntos
| Concepto | Problema que resuelve | Pregunta que lo identifica |
|---|---|---|
| Abstracción | La complejidad: no todo debe conocerse para usar algo | ¿Qué ofrece esta clase y qué oculta? |
| Encapsulamiento | Los estados inválidos: nadie debe romper las reglas | ¿Quién puede modificar este dato y bajo qué condiciones? |
| Herencia | La duplicación: lo común se escribe una vez | ¿«Es un» es verdadero en todos los casos? |
| Polimorfismo | La rigidez: agregar tipos obliga a modificar código | ¿La misma llamada funciona con tipos distintos? |
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
8. Python, Java y C++ frente a frente
Los cuatro conceptos existen en los tres lenguajes, pero cada uno decide cómo se expresan. Los fragmentos de Java y C++ se incluyen para comparar; no se ejecutan en esta página.
| Concepto | Python | Java | C++ |
|---|---|---|---|
| Abstracción | Clases base con métodos que fallan o clases abstractas (ABC) |
interface y abstract class |
Clases con métodos virtuales puros |
| Encapsulamiento | Convención (_x) y name mangling (__x); no se verifica el acceso |
private, protected, public; el compilador lo verifica |
private, protected, public; el compilador lo verifica |
| Herencia | Múltiple, con MRO | Simple de clases; múltiple de interfaces | Múltiple |
| Polimorfismo de subtipo | Siempre dinámico | Dinámico por omisión en métodos de instancia | Solo si el método es virtual |
El último punto es una diferencia importante. En Java, todo método de instancia se despacha dinámicamente. En C++, hay que pedirlo con virtual:
// Java: el despacho es dinámico por omisión.
abstract class Material {
abstract int plazo();
}
class Libro extends Material {
int plazo() { return 7; }
}
class Revista extends Material {
int plazo() { return 3; }
}
// uso:
Material m = new Libro();
System.out.println(m.plazo()); // 7: se ejecuta Libro.plazo()
// C++: el despacho dinámico se solicita con "virtual".
class Material {
public:
virtual int plazo() const { return 5; } // sin "virtual", m->plazo() daría 5
virtual ~Material() = default;
};
class Libro : public Material {
public:
int plazo() const override { return 7; }
};
// uso:
Material* m = new Libro();
std::cout << m->plazo(); // 7 gracias a "virtual"
delete m;
Importante: estos fragmentos son afirmaciones didácticas, no resultados ejecutados por esta página. Verifíquelos con un compilador si los muestra en clase. La diferencia de fondo es de diseño: C++ no cobra el costo del despacho dinámico a quien no lo pide; Java y Python lo hacen siempre.
9. Errores conceptuales frecuentes
Error común: creer que «usar clases» es lo mismo que «hacer programación orientada a objetos». Una clase que solo agrupa funciones sin proteger estado ni expresar relaciones aporta poco.
Error común: pensar que la herencia es la herramienta principal de la programación orientada a objetos. Con frecuencia es más flexible la composición.
Error común: creer que
super()siempre llama a «la clase padre». Llama a la siguiente clase del MRO.
Error común: asumir que lo que se ve en la variable (su nombre, su aparente tipo) determina el método que se ejecuta. En Python y en Java lo determina el objeto real.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
10. Libreta de trabajo
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
11. Preguntas de reflexión
- En el programa de los catálogos (sección 3), ¿qué habría que cambiar en
cargarsi se agregara una tercera implementación? ¿Y si se cambiara el nombre del métodoexiste? Relacione sus respuestas con el concepto de interfaz. - Elija una clase con atributos sin reglas (por ejemplo, un punto con
xey). ¿Tiene sentido encapsularla? ¿Qué cambiaría su respuesta? - ¿Qué ventajas y qué riesgos tiene que una subclase sobrescriba un método de su superclase? Dé un ejemplo en el que la sobrescritura rompa lo que el resto del programa espera.
- Explique con sus palabras por qué
super()no significa «la clase padre». ¿En qué situación la diferencia importa? - ¿Qué diferencia hay entre decidir en el código (con
ifpor tipo) y decidir mediante polimorfismo? ¿Cuándo preferiría cada una?
12. Lecturas recomendadas
- Sebesta, R. (2015). Concepts of programming languages (11th ed.), capítulo sobre soporte para la programación orientada a objetos. Pearson.
- Python Software Foundation. The Python Tutorial, sección «Classes» (herencia y herencia múltiple). Acceso libre en docs.python.org.
- Python Software Foundation. The Python Language Reference, sección «Data model». Acceso libre en docs.python.org.
- Meyer, B. (1997). Object-oriented software construction (2nd ed.). Prentice Hall. (Origen del principio abierto/cerrado.)