# Clases y POO en Python: __init__, self, métodos, herencia y dataclass

URL: https://skillaria.top/es/guides/python/clases-python

En Python, una clase describe los campos y los métodos de un tipo de objeto, y los objetos (instancias) se crean llamando a la clase: __init__ rellena los campos y self apunta al objeto actual. La herencia permite reutilizar el comportamiento de la clase padre y redefinir parte de sus métodos, y el decorador @dataclass genera __init__, __repr__ y __eq__ para las clases que sobre todo guardan datos.

Una clase en Python es un plano: describe qué datos guarda un objeto y qué se puede hacer con ellos. Un objeto (instancia) es un ejemplar concreto fabricado según ese plano. La programación orientada a objetos (POO) en Python se apoya en tres cosas: clases con métodos, herencia y métodos especiales como `__init__` y `__repr__`. Lo veremos paso a paso con un ejemplo que se repite a lo largo del artículo: el control de gastos.

## Para qué sirven las clases si ya existen los diccionarios

Los datos se pueden guardar en un diccionario: `{"titulo": "Café", "importe": 250}`. El problema es que el diccionario no sabe qué claves son correctas. Una errata como `gasto.get("importo", 0)` no provoca ningún error: devuelve cero, y el informe empieza a mentir sin que nadie lo note.

En un objeto de clase, los campos se declaran de antemano. Acceder a un atributo que no existe provoca de inmediato `AttributeError`, y el Python actual incluso sugiere el nombre correcto. Además, la clase reúne en un solo sitio los datos y las acciones sobre ellos: un gasto sabe calcular su importe con impuestos, y el informe calcula su total.

## Cómo crear una clase en Python

La clase se declara con la palabra `class`, y el nombre, según PEP 8, se escribe en CamelCase. Un ejemplo sencillo:

```
class Gasto:
    def __init__(self, titulo, importe):
        self.titulo = titulo
        self.importe = importe

cafe = Gasto("Café", 250)
taxi = Gasto("Taxi", 300)
print(cafe.titulo, cafe.importe)   # Café 250
```

La llamada `Gasto("Café", 250)` crea una instancia nueva de la clase. `cafe` y `taxi` son dos objetos distintos del mismo tipo, y cada uno tiene sus propios valores. Para comprobar la pertenencia se usa `isinstance(cafe, Gasto)`, que devuelve `True`.

### Constructor __init__: inicialización de la clase

`__init__` suele llamarse constructor de la clase, aunque con más precisión es un inicializador. El objeto en sí lo crea el método `__new__`; `__init__` se llama justo después y reparte los argumentos recibidos entre los campos. No debe devolver nada (en realidad, tiene que devolver `None`).

En `__init__` resulta cómodo validar los datos de entrada, para que el programa no llegue a tener objetos claramente incorrectos:

```
class Gasto:
    def __init__(self, titulo, importe):
        if importe < 0:
            raise ValueError("El importe no puede ser negativo")
        self.titulo = titulo
        self.importe = importe
```

### Qué es self en Python

`self` es la referencia al objeto con el que trabaja el método en ese momento. Cuando se escribe `cafe.con_impuesto()`, Python llama en realidad a `Gasto.con_impuesto(cafe)` y pasa el objeto como primer argumento. Por eso en la declaración del método `self` va primero, y al llamarlo no se escribe.

`self` no es una palabra reservada, sino una convención: técnicamente el parámetro podría llamarse de cualquier manera, pero nadie lo hace. Un error frecuente de los principiantes es olvidar `self` en la declaración del método. Entonces la llamada falla con `TypeError: Gasto.con_impuesto() takes 0 positional arguments but 1 was given`.

## Métodos de la clase y métodos especiales

Un método es una función declarada dentro de la clase. Ve los campos del objeto a través de `self`:

```
class Gasto:
    def __init__(self, titulo, importe):
        self.titulo = titulo
        self.importe = importe

    def con_impuesto(self, tasa=0.2):
        return self.importe * (1 + tasa)

    def __repr__(self):
        return f"Gasto({self.titulo!r}, {self.importe})"

cafe = Gasto("Café", 250)
print(cafe.con_impuesto())   # 300.0
print(cafe)                  # Gasto('Café', 250)
```

Sin `__repr__`, la salida tendría el aspecto de `<__main__.Gasto object at 0x7f...>`, algo inútil para depurar. Los métodos con dos guiones bajos al principio y al final se llaman especiales (dunder, de «double underscore»): Python los invoca por su cuenta. `__str__` define la salida «bonita» para el usuario, `__eq__` la comparación con `==` y `__len__` el resultado de `len(obj)`.

### @classmethod y @staticmethod

Un método normal trabaja con una instancia. Un método de clase, marcado con `@classmethod`, recibe como primer argumento la propia clase (`cls`), y suele usarse como constructor alternativo. Un método estático, `@staticmethod`, no recibe ni el objeto ni la clase: es una función normal que lógicamente pertenece a la clase.

```
class Gasto:
    def __init__(self, titulo, importe):
        self.titulo = titulo
        self.importe = importe

    @classmethod
    def desde_csv(cls, linea):
        titulo, importe = linea.split(";")
        return cls(titulo, float(importe))

    @staticmethod
    def importe_valido(valor):
        return valor >= 0

g = Gasto.desde_csv("Almuerzo;520")
print(g.importe)                      # 520.0
print(Gasto.importe_valido(-1))       # False
```

### Destructor de la clase: __del__

Python tiene el método `__del__`, que se llama antes de destruir el objeto el recolector de basura. No conviene confiar en él para liberar recursos, porque el momento de la llamada no está garantizado. Para archivos y conexiones use el gestor de contexto `with` o el método explícito `close()`.

## Atributos de clase y atributos de instancia

Los campos asignados con `self.nombre = ...` pertenecen a un objeto concreto. Las variables declaradas directamente en el cuerpo de la clase son atributos de clase: son comunes a todas las instancias. Así se suelen guardar las constantes de la clase.

```
class Gasto:
    MONEDA = "EUR"            # atributo de clase, constante

    def __init__(self, titulo, importe):
        self.titulo = titulo  # atributos de instancia
        self.importe = importe

a = Gasto("Café", 250)
b = Gasto("Té", 150)
print(a.MONEDA, Gasto.MONEDA)   # EUR EUR
```

Una trampa peligrosa es el atributo de clase mutable. Si en el cuerpo de la clase se declara `filas = []` y luego se añade con `self.filas.append(...)`, la lista será común para todos los objetos. Los campos mutables hay que crearlos siempre en `__init__`.

Para ver los campos de un objeto se usa `vars(obj)` o `obj.__dict__`, y para leer un atributo por su nombre desde una cadena, `getattr(obj, "importe")`.

## Propiedades de la clase: @property

Una propiedad es un método que desde fuera se lee como un campo normal, sin paréntesis. Resulta útil para valores calculados: el total de un informe no hace falta guardarlo aparte, y así nunca se olvida actualizarlo.

```
class Informe:
    def __init__(self):
        self.filas = []

    def añadir(self, gasto):
        self.filas.append(gasto)

    @property
    def total(self):
        return sum(g.importe for g in self.filas)

r = Informe()
r.añadir(Gasto("Café", 250))
r.añadir(Gasto("Taxi", 300))
print(r.total)   # 550
```

Si hace falta controlar la escritura, se añade un setter con `@total.setter`. Sin setter, la asignación `r.total = 0` provoca `AttributeError`.

Sobre la encapsulación: en Python no existen campos privados en sentido estricto. Un guion bajo (`_cache`) es un acuerdo de «no tocar desde fuera»; dos guiones bajos (`__secreto`) activan el cambio de nombre (name mangling), para que un heredero no pise el campo por accidente.

## Herencia de clases en Python

La herencia permite crear una clase hija a partir de una padre: la hija recibe todos los campos y métodos y redefine los que necesite. La clase padre se indica entre paréntesis.

```
class InformeMensual(Informe):
    def __init__(self, mes):
        super().__init__()        # llamar al __init__ del padre
        self.mes = mes

    def añadir(self, gasto):      # redefinición del método
        print(f"[{self.mes}] + {gasto}")
        super().añadir(gasto)     # conservar el comportamiento del padre

m = InformeMensual("octubre")
m.añadir(Gasto("Café", 250))   # [octubre] + Gasto('Café', 250)
print(m.total)                 # 250 — la propiedad se heredó
```

`super()` devuelve un objeto intermediario para llamar a los métodos del padre. Olvidar `super().__init__()` en el `__init__` de la hija es un error clásico: el campo `filas` no se crea, y el primer `añadir` provoca `AttributeError`.

La relación de parentesco se comprueba con `issubclass(InformeMensual, Informe)`. Todas las clases de Python 3 heredan implícitamente de `object`, de ahí vienen los métodos básicos como `__repr__` y `__eq__`. Python admite también la herencia múltiple (`class C(A, B)`); el orden de búsqueda de métodos se muestra con `C.__mro__`.

### Clase abstracta en Python

Una clase abstracta define una interfaz: qué métodos deben implementar las subclases. No se puede crear un objeto de ella. Para esto sirve el módulo `abc`:

```
from abc import ABC, abstractmethod

class Exportador(ABC):
    @abstractmethod
    def exportar(self, informe): ...

class ExportadorCsv(Exportador):
    def exportar(self, informe):
        return "\n".join(f"{g.titulo};{g.importe}" for g in informe.filas)

Exportador()   # TypeError: Can't instantiate abstract class Exportador ...
```

Una subclase que no implemente todos los métodos abstractos sigue siendo abstracta, y al crear un objeto dará el mismo error.

## Composición en lugar de jerarquías profundas

Tras conocer la herencia, da ganas de construir escaleras: gasto base, gasto en dinero, gasto en euros, y así hasta cinco niveles. En Python eso se hace poco. La herencia describe la relación «es un», mientras que la composición describe «tiene»: un informe no es una lista de gastos, sino que tiene una lista de gastos en su campo `filas`.

Regla práctica: si solo necesita usar una capacidad de otro objeto, guárdelo en un campo en lugar de heredar. Para los datos suele bastar con un dataclass, y la lógica puede quedarse en funciones normales.

## Dataclasses en Python: clase sin código repetitivo

Para las clases que sobre todo guardan datos, escribir a mano `__init__`, `__repr__` y `__eq__` resulta tedioso. El decorador `@dataclass` del módulo `dataclasses` los genera a partir de las anotaciones de los campos:

```
from dataclasses import dataclass, field

@dataclass
class Gasto:
    titulo: str
    importe: float
    etiquetas: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.importe < 0:
            raise ValueError("El importe no puede ser negativo")

g = Gasto("Café", 250)
print(g)                           # Gasto(titulo='Café', importe=250, etiquetas=[])
print(g == Gasto("Café", 250))     # True
```

La validación que antes se escribía en `__init__` se traslada a `__post_init__`, que se ejecuta justo después del inicializador generado. Los valores mutables por defecto se indican solo con `field(default_factory=list)`: escribir `etiquetas: list = []` el dataclass lo prohíbe y lanza `ValueError`.

### frozen y slots

El parámetro `frozen=True` hace el objeto inmutable, y `slots=True` (desde Python 3.10) crea `__slots__` y ahorra memoria. Intentar modificar un campo de un objeto congelado provoca `FrozenInstanceError`. Para obtener un objeto con otro valor, se crea una copia:

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Gasto:
    titulo: str
    importe: float

g = Gasto("Café", 250)
# g.importe = 300   -> dataclasses.FrozenInstanceError
g2 = replace(g, importe=300)
print(g, g2)   # Gasto(titulo='Café', importe=250) Gasto(titulo='Café', importe=300)
```

Desde Python 3.13, la función general `copy.replace()` hace lo mismo. Los objetos dataclass congelados se pueden guardar en conjuntos y usar como claves de diccionario, porque tienen `__hash__`.

## Novedades en clases: Self, @override y __slots__

Para tipar las clases hay dos herramientas útiles. `typing.Self` (Python 3.11, PEP 673) indica que se devuelve un objeto de la misma clase, y funciona también en los métodos heredados. El decorador `@override` (Python 3.12, PEP 698) marca un método que debe redefinir un método del padre: si se equivoca en el nombre, mypy o pyright avisan de que no existe ningún método base con ese nombre.

```
from typing import Self, override

class Informe:
    def __init__(self):
        self.filas = []

    def añadir(self, g) -> Self:
        self.filas.append(g)
        return self              # se puede encadenar la llamada

class InformeMensual(Informe):
    @override
    def añadir(self, g) -> Self:
        return super().añadir(g)
```

En Python 3.15 se eliminó una restricción de nicho: las subclases de `tuple` pueden tener `__slots__` no vacíos, y `__dict__` y `__weakref__` se pueden indicar en el `__slots__` de cualquier clase. En la 3.14, esa declaración en una subclase de tupla provocaba `TypeError: nonempty __slots__ not supported for subtype of 'tuple'`.

## Errores frecuentes con clases

| Error | Síntoma | Cómo corregirlo |
| --- | --- | --- |

| Falta `self` en un método | `TypeError: ... takes 0 positional arguments but 1 was given` | El primer parámetro del método es siempre `self` |

| Campo sin `self.` en `__init__` | El valor desaparece al terminar `__init__` | Escribir `self.importe = importe` |

| Lista como atributo de clase | Los datos se «filtran» entre objetos | Crear las listas en `__init__` o con `default_factory` |

| Falta `super().__init__()` | `AttributeError` en los campos del padre | Llamar al inicializador del padre |

| Llamar a una propiedad con paréntesis | `TypeError: 'int' object is not callable` | Leer la propiedad sin paréntesis: `r.total` |

## Resumen

La POO en Python empieza con algo sencillo: la clase describe campos y métodos, `__init__` rellena los campos y `self` apunta al objeto actual. La herencia y `super()` permiten ampliar el comportamiento, las clases abstractas definen interfaces y `@property` sirve para los campos calculados. Para las clases de datos use `@dataclass`, y en lugar de jerarquías profundas prefiera la composición. Las clases y los dataclasses se practican en la lección del curso de Python con el registro de gastos como ejemplo.

## FAQ

**¿En qué se diferencia una clase de un objeto en Python?**

La clase es la descripción del tipo: qué campos y métodos tiene. El objeto, o instancia, se crea llamando a la clase, por ejemplo Gasto("Café", 250), y guarda los valores concretos de sus campos.

**¿Es obligatorio escribir __init__ en una clase?**

No. Si no se declara __init__, se usa el heredado de object, que no admite argumentos. En un dataclass lo genera el decorador.

**¿En qué se diferencia __str__ de __repr__?**

__repr__ es la representación inequívoca para el desarrollador y el depurador; __str__ es la salida legible para el usuario en print y en las f-strings. Si no hay __str__, Python usa __repr__.

**¿Cuándo elegir un dataclass y cuándo una clase normal?**

El dataclass conviene cuando la clase guarda sobre todo datos y se necesitan __init__, __repr__ y comparación listos para usar. Una clase normal es mejor cuando la lógica de inicialización es compleja o el comportamiento pesa más que los datos.

**¿Hay campos privados en Python?**

No existe privacidad estricta. Un guion bajo al principio del nombre es un acuerdo de «no usar desde fuera»; dos guiones bajos activan el cambio de nombre, que protege el campo de sobrescribirse por accidente en una subclase.


---
Source: Skillaria.Top (https://skillaria.top)
