Clases y POO en Python: __init__, self, métodos, herencia y dataclass
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.
Preguntas frecuentes
¿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.
Este tema se explica a fondo y con práctica en el curso «Python 3.15: от новичка до профи».
Ir al curso