# programacion-orientada-a-objetos-en-python
Programación Orientada a Objetos en Python
Volver a la portada del cursoPrincipios fundamentales de POO
Sección 1 · Introducción a POO
Principios fundamentales de POO
Una tienda en línea con un solo producto se maneja con un diccionario y dos funciones. Con tres tipos de producto, descuentos y carritos de varios clientes, ese código empieza a repetir validaciones en todos lados y cualquier cambio rompe algo lejano. La programación orientada a objetos (POO) propone otra organización: juntar los datos y las operaciones que les corresponden en una misma unidad. En esta lectura fijamos el vocabulario y los cuatro pilares usando PyShop, la tienda que construiremos durante el curso.
El vocabulario básico
Una clase es el molde: describe qué datos tiene un producto y qué sabe hacer. Un objeto (o instancia) es cada producto concreto creado con ese molde: la taza y el teclado son dos objetos de la misma clase. Los atributos son los datos de cada objeto (nombre, precio, stock) y en conjunto forman su estado. Los métodos son funciones definidas dentro de la clase (reponer, subtotal) y forman su comportamiento.
taza = ProductoFisico("Taza", 149.0, stock=20, peso_kg=0.4)
teclado = ProductoFisico("Teclado", 899.0, stock=3, peso_kg=1.2)
print(taza.stock, teclado.stock) # 20 3
taza.reponer(5) # comportamiento que cambia el estado
print(taza.stock, teclado.stock) # 25 3: solo cambió la taza
Producto.reponer(taza, -5) # equivale a taza.reponer(-5)
print(taza.stock) # 20
La última llamada explica self. Cuando escribes taza.reponer(5), Python busca reponer en la clase y la llama pasando el propio objeto como primer argumento. Ese primer parámetro se llama self por convención: es la forma en que el método sabe a qué producto debe sumarle stock. No es una palabra reservada, pero cambiarle el nombre solo confunde a quien lea tu código.
Los cuatro pilares en la tienda
Encapsulación: el objeto protege su estado. En PyShop el precio pasa por un @property que rechaza valores no positivos, así que nadie puede dejar la taza con precio negativo por accidente. Abstracción: se define qué debe saber hacer algo sin decir cómo. Producto declara que todo producto calcula su envío, pero no lo implementa; por eso no se puede instanciar directamente. Herencia: ProductoFisico y ProductoDigital reutilizan todo lo de Producto y solo agregan lo que los distingue. Polimorfismo: el mismo mensaje, costo_envio, produce resultados distintos según el tipo de objeto que lo recibe.
try:
taza.precio = -10
except ValueError as e:
print("Encapsulación:", e) # Precio inválido para Taza: -10
try:
Producto("X", 10)
except TypeError as e:
print("Abstracción:", e) # Can't instantiate abstract class Producto ...
ebook = ProductoDigital("Ebook de Python", 299.0)
print(isinstance(ebook, Producto)) # True: herencia
for p, n in ((taza, 2), (teclado, 1), (ebook, 1)):
print(p.nombre, p.costo_envio(n)) # 61.0, 67.0, 0.0
El mensaje completo de la abstracción es Can't instantiate abstract class Producto without an implementation for abstract method 'costo_envio'. Cada pilar tiene su propia sección más adelante; por ahora basta con reconocerlos.
En Python todo es un objeto
Los enteros, las cadenas, las funciones y hasta las clases son objetos. Cuatro herramientas integradas te permiten inspeccionarlos: type dice de qué clase es algo, isinstance pregunta si pertenece a una clase o a sus descendientes, id devuelve su identidad y dir lista sus atributos y métodos.
print(type(3), type("hola")) # <class 'int'> <class 'str'>
print(type(taza)) # <class '__main__.ProductoFisico'>
print(isinstance(3, object), isinstance(taza, object)) # True True
print(type(ProductoFisico), type(len)) # ABCMeta, builtin_function_or_method
otra = taza
print(id(otra) == id(taza)) # True: dos nombres, un objeto
print([m for m in dir(taza) if not m.startswith("_")])
La última línea muestra ['costo_envio', 'moneda', 'nombre', 'peso_kg', 'precio', 'reponer', 'retirar', 'sku', 'stock', 'subtotal']: atributos y métodos mezclados, porque para Python ambos son nombres que cuelgan del objeto. Que type(ProductoFisico) sea ABCMeta confirma que una clase también es un objeto, creado por otra clase.
Cuándo no hace falta una clase
La POO es una herramienta, no una obligación. Si solo necesitas transformar datos sin guardar estado, una función es más clara. Calcular el IVA de un subtotal no requiere una clase CalculadoraIVA:
def iva(monto, tasa=0.16):
return round(monto * tasa, 2)
print(iva(1496.0)) # 239.36
Una señal útil: si tu clase tiene __init__ y un único método, probablemente es una función disfrazada. Si solo agrupa datos sin comportamiento, un @dataclass o un diccionario bastan. La clase vale la pena cuando hay estado que proteger y varias operaciones que lo comparten, como el stock de un producto.
Trampas comunes
- Olvidar
selfen la definición. Un método escrito comodef reponer(cantidad)falla conTypeErroral llamarlo, porque Python sí pasa el objeto como primer argumento. - Confundir clase con objeto.
ProductoFisico.stockno es el stock de nada: devuelve el objetopropertyy sumarle 1 lanzaTypeError. El estado vive en cada instancia. - Usar
type(x) == Productopara preguntar por el tipo. DevuelveFalsepara una taza, porque su clase exacta esProductoFisico.isinstancerespeta la herencia. - Crear clases por costumbre. Una clase sin estado añade una capa de indirección que no aporta nada.
Cierre
Antes de escribir una clase, nombra su estado y su comportamiento: qué datos guarda y qué operaciones cambian esos datos. Si no puedes nombrar ambos, empieza con funciones. En las siguientes lecciones construirás Producto desde cero con class y __init__.