> ## Documentation Index
> Fetch the complete documentation index at: https://apuntes.simulab.es/llms.txt
> Use this file to discover all available pages before exploring further.

# Programación orientada a objetos en Python: clases, herencia y métodos especiales

> Clases y objetos en Python: __init__ y self, atributos de instancia y de clase, métodos, propiedades con @property, herencia y super(), polimorfismo y duck typing, métodos especiales (__str__, __repr__, __eq__, __len__, __add__), dataclasses y métodos de clase y estáticos, con ejemplos de código.

En Python **todo es un objeto**: los números, las cadenas, las listas y hasta las funciones. Definir clases propias permite crear tipos que se comportan como los del lenguaje: que se puedan imprimir, comparar, sumar o recorrer con `for`. La sintaxis es más ligera que la de [Java](https://apuntes.simulab.es/programacion/java/poo-java.md), y la filosofía, más flexible: lo que importa es lo que un objeto **sabe hacer**, no de qué clase es.

## Lo que vas a aprender

* Cómo se define una clase con `__init__` y `self`.
* La diferencia entre atributos de instancia y de clase.
* Las propiedades con `@property`.
* La herencia y `super()`.
* El polimorfismo y el *duck typing*.
* Los métodos especiales y las `dataclass`.

## Fundamentos

### Una clase

```python theme={null}
class Cuenta:
    def __init__(self, titular, saldo=0.0):
        self.titular = titular
        self._saldo = saldo              # el _ indica "uso interno"

    def ingresar(self, cantidad):
        if cantidad <= 0:
            raise ValueError("Cantidad no válida")
        self._saldo += cantidad

    def retirar(self, cantidad):
        if cantidad > self._saldo:
            return False
        self._saldo -= cantidad
        return True

c = Cuenta("Ana", 100)
c.ingresar(50)
print(c._saldo)    # 150 (se puede, pero por convención no se debe)
```

* `__init__` es el constructor (inicializador).
* `self` es el objeto actual. Es **explícito**: aparece como primer parámetro de cada método.

### Encapsulación por convención

Python no tiene `private`. Se usa la convención de un guion bajo (`_saldo`) para "no tocar desde fuera", y dos (`__saldo`) para que el nombre se cambie internamente y sea difícil acceder por accidente.

### Propiedades

Permiten usar un método como si fuera un atributo, con control:

```python theme={null}
class Cuenta:
    def __init__(self, titular, saldo=0.0):
        self.titular = titular
        self._saldo = saldo

    @property
    def saldo(self):
        return self._saldo

    @saldo.setter
    def saldo(self, valor):
        if valor < 0:
            raise ValueError("El saldo no puede ser negativo")
        self._saldo = valor

c = Cuenta("Ana", 100)
print(c.saldo)      # 100, sin paréntesis
c.saldo = -5        # ValueError
```

### Atributos de clase

```python theme={null}
class Alumno:
    total = 0                       # compartido por todos los alumnos

    def __init__(self, nombre):
        self.nombre = nombre        # propio de cada alumno
        Alumno.total += 1
```

### Herencia

```python theme={null}
class CuentaAhorro(Cuenta):
    def __init__(self, titular, saldo=0.0, interes=0.02):
        super().__init__(titular, saldo)
        self.interes = interes

    def aplicar_interes(self):
        self.ingresar(self.saldo * self.interes)
```

Python admite **herencia múltiple** (`class C(A, B)`); el orden en que se buscan los métodos lo da `C.__mro__`.

### Polimorfismo y duck typing

"Si camina como un pato y grazna como un pato, es un pato." No hace falta que los objetos compartan una clase base: basta con que tengan el método que se usa.

```python theme={null}
class Circulo:
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2

class Cuadrado:
    def __init__(self, l): self.l = l
    def area(self): return self.l ** 2

figuras = [Circulo(1), Cuadrado(2)]
print(sum(f.area() for f in figuras))   # 7.14159
```

Si se quiere forzar una interfaz, se usa `abc.ABC` y `@abstractmethod`.

### Métodos especiales

Hacen que los objetos funcionen con la sintaxis del lenguaje:

| Método | Se usa con | Para |
| - | - | - |
| `__str__` | `print(x)`, `str(x)` | Texto legible |
| `__repr__` | La consola, depuración | Texto preciso |
| `__eq__`, `__lt__` | `==`, `<` | Comparar |
| `__len__` | `len(x)` | Longitud |
| `__add__`, `__mul__` | `+`, `*` | Operar |
| `__getitem__` | `x[i]` | Indexar |
| `__iter__` | `for e in x` | Recorrer |

```python theme={null}
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, otro):
        return Vector(self.x + otro.x, self.y + otro.y)

    def __eq__(self, otro):
        return (self.x, self.y) == (otro.x, otro.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

print(Vector(1, 2) + Vector(3, 4))   # Vector(4, 6)
```

### dataclasses

Para clases que sobre todo guardan datos, `@dataclass` genera `__init__`, `__repr__` y `__eq__` automáticamente:

```python theme={null}
from dataclasses import dataclass

@dataclass
class Punto:
    x: float
    y: float = 0.0

p = Punto(3)
print(p)                      # Punto(x=3, y=0.0)
print(p == Punto(3, 0.0))     # True
```

### Métodos de clase y estáticos

* `@classmethod` recibe la clase (`cls`) en lugar del objeto. Se usa para constructores alternativos: `Fecha.desde_texto("2026-10-12")`.
* `@staticmethod` no recibe ni objeto ni clase: es una función normal agrupada dentro de la clase.

## Ejemplos resueltos

> [accordion] Ejemplo 1 · Una pila como clase
>
> ```python theme={null}
> class Pila:
>     def __init__(self):
>         self._datos = []
>
>     def apilar(self, x):
>         self._datos.append(x)
>
>     def desapilar(self):
>         if not self._datos:
>             raise IndexError("Pila vacía")
>         return self._datos.pop()
>
>     def __len__(self):
>         return len(self._datos)
>
> p = Pila()
> p.apilar(1); p.apilar(2)
> print(p.desapilar(), len(p))   # 2 1
> ```
>
> Compárala con la animación de [estructuras de datos](https://apuntes.simulab.es/programacion/algoritmos/estructuras-datos.md).

> [accordion] Ejemplo 2 · Ordenar objetos
>
> ```python theme={null}
> from dataclasses import dataclass
>
> @dataclass(order=True)
> class Alumno:
>     nota: float
>     nombre: str
>
> clase = [Alumno(7.5, "Ana"), Alumno(9, "Luis"), Alumno(6, "Eva")]
> print(max(clase).nombre)                            # Luis
> print([a.nombre for a in sorted(clase)])            # ['Eva', 'Ana', 'Luis']
> ```
>
> Con `order=True`, la dataclass compara por los campos en orden: primero la nota.

> [accordion] Ejemplo 3 · Herencia y sobrescritura
>
> ```python theme={null}
> class Animal:
>     def hablar(self):
>         return "..."
>
> class Perro(Animal):
>     def hablar(self):
>         return "Guau"
>
> class Gato(Animal):
>     def hablar(self):
>         return "Miau"
>
> print([a.hablar() for a in (Perro(), Gato(), Animal())])   # ['Guau', 'Miau', '...']
> ```

## Errores frecuentes

* **Olvidar `self`** como primer parámetro de un método o al usar un atributo (`saldo` en lugar de `self.saldo`).
* **Usar una lista como atributo de clase** pensando que es de cada objeto: la comparten todos.
* **Olvidar llamar a `super().__init__()`** en la subclase: los atributos del padre no se crean.
* **Sobrescribir `__eq__` y no `__hash__`** si los objetos se van a usar en conjuntos o como claves de diccionario.
* **Crear getters y setters para todo**, al estilo Java. En Python se usan atributos públicos y se pasa a `@property` solo si hace falta control.

## Preguntas frecuentes

> [accordion] ¿Cuándo uso una clase y cuándo basta con funciones y diccionarios?
>
> Cuando los datos y las operaciones van siempre juntos, cuando hay varios objetos del mismo tipo con su propio estado, o cuando quieres que el tipo funcione con `+`, `for` o `print`. Para guiones cortos o transformaciones de datos, las funciones suelen bastar.

> [accordion] ¿Qué diferencia hay entre **str** y **repr**?
>
> `__str__` es para el usuario (`print`): legible. `__repr__` es para el programador (la consola, la depuración): preciso y, si se puede, que sirva para recrear el objeto. Si solo se define `__repr__`, `print` también lo usa.

## Temas relacionados

> [card] [Fundamentos de Python](https://apuntes.simulab.es/programacion/python/fundamentos-python.md)
>
> Lo básico del lenguaje.

> [card] [POO en Java](https://apuntes.simulab.es/programacion/java/poo-java.md)
>
> Clases, herencia e interfaces en Java.

> [card] [Estructuras de datos](https://apuntes.simulab.es/programacion/algoritmos/estructuras-datos.md)
>
> Pilas y colas como clases.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.