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.
Classes in Python — objects, encapsulation and magic methods from scratch
What is a class and why use it?
# Without classes
name = 'Sergio'
grade = 7.5
# With classes — data and behaviour together
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def passed(self):
return self.grade >= 5.0
sergio = Student('Sergio', 7.5)
print(sergio.passed()) # → True
Defining a class
class ClassName:
"""Optional docstring"""
pass
my_object = ClassName()
__init__ — the constructor
class Rectangle:
def __init__(self, base, height):
self.base = base
self.height = height
r1 = Rectangle(10, 5)
r2 = Rectangle(3, 8)
print(r1.base) # → 10
Parameter vs attribute — the most common confusion
class Rectangle:
def __init__(self, base, height):
self.base = base
self.height = height
base and height — parameters of __init__. Local variables that only exist while __init__ runs. Gone when the method ends.
self.base and self.height — attributes of the object. Belong to self and exist as long as the object exists. Accessible from any method in the class.
self.base = base does two things at once:
self.base = base # ↑ ↑ # creates initial value from # attribute the parameter
They don’t have to share the same name, though they usually do for clarity:
class Rectangle:
def __init__(self, b, h): # parameters: b and h
self.base = b # attribute: self.base
self.height = h # attribute: self.height
Attributes don’t have to come from parameters:
class Rectangle:
def __init__(self, base, height):
self.base = base # from parameter
self.height = height # from parameter
self.color = 'white' # fixed value, not a parameter
self.calculated = base * height # calculated
Self
self is the first parameter of any instance method, represents the specific object the method acts on. Python passes it automatically:
class Rectangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height
r = Rectangle(10, 5)
print(r.area()) # Python passes r as self → 50
Class vs instance attributes
class Student:
university = 'ULPGC' # class attribute — shared
count = 0
def __init__(self, name, grade):
self.name = name # instance attribute — own
self.grade = grade
Student.count += 1
Encapsulation
Attributes starting with __ are hidden:
class Natural:
def __init__(self, number):
self.__value = number # private
n = Natural(10)
print(n.__value) # AttributeError
Getters and setters
class Natural:
def __init__(self, number):
self.__value = number
def get_value(self):
return self.__value
def set_value(self, new_value):
self.__value = new_value if new_value >= 0 else 0
@property — the pythonic way
class Natural:
def __init__(self, number):
self.value = number
@property
def value(self):
return self.__value
@value.setter
def value(self, new_value):
self.__value = new_value if new_value >= 0 else 0
n = Natural(10)
print(n.value) # getter → 10
n.value = -5 # setter
print(n.value) # → 0
Class methods — @classmethod
All methods so far received self, bound to a specific object. Class methods are bound to the class itself, not any object. Declared with @classmethod, first parameter is cls:
class Student:
_count = 0
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student._count += 1
@classmethod
def total_students(cls):
return cls._count
e1 = Student('Sergio', 7.5)
e2 = Student('María', 8.0)
print(Student.total_students()) # → 2
Also used as factory methods, alternative ways to create objects:
class Angle:
def __init__(self, degrees):
self.degrees = degrees
@classmethod
def from_radians(cls, radians):
import math
return cls(radians * 180 / math.pi)
a = Angle.from_radians(3.14159 / 2)
print(a.degrees) # → 90.0
Key difference:
def method(self): # instance method — needs object
return self.attribute
@classmethod
def class_method(cls): # class method — no object needed
return cls.class_attr
Magic methods — __str__ and __repr__
class Rational:
def __init__(self, num, den):
self.num = num
self.den = den
def __str__(self):
return f'{self.num}/{self.den}' # for print() — user-facing
def __repr__(self):
return f'Rational({self.num}, {self.den})' # for repr() — programmer-facing
r = Rational(3, 4)
print(r) # → 3/4
print(repr(r)) # → Rational(3, 4)
Without either: <__main__.Rational object at 0x...>
Complete example
class Student:
university = 'ULPGC'
_count = 0
def __init__(self, name, age, grade):
self.name = name
self.age = age
self.grade = grade
Student._count += 1
@property
def grade(self):
return self.__grade
@grade.setter
def grade(self, value):
if not (0 <= value <= 10):
raise ValueError(f'Grade out of range: {value}')
self.__grade = value
@property
def passed(self):
return self.__grade >= 5.0
def classification(self):
if self.__grade >= 9: return 'Outstanding'
elif self.__grade >= 7: return 'Good'
elif self.__grade >= 5: return 'Passed'
else: return 'Failed'
@classmethod
def total_students(cls):
return cls._count
def __str__(self):
status = '✓' if self.passed else '✗'
return f'{self.name} ({self.age}) — {self.__grade:.1f} {status}'
def __repr__(self):
return f'Student("{self.name}", {self.age}, {self.__grade})'
Quick summary
# DEFINE
class MyClass:
class_attr = 0
def __init__(self, param):
self.attr = param
def method(self):
return self.attr
# INSTANTIATE
obj = MyClass(value)
# ENCAPSULATION WITH @property
@property
def value(self):
return self.__value
@value.setter
def value(self, new):
if new >= 0: self.__value = new
# MAGIC METHODS
def __str__(self): return f'...' # print() — user-facing
def __repr__(self): return f'...' # repr() — programmer-facing
# PRIVATE
self.__private # hidden — name mangling
self._convention # convention only
# CLASS METHOD
@classmethod
def class_method(cls): ...

4 comentarios