Clases en Python — objetos, encapsulamiento y métodos mágicos desde cero
Las clases en Python son el mecanismo central de la Programación Orientada a Objetos (POO), el paradigma que domina FP2. Una clase es un molde para crear objetos que agrupan datos y comportamiento en una sola unidad.
En este artículo vemos cómo definir clases, inicializarlas con __init__, proteger sus datos con encapsulamiento y dar representaciones útiles con los métodos mágicos __str__ y __repr__.
Tabla de Contenidos
¿Qué son las clases en Python y por qué usarlas?
En FP1 organizabas el código con funciones. En FP2 das un paso más, agrupas datos relacionados y las funciones que los manipulan en una clase. Un objeto es una instancia concreta de esa clase.
# Sin clases — datos sueltos
nombre = 'Sergio'
edad = 19
nota = 7.5
def es_aprobado(nota):
return nota >= 5.0
# Con clases — datos y comportamiento juntos
class Estudiante:
def __init__(self, nombre, edad, nota):
self.nombre = nombre
self.edad = edad
self.nota = nota
def es_aprobado(self):
return self.nota >= 5.0
sergio = Estudiante('Sergio', 19, 7.5)
print(sergio.es_aprobado()) # → True
La ventaja no es solo organizativa, cuando tienes 50 estudiantes, cada uno tiene sus propios datos independientes sin que interfieran entre sí.
Definir una clase
class NombreClase:
"""Docstring opcional que documenta la clase"""
pass
La palabra reservada class seguida del nombre, por convención en PascalCase (primera letra de cada palabra en mayúscula). El pass crea una clase vacía.
Para crear un objeto (instanciar la clase):
mi_objeto = NombreClase()
El método __init__ — el constructor
__init__ es el método que se ejecuta automáticamente cada vez que se crea un objeto. Se usa para inicializar los atributos del objeto:
class Rectangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
r1 = Rectangulo(10, 5)
r2 = Rectangulo(3, 8)
print(r1.base) # → 10
print(r2.altura) # → 8
__init__ no devuelve nada, su único trabajo es inicializar el objeto.
Parámetro vs atributo — la confusión más común
En __init__ hay una distinción clave que confunde mucho al principio:
class Rectangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
base y altura — son parámetros del método __init__. Son variables locales que solo existen mientras se ejecuta __init__. Desaparecen cuando el método termina.
self.base y self.altura son atributos del objeto. Pertenecen al objeto self y existen mientras el objeto exista. Se pueden usar en cualquier método de la clase.
La línea self.base = base hace dos cosas a la vez:
self.base = base # ↑ ↑ # crea el valor inicial que # atributo viene del parámetro
No tienen que llamarse igual, aunque por claridad normalmente sí:
class Rectangulo:
def __init__(self, b, h): # parámetros: b y h
self.base = b # atributo: self.base
self.altura = h # atributo: self.altura
Y puedes tener atributos que no vienen de parámetros, se calculan o se inicializan con un valor fijo:
class Rectangulo:
def __init__(self, base, altura):
self.base = base # viene del parámetro
self.altura = altura # viene del parámetro
self.color = 'blanco' # valor fijo, no es parámetro
self.area_calculada = base * altura # calculado
Self — el objeto sobre el que actúa el método
self es el primer parámetro de cualquier método de instancia, representa el objeto concreto sobre el que se ejecuta el método. Python lo pasa automáticamente, tú no lo incluyes al llamar al método:
class Rectangulo:
def __init__(self, base, altura):
self.base = base # self = el objeto recién creado
self.altura = altura
def area(self):
return self.base * self.altura # self = el objeto que llama
r1 = Rectangulo(10, 5)
print(r1.area()) # Python pasa r1 como self automáticamente → 50
El nombre self es una convención, Python acepta cualquier nombre, pero usar otro confunde a cualquiera que lea tu código.
Métodos de instancia
Los métodos son funciones definidas dentro de la clase que actúan sobre el objeto:
class Rectangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return self.base * self.altura
def perimetro(self):
return 2 * (self.base + self.altura)
def es_cuadrado(self):
return self.base == self.altura
def rotar(self):
self.base, self.altura = self.altura, self.base
r = Rectangulo(10, 5)
print(r.area()) # → 50
print(r.perimetro()) # → 30
print(r.es_cuadrado()) # → False
r.rotar()
print(r.base, r.altura) # → 5 10
Un método puede llamar a otro método del mismo objeto usando self:
def describir(self):
estado = 'cuadrado' if self.es_cuadrado() else 'rectángulo'
return f'{estado} de área {self.area()}'
Atributos de clase vs atributos de instancia
Hasta ahora todos los atributos eran de instancia, cada objeto tiene los suyos. Los atributos de clase son compartidos por todos los objetos:
class Estudiante:
universidad = 'ULPGC' # atributo de clase, compartido
contador = 0
def __init__(self, nombre, nota):
self.nombre = nombre # atributo de instancia — propio
self.nota = nota
Estudiante.contador += 1
e1 = Estudiante('Sergio', 7.5)
e2 = Estudiante('María', 8.0)
print(e1.universidad) # → ULPGC
print(e2.universidad) # → ULPGC
print(Estudiante.contador) # → 2
Encapsulamiento — proteger los datos
El encapsulamiento es el principio de ocultar los datos internos de un objeto y controlar cómo se accede y modifica. En Python los atributos que empiezan con __ (doble guion bajo) se ocultan:
class Natural:
def __init__(self, numero):
self.__valor = numero # atributo privado
n = Natural(10)
print(n.__valor) # AttributeError — no accesible desde fuera
Python implementa esto con name mangling, internamente renombra __valor a _Natural__valor. No es privacidad real, pero es la convención acordada.
Un solo guion bajo _valor significa «trata esto como privado por convención» pero no impide el acceso técnico.
Getters y setters
Cuando ocultas un atributo necesitas métodos para acceder y modificarlo de forma controlada:
class Natural:
def __init__(self, numero):
self.__valor = numero
def get_valor(self):
return self.__valor
def set_valor(self, nuevo_valor):
if nuevo_valor >= 0:
self.__valor = nuevo_valor
else:
self.__valor = 0 # no permite negativos
n = Natural(10)
print(n.get_valor()) # → 10
n.set_valor(-5)
print(n.get_valor()) # → 0
La ventaja del setter es que controla qué valores son válidos, el objeto nunca puede entrar en un estado incorrecto.
El decorador @property — la forma pythónica
Los getters y setters funcionan pero la sintaxis n.get_valor() es verbosa. El decorador @property permite usarlos como si fueran atributos normales:
class Natural:
def __init__(self, numero):
self.valor = numero # llama al setter
@property
def valor(self):
return self.__valor
@valor.setter
def valor(self, nuevo_valor):
self.__valor = nuevo_valor if nuevo_valor >= 0 else 0
n = Natural(10)
print(n.valor) # llama al getter → 10
n.valor = -5 # llama al setter
print(n.valor) # → 0
Con @property el código que usa la clase es más limpio, n.valor en vez de n.get_valor(). El control interno es el mismo.
Si defines solo @property sin el setter tienes una propiedad de solo lectura:
@property
def area(self):
return self.base * self.altura # no tiene setter — no se puede cambiar
Métodos de clase — @classmethod
Hasta ahora todos los métodos recibían self como primer parámetro, están vinculados a un objeto concreto. Los métodos de clase se vinculan a la propia clase, no a ningún objeto. Se declaran con el decorador @classmethod y su primer parámetro es cls (la clase):
class Estudiante:
_contador = 0 # atributo de clase
def __init__(self, nombre, nota):
self.nombre = nombre
self.nota = nota
Estudiante._contador += 1
@classmethod
def total_estudiantes(cls):
return cls._contador # accede al atributo de clase
e1 = Estudiante('Sergio', 7.5)
e2 = Estudiante('Sofía', 8.0)
print(Estudiante.total_estudiantes()) # → 2
print(e1.total_estudiantes()) # → 2 (también funciona desde objeto)
El uso principal de @classmethod es manejar atributos de clase, como el contador del ejemplo. También se usa para crear «métodos factoría», formas alternativas de crear objetos:
class Angulo:
def __init__(self, grados):
self.grados = grados
@classmethod
def desde_radianes(cls, radianes):
import math
return cls(radianes * 180 / math.pi) # crea un objeto Angulo
a1 = Angulo(90) # desde grados
a2 = Angulo.desde_radianes(3.14159 / 2) # desde radianes
print(a2.grados) # → 90.0
La diferencia clave con los métodos normales:
# Método de instancia: necesita un objeto
def metodo(self):
return self.atributo # accede a datos del objeto
# Método de clase: no necesita objeto
@classmethod
def metodo_clase(cls):
return cls.atributo_clase # solo accede a datos de clase
Métodos mágicos — __str__ y __repr__
Los métodos mágicos son métodos especiales que Python llama automáticamente en situaciones concretas. Sus nombres empiezan y terminan con __.
__str__ — representación informal del objeto. Se llama cuando usas print() o str():
class Racional:
def __init__(self, num, den):
self.num = num
self.den = den
def __str__(self):
return f'{self.num}/{self.den}'
r = Racional(3, 4)
print(r) # → 3/4
print(str(r)) # → 3/4
__repr__ — representación formal del objeto. Debe ser una expresión válida para recrear el objeto. Se llama en el intérprete interactivo y en la depuración:
class Racional:
def __init__(self, num, den):
self.num = num
self.den = den
def __repr__(self):
return f'Racional({self.num}, {self.den})'
def __str__(self):
return f'{self.num}/{self.den}'
r = Racional(3, 4)
print(repr(r)) # → Racional(3, 4)
print(r) # → 3/4
La diferencia: __str__ es para el usuario, __repr__ es para el programador. Si solo defines uno, define __repr__, Python lo usa como fallback para __str__ cuando este no está definido.
Sin ninguno de los dos:
r = Racional(3, 4) print(r) # → <__main__.Racional object at 0x7f1234567890>
Un ejemplo completo — clase Estudiante
Aquí tienes una clase completa que usa todo lo que hemos visto:
class Estudiante:
universidad = 'ULPGC'
_contador = 0
def __init__(self, nombre, edad, nota):
self.nombre = nombre
self.edad = edad
self.nota = nota # usa el setter
Estudiante._contador += 1
@property
def nota(self):
return self.__nota
@nota.setter
def nota(self, valor):
if not (0 <= valor <= 10):
raise ValueError(f'Nota fuera de rango: {valor}')
self.__nota = valor
@property
def aprobado(self):
return self.__nota >= 5.0
def clasificacion(self):
if self.__nota >= 9: return 'Sobresaliente'
elif self.__nota >= 7: return 'Notable'
elif self.__nota >= 5: return 'Aprobado'
else: return 'Suspenso'
@classmethod
def total_estudiantes(cls):
return cls._contador
def __str__(self):
estado = '✓' if self.aprobado else '✗'
return f'{self.nombre} ({self.edad} años) — {self.__nota:.1f} {estado}'
def __repr__(self):
return f'Estudiante("{self.nombre}", {self.edad}, {self.__nota})'
# Uso
e1 = Estudiante('Sergio', 22, 7.5)
e2 = Estudiante('María', 20, 4.8)
print(e1) # → Sergio (22 años) — 7.5 ✓
print(e2) # → María (20 años) — 4.8 ✗
print(repr(e1)) # → Estudiante("Sergio", 22, 7.5)
print(e1.clasificacion()) # → Notable
print(Estudiante.total_estudiantes()) # → 2
try:
e3 = Estudiante('Juan', 21, 11)
except ValueError as err:
print(err) # → Nota fuera de rango: 11
Visualízalo con Python Tutor
Copia este código en pythontutor.com:
class Rectangulo:
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return self.base * self.altura
r1 = Rectangulo(10, 5)
r2 = Rectangulo(3, 8)
print(r1.area())
print(r2.area())
Observa cómo cada objeto tiene sus propios atributos base y altura, independientes entre sí, y cómo self apunta al objeto correcto en cada llamada a area().
Resumen rápido
# DEFINIR UNA CLASE
class MiClase:
atributo_clase = 0 # compartido por todos los objetos
def __init__(self, param):
self.atributo = param # propio de cada objeto
def metodo(self):
return self.atributo
# INSTANCIAR
obj = MiClase(valor)
# ENCAPSULAMIENTO
class MiClase:
def __init__(self, valor):
self.valor = valor # usa el setter
@property
def valor(self):
return self.__valor
@valor.setter
def valor(self, nuevo):
if nuevo >= 0:
self.__valor = nuevo
# MÉTODOS MÁGICOS
def __str__(self): # para print() — legible por el usuario
return f'...'
def __repr__(self): # para repr() — para el programador
return f'MiClase({self.atributo})'
# ATRIBUTOS PRIVADOS
self.__privado # oculto — name mangling
self._convención # convención privado — accesible pero no recomendado
# MÉTODOS DE CLASE
@classmethod
def metodo_clase(cls):
return cls.atributo_clase
En el próximo artículo vemos herencia y polimorfismo, cómo crear clases que heredan de otras y redefinen su comportamiento.
Siguiente paso: prácticaClases en Python — 3 programas reales con encapsulamiento y @property¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

6 comentarios