Python
Clases: programación orientada a objetos en Python
Cómo definir tus propios tipos con class, qué hace __init__, por qué hay que escribir self a mano y en qué se diferencian los atributos de clase de los de instancia.
De dónde venimos
Hasta ahora, cuando un programa necesitaba representar “algo” con varios datos —una persona con nombre y edad, una cuenta con titular y saldo— tenías dos opciones, y ninguna era del todo buena.
La primera era guardarlo en variables sueltas:
nombre_1 = "Ana"
edad_1 = 30
nombre_2 = "Luis"
edad_2 = 42Nada dice que nombre_1 y edad_1 pertenecen a la misma persona: eso vive solo en tu cabeza y en el sufijo _1. Con tres personas ya es inmanejable.
La segunda, mucho mejor, era un diccionario:
ana = {"nombre": "Ana", "edad": 30}
luis = {"nombre": "Luis", "edad": 42}
print(ana["nombre"], ana["edad"])Ana 30Esto ya agrupa los datos de cada persona en un solo valor. Pero le siguen faltando dos cosas:
- Nada garantiza la forma. Si en algún punto del programa escribes
{"nombre": "Carla"}sin la edad, o{"nombe": "Marta"}con la clave mal escrita, el diccionario se crea igual y el error aparece mucho después. - Los datos y su comportamiento viven separados. Una función
cumplir_anios(persona)funciona, pero está suelta en el archivo, sin ninguna relación visible con los diccionarios sobre los que opera. Nada impide llamarla con un diccionario que represente otra cosa.
Una clase resuelve las dos: define un tipo propio con una forma fija y con las operaciones que le pertenecen, todo en un mismo sitio.
Tu primera clase
Una clase es un molde. Describe qué datos tiene algo y qué sabe hacer. Cada valor concreto creado a partir de ese molde se llama instancia u objeto.
La analogía habitual: la clase es el plano de una casa, y cada instancia es una casa construida con ese plano. El plano no se puede habitar, y dos casas del mismo plano tienen distintos dueños y distinto color de puerta.
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
ana = Persona("Ana", 30)
luis = Persona("Luis", 42)
print(ana.nombre, ana.edad)
print(luis.nombre, luis.edad)
print(type(ana))Guárdalo como persona.py y ejecútalo:
python persona.pyAna 30
Luis 42
<class '__main__.Persona'>Lo que hay que mirar de ese código:
class Persona:abre la definición. Termina en dos puntos y su cuerpo va indentado, exactamente como cualquier otro bloque: aquí no hay ninguna regla nueva.- Por convención, los nombres de clase se escriben en PascalCase (
Persona,CuentaBancaria): mayúscula inicial y sin guiones bajos. Es distinto del snake_case de variables y funciones, y esa diferencia visual es deliberada: al leerCuentaBancaria(...)sabes de inmediato que estás creando un objeto, no llamando a una función cualquiera. Persona("Ana", 30)crea una instancia. Se escribe igual que una llamada a función, con los paréntesis y los argumentos.- A los datos guardados dentro del objeto se accede con un punto:
ana.nombre. Sin comillas y sin corchetes, a diferencia del diccionario. type(ana)muestra<class '__main__.Persona'>.__main__es simplemente el nombre que Python le da al archivo que estás ejecutando; lo importante es la partePersona: acabas de crear un tipo nuevo, al mismo nivel queintostr.
El constructor: el método que se ejecuta al crear el objeto
__init__ es un método especial que Python llama automáticamente cada vez que creas una instancia. Su trabajo es preparar el objeto recién nacido: recibir los datos iniciales y guardarlos dentro de él. A ese rol se le llama constructor.
El nombre no es arbitrario ni se puede cambiar: viene de initialize (inicializar), y los dos guiones bajos de cada lado son la marca que Python usa para sus nombres especiales, esos que el propio lenguaje llama por ti en momentos determinados. Tú nunca escribes ana.__init__(...) a mano; escribes Persona("Ana", 30) y Python se encarga.
class Persona:
def __init__(self, nombre):
print("Creando una persona llamada", nombre)
self.nombre = nombre
print("antes")
ana = Persona("Ana")
print("después")antes
Creando una persona llamada Ana
despuésEl mensaje aparece justo en la línea donde se crea el objeto, ni antes ni después. Definir la clase no ejecutó nada, igual que definir una función no la ejecuta.
Y como __init__ es un método normal en todo lo demás, admite parámetros con valor por defecto: eso es lo que permite escribir CuentaBancaria("Ana") sin indicar saldo, como verás más abajo.
__init__ no devuelve nada; su único trabajo es rellenar el objeto. Escribir un return con un valor dentro de __init__ produce un TypeError. Quien devuelve la instancia es Python, no tú.
self: la instancia sobre la que se llama el método
Este es el punto que más confunde a quien llega desde otros lenguajes, así que vale la pena desarmarlo con calma.
self es una referencia al objeto concreto sobre el que se está llamando el método. Si escribes ana.saludar(), dentro de saludar la variable self es ana. Si escribes luis.saludar(), en esa llamada self es luis. Es lo que permite que un mismo método escrito una sola vez funcione con los datos de cada objeto por separado.
Lo que sorprende no es que exista, sino que haya que escribirlo a mano como primer parámetro de cada método. En Java o C++ el equivalente (this) está disponible sin declararlo. En Python hay que ponerlo, y la razón es que Python prefiere que las cosas sean explícitas antes que mágicas.
La forma más rápida de entenderlo es ver que las dos líneas siguientes hacen literalmente lo mismo:
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def saludar(self):
print("Hola, me llamo", self.nombre)
ana = Persona("Ana")
ana.saludar() # la forma normal
Persona.saludar(ana) # exactamente lo mismo, escrito a manoGuárdalo como self.py y ejecútalo:
python self.pyHola, me llamo Ana
Hola, me llamo AnaAhí está todo el misterio: ana.saludar() es una abreviatura de Persona.saludar(ana). El objeto de la izquierda del punto se pasa como primer argumento. Por eso el método declara un parámetro más de los que parece recibir en la llamada, y por eso ese parámetro va siempre primero.
self no es una palabra clave del lenguaje: es solo un nombre de parámetro, y podrías llamarlo yo o pepe y funcionaría igual. Pero no lo hagas. self es una convención universal en Python y absolutamente todo el mundo la respeta; cambiarla solo consigue que tu código sea ilegible para cualquier otra persona.
Los dos errores clásicos con self:
- Olvidarlo en la definición. Si escribes
def saludar():y luego llamasana.saludar(), el error esTypeError: ... takes 0 positional arguments but 1 was given. Ese “1 argumento” que dices no haber pasado es el propioana, que Python pasó por ti. - Olvidar el
self.dentro del cuerpo. Escribirprint("Hola,", nombre)en vez deself.nombreprovocaNameError: name 'nombre' is not defined, porque dentro del métodonombresería una variable local que no existe. Los datos del objeto siempre se leen y se escriben a través deself..
Métodos: el comportamiento del objeto
Un método es una función definida dentro de una clase. Ya usabas métodos desde las listas (.append(), .sort()) y los diccionarios (.get(), .items()); la diferencia es que ahora escribes los tuyos.
Se definen con def, como cualquier función, y se llaman con un punto sobre el objeto:
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def saludar(self):
print("Hola, me llamo", self.nombre)
def es_mayor_de_edad(self):
return self.edad >= 18
def cumplir_anios(self):
self.edad += 1
return self.edad
ana = Persona("Ana", 30)
nico = Persona("Nico", 15)
ana.saludar()
print(ana.es_mayor_de_edad())
print(nico.es_mayor_de_edad())
print(nico.cumplir_anios())
print(nico.edad)Guárdalo como metodos_clase.py y ejecútalo:
python metodos_clase.pyHola, me llamo Ana
True
False
16
16Tres detalles que resumen todo lo anterior:
es_mayor_de_edad()devuelve un valor en vez de imprimirlo, siguiendo la misma regla que ya viste con las funciones: quien llama decide qué hacer con el resultado.cumplir_anios()modifica el objeto:self.edad += 1cambia el dato guardado dentro de esa instancia, y el cambio persiste después de que el método termina. Eso es lo que distingue un objeto de una simple caja de datos de solo lectura.- El mismo método da
TrueparaanayFalseparanico. Un solo código, dos resultados, porqueselfapunta a un objeto distinto en cada llamada.
Atributos de instancia y atributos de clase
Los datos guardados dentro de un objeto se llaman atributos, y hay dos tipos que es fácil confundir:
- Atributo de instancia: se crea con
self.algo = ...dentro de__init__(o de cualquier método). Cada objeto tiene el suyo, con su propio valor. - Atributo de clase: se escribe directamente en el cuerpo de la clase, fuera de todo método. Existe uno solo, compartido por todas las instancias.
El caso de uso típico de un atributo de clase es algo que pertenece al concepto entero y no a un ejemplar concreto: una constante, una configuración común, o un contador de cuántos objetos se han creado.
class Persona:
especie = "humano" # atributo de clase: uno solo para todas
creadas = 0 # contador compartido
def __init__(self, nombre):
self.nombre = nombre # atributo de instancia: uno por objeto
Persona.creadas += 1
ana = Persona("Ana")
luis = Persona("Luis")
print(ana.nombre, "/", luis.nombre)
print(ana.especie, "/", luis.especie)
print("Personas creadas:", Persona.creadas)
Persona.especie = "homo sapiens" # cambiarlo en la clase afecta a todas
print(ana.especie, "/", luis.especie)Guárdalo como atributos.py y ejecútalo:
python atributos.pyAna / Luis
humano / humano
Personas creadas: 2
homo sapiens / homo sapiensnombre es distinto en cada objeto; especie es el mismo para los dos, y al cambiarlo en la clase cambia para todos a la vez. El contador creadas funciona porque __init__ se ejecuta una vez por instancia y suma sobre Persona.creadas, no sobre self.creadas.
Un atributo de clase se puede leer desde la instancia (ana.especie funciona), pero asignarle algo desde la instancia no cambia el atributo compartido: crea un atributo de instancia nuevo que solo existe para ese objeto y que tapa al de la clase.
class Persona:
especie = "humano"
def __init__(self, nombre):
self.nombre = nombre
ana = Persona("Ana")
luis = Persona("Luis")
ana.especie = "marciano" # crea un atributo propio de ana
print(ana.especie)
print(luis.especie)
print(Persona.especie)marciano
humano
humanoPor eso el contador del ejemplo anterior se escribe Persona.creadas += 1 y no self.creadas += 1: lo segundo leería el valor compartido, le sumaría uno y guardaría el resultado como atributo propio de cada objeto, dejando el contador de la clase en 0 para siempre. Es un bug sutil y muy común.
Nunca uses una lista (ni ningún otro valor mutable) como atributo de clase si esperas que cada objeto tenga la suya: al ser compartida, lo que agregue un objeto lo verán todos los demás. Es exactamente la misma trampa que la lista como valor por defecto de un parámetro. La solución es la misma: créala dentro de __init__ con self.items = [], para que nazca una por instancia.
Ejemplo completo: una clase CuentaBancaria
Este ejemplo junta todo: constructor con valor por defecto, atributos de instancia, métodos que modifican el objeto y validación con if.
class CuentaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self.saldo = saldo
def depositar(self, monto):
if monto <= 0:
print("El monto a depositar debe ser positivo.")
return
self.saldo += monto
print(self.titular, "deposita", monto, "-> saldo:", self.saldo)
def retirar(self, monto):
if monto > self.saldo:
print("Fondos insuficientes:", self.titular, "tiene", self.saldo,
"y pidió", monto)
return False
self.saldo -= monto
print(self.titular, "retira", monto, "-> saldo:", self.saldo)
return True
cuenta = CuentaBancaria("Ana", 1000)
otra = CuentaBancaria("Luis") # sin saldo: usa el valor por defecto, 0
cuenta.depositar(500)
cuenta.retirar(200)
cuenta.retirar(5000)
otra.depositar(50)
print("Saldo final de", cuenta.titular + ":", cuenta.saldo)
print("Saldo final de", otra.titular + ":", otra.saldo)Guárdalo como cuenta.py y ejecútalo:
python cuenta.pyAna deposita 500 -> saldo: 1500
Ana retira 200 -> saldo: 1300
Fondos insuficientes: Ana tiene 1300 y pidió 5000
Luis deposita 50 -> saldo: 50
Saldo final de Ana: 1300
Saldo final de Luis: 50Por qué está escrito así:
- La validación vive dentro de la clase.
retirar()comprueba los fondos por su cuenta; quien usa la cuenta no puede olvidarse de hacerlo. Con un diccionario suelto, esa comprobación estaría repartida por todo el programa y bastaría un sitio donde falte para dejar el saldo en negativo. returna secas para salir temprano. Endepositar(), elreturnsin valor corta el método en el acto cuando el monto es inválido, evitando unelseque indentaría el resto del cuerpo. Es el mismo uso dereturnya visto en funciones.retirar()devuelveTrueoFalse. Así quien la llama puede reaccionar (if cuenta.retirar(200):) en vez de adivinar si salió bien.depositar()no devuelve nada porque no puede “fallar a medias”.- Los dos objetos son independientes.
cuentayotratienen su propiosaldo; depositar en uno no afecta al otro, porqueself.saldoapunta al atributo de esa instancia. - La llamada
print(..., cuenta.titular + ":", ...)pega los dos puntos al nombre con el operador+de concatenación, porqueprint()con comas insertaría un espacio antes del:.
Controlar cómo se imprime un objeto
Si imprimes un objeto directamente con print(), Python no sabe cómo mostrarlo y recurre a lo único que tiene: el nombre de la clase y su dirección en memoria, algo como <__main__.Persona object at 0x000001C4A2B3F7D0> (el número cambia en cada ejecución). Es correcto, pero inútil para leer.
El método especial __str__ permite decidir ese texto. Igual que __init__, no lo llamas tú: Python lo usa automáticamente cuando el objeto necesita convertirse en cadena.
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def __str__(self):
return "Persona(" + self.nombre + ", " + str(self.edad) + " años)"
ana = Persona("Ana", 30)
print(ana)
print(str(ana))Guárdalo como str_persona.py y ejecútalo:
python str_persona.pyPersona(Ana, 30 años)
Persona(Ana, 30 años)Fíjate en que __str__ devuelve una cadena, no la imprime: quien imprime es print(). Y que la edad se pasa por str() para poder concatenarla, porque + entre un número y un texto falla.
__init__ y __str__ pertenecen a una familia de métodos especiales (los que van rodeados de dos guiones bajos) que Python llama en momentos concretos: hay otros para comparar objetos con ==, para sumarlos con + o para medirlos con len(). No hace falta conocerlos todos; con saber que existen y que se reconocen por los guiones bajos es suficiente por ahora. Verás también __repr__, muy parecido a __str__ pero pensado para la representación técnica que se ve al depurar.
En Python no existe private
En Java o C++ puedes marcar un atributo como private y el compilador impide tocarlo desde fuera de la clase. En Python no hay nada equivalente: todos los atributos de un objeto son accesibles y modificables desde cualquier parte del programa.
Lo que existe es una convención: anteponer un guion bajo al nombre (_saldo) significa “esto es interno, no lo toques desde afuera”. El lenguaje no lo impide, no avisa y no falla; simplemente todo el mundo entiende ese guion bajo como un cartel de “no entrar”.
class CuentaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self._saldo = saldo # "_" = uso interno, no tocar desde fuera
def depositar(self, monto):
self._saldo += monto
def ver_saldo(self):
return self._saldo
cuenta = CuentaBancaria("Ana", 1000)
cuenta.depositar(500)
print(cuenta.ver_saldo())
cuenta._saldo = 999999 # Python lo permite sin quejarse
print(cuenta.ver_saldo())Guárdalo como guion_bajo.py y ejecútalo:
python guion_bajo.py1500
999999La última asignación saltó por encima de toda la lógica de la clase y dejó el saldo en un valor imposible, sin un solo mensaje de error.
Esta filosofía se resume en una frase muy citada de la comunidad Python: aquí somos todos adultos responsables. En vez de que el lenguaje prohíba, se confía en que quien lee el código respete la señal. Si vienes de un lenguaje con private, la adaptación mental es esta: el guion bajo no protege el atributo, documenta que no forma parte de lo que la clase promete mantener estable.
Siguiente paso
Con las clases se cierra la base del lenguaje: ya sabes guardar datos, decidir, repetir, encapsular en funciones, agrupar en colecciones y definir tus propios tipos.
En el camino fueron apareciendo los mismos cuatro símbolos una y otra vez —(), {}, [] y :— con significados distintos según dónde estaban, y cada vez se explicaron por separado. Toca juntarlos y verlos como un sistema: continúa con Repaso de sintaxis: (), {}, [] y :.