</>waridocu

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.

pythonclasesobjetosselfinitpoo

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:

Ejemplo: los datos de dos personas, sueltospython
nombre_1 = "Ana"
edad_1 = 30
nombre_2 = "Luis"
edad_2 = 42

Nada 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:

Ejemplo: los mismos datos, agrupados en diccionariospython
ana = {"nombre": "Ana", "edad": 30}
luis = {"nombre": "Luis", "edad": 42}

print(ana["nombre"], ana["edad"])
texttext
Ana 30

Esto 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.

Ejemplo: definir una clase y crear dos instanciaspython
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:

bashbash
python persona.py
texttext
Ana 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 leer CuentaBancaria(...) 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 parte Persona: acabas de crear un tipo nuevo, al mismo nivel que int o str.

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.

Ejemplo: ver cuándo se ejecuta __init__python
class Persona:
    def __init__(self, nombre):
        print("Creando una persona llamada", nombre)
        self.nombre = nombre


print("antes")
ana = Persona("Ana")
print("después")
texttext
antes
Creando una persona llamada Ana
después

El 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.

Nota

__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:

Ejemplo: qué hace Python cuando escribes ana.saludar()python
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 mano

Guárdalo como self.py y ejecútalo:

bashbash
python self.py
texttext
Hola, me llamo Ana
Hola, me llamo Ana

Ahí 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.

Importante

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.

Advertencia

Los dos errores clásicos con self:

  • Olvidarlo en la definición. Si escribes def saludar(): y luego llamas ana.saludar(), el error es TypeError: ... takes 0 positional arguments but 1 was given. Ese “1 argumento” que dices no haber pasado es el propio ana, que Python pasó por ti.
  • Olvidar el self. dentro del cuerpo. Escribir print("Hola,", nombre) en vez de self.nombre provoca NameError: name 'nombre' is not defined, porque dentro del método nombre sería una variable local que no existe. Los datos del objeto siempre se leen y se escriben a través de self..

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:

Ejemplo: métodos que leen y modifican los datos del propio objetopython
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:

bashbash
python metodos_clase.py
texttext
Hola, me llamo Ana
True
False
16
16

Tres 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 += 1 cambia 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 True para ana y False para nico. Un solo código, dos resultados, porque self apunta 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.

Ejemplo: atributo de clase (compartido) vs. atributo de instancia (propio)python
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:

bashbash
python atributos.py
texttext
Ana / Luis
humano / humano
Personas creadas: 2
homo sapiens / homo sapiens

nombre 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.

Advertencia

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.

Ejemplo: asignar sobre la instancia no toca el atributo de clasepython
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)
texttext
marciano
humano
humano

Por 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.

Advertencia

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.

Ejemplo: una cuenta bancaria que valida sus propias operacionespython
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:

bashbash
python cuenta.py
texttext
Ana 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: 50

Por 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.
  • return a secas para salir temprano. En depositar(), el return sin valor corta el método en el acto cuando el monto es inválido, evitando un else que indentaría el resto del cuerpo. Es el mismo uso de return ya visto en funciones.
  • retirar() devuelve True o False. 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. cuenta y otra tienen su propio saldo; depositar en uno no afecta al otro, porque self.saldo apunta al atributo de esa instancia.
  • La llamada print(..., cuenta.titular + ":", ...) pega los dos puntos al nombre con el operador + de concatenación, porque print() 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.

Ejemplo: decidir cómo se imprime un objeto con __str__python
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:

bashbash
python str_persona.py
texttext
Persona(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.

Nota

__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”.

Ejemplo: el guion bajo es una convención, no una barrerapython
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:

bashbash
python guion_bajo.py
texttext
1500
999999

La ú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.

Nota

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 :.