Clases y objetos

Cada tipo con el que has trabajado hasta ahora (strings, listas, diccionarios) es en realidad una clase. Cuando llamas a "hola".upper(), estás llamando a un método en un objeto string. Las clases te permiten definir tus propios tipos con sus propios datos y comportamiento. Una clase Jugador puede contener un nombre, una puntuación y un nivel, y saber cómo mostrarse a sí misma.
Plantilla e instancias
Una clase es una plantilla. Una instancia es algo específico hecho a partir de esa plantilla. Puedes hacer tantas instancias como necesites, cada una con sus propios datos pero compartiendo los mismos métodos definidos en la clase.
class Perro:
def ladrar(self):
print("¡Guau!")
rex = Perro()
luna = Perro()
rex.ladrar() # "¡Guau!"
luna.ladrar() # "¡Guau!"Perro es la clase. rex y luna son instancias: dos perros diferentes, cada uno compartiendo el mismo comportamiento definido en la clase.
Perro(), y obtienes una instancia nueva. Cada instancia comparte los métodos de la clase pero mantiene sus propios datos, así que rex y luna pueden comportarse igual siendo perros separados. __init__ y self
__init__ es el método que Python llama automáticamente cuando creas una nueva instancia. Es donde estableces los datos iniciales para el objeto. self es cómo un método se refiere a la instancia específica en la que está operando, y siempre es el primer parámetro.
class Jugador:
def __init__(self, nombre, puntaje=0):
self.nombre = nombre
self.puntaje = puntaje
def agregar_puntos(self, puntos):
self.puntaje += puntos
def mostrar(self):
print(f"{self.nombre}: {self.puntaje} puntos")
maría = Jugador("María")
carlos = Jugador("Carlos", puntaje=50)
maría.agregar_puntos(30)
maría.mostrar() # "María: 30 puntos"
carlos.mostrar() # "Carlos: 50 puntos"self.nombre y self.puntaje son atributos de instancia: pertenecen al objeto específico, no a la clase misma. Cada instancia de Jugador tiene su propio nombre y puntaje.
__init__ se ejecuta en el momento en que creas una instancia, así que es donde estableces los datos iniciales con self.nombre = valor. self es la instancia con la que Python está trabajando, y siempre es el primer parámetro de un método, entregado a ti automáticamente. Nunca pasas self tú mismo cuando llamas a alice.mostrar(). Métodos
Cualquier función definida dentro de una clase es un método. Los métodos de instancia siempre tienen self como el primer parámetro; Python lo pasa automáticamente. Los métodos pueden leer y cambiar los datos de la instancia a través de self.
class Círculo:
def __init__(self, radio):
self.radio = radio
def área(self):
return 3.14159 * self.radio ** 2
def escalar(self, factor):
self.radio *= factor
return self # devolver self permite encadenamiento: c.escalar(2).escalar(0.5)
c = Círculo(5)
print(c.área()) # 78.53975
c.escalar(2)
print(c.área()) # 314.159self, la instancia con la que está trabajando. Python pasa self para ti, así que llamas a c.área() sin nada extra. A través de self un método lee y cambia los datos propios de ese objeto. Variables de clase vs variables de instancia
Las variables definidas directamente en la clase (no dentro de __init__) son variables de clase. Todas las instancias comparten la misma variable de clase. Las variables establecidas en self dentro de __init__ son variables de instancia, únicas para cada objeto.
class Jugador:
max_vidas = 3 # variable de clase, igual para cada Jugador
def __init__(self, nombre):
self.nombre = nombre # variable de instancia, única para cada Jugador
self.vidas = Jugador.max_vidas
def morir(self):
self.vidas -= 1
maría = Jugador("María")
carlos = Jugador("Carlos")
Jugador.max_vidas = 5 # cambia para todas las instancias presentes y futurasUsa variables de clase para valores compartidos entre todas las instancias: constantes, contadores, predeterminados. Usa variables de instancia para datos que difieren por objeto.
self en __init__ pertenece a ese objeto específico. Escribir self.attr = valor siempre crea o actualiza la copia propia de la instancia. Así que recurre a variables de clase cuando un valor es igual para todos, variables de instancia cuando difiere por objeto. __str__ y __repr__
__str__ controla lo que print() y f-strings muestran para tu objeto. __repr__ controla la vista de desarrollador mostrada en la consola y para depuración. Siempre define __repr__. Define __str__ cuando quieres una visualización limpia orientada al usuario separada de la vista de depuración.
class Jugador:
def __init__(self, nombre, puntaje):
self.nombre = nombre
self.puntaje = puntaje
def __str__(self):
return f"{self.nombre} ({self.puntaje} pts)"
def __repr__(self):
return f"Jugador(nombre={self.nombre!r}, puntaje={self.puntaje})"
maría = Jugador("María", 87)
print(maría) # "María (87 pts)" (usa __str__)
repr(maría) # "Jugador(nombre='María', puntaje=87)" (usa __repr__)Siempre define __repr__. Define __str__ cuando quieres una representación limpia orientada al usuario separada de la vista de depuración. Si solo se define __repr__, Python lo usa para ambos.
__str__ es lo que print() y f-strings muestran, la versión amable. __repr__ es la vista de desarrollador que ves en la consola. Siempre escribe __repr__; es el que tiene un trabajo incluso cuando olvidas __str__. Agrega __str__ solo cuando el texto orientado al usuario debe leer diferente. Convención privada
Python no tiene variables privadas reales, pero un solo guion bajo al inicio de un nombre (_saldo) es una convención que señala "esto es interno, no lo uses directamente desde fuera de la clase". No se aplica por el lenguaje; es una comunicación a otros desarrolladores.
class CuentaBancaria:
def __init__(self, saldo):
self._saldo = saldo # _ significa "no toques"
def depositar(self, cantidad):
if cantidad > 0:
self._saldo += cantidad
def saldo(self):
return self._saldoUn doble guion bajo (__nombre) desencadena ofuscación de nombres; Python renombra el atributo a _NombreClase__nombre para evitar conflictos en subclases. Es raramente necesario. Un solo guion bajo es la convención en la mayoría del código.
_saldo) es la señal acordada para "interno, déjalo en paz desde fuera". Nada te detiene alcanzando adentro, es un mensaje a otros desarrolladores, incluyendo tú futuro. Un doble guion bajo es una herramienta más rara para evitar choques de nombres en subclases; el único guion bajo es lo que usarás día a día. Herencia
Una clase puede heredar de otra clase, obteniendo automáticamente todos sus atributos y métodos. Luego puedes anular métodos específicos en la subclase para cambiar su comportamiento. Esto te permite reutilizar una base común y especializar donde sea necesario.
class Animal:
def __init__(self, nombre):
self.nombre = nombre
def hablar(self):
return "..."
class Perro(Animal):
def hablar(self):
return f"¡{self.nombre} ladra Guau!"
class Gato(Animal):
def hablar(self):
return f"¡{self.nombre} maúlla Miau!"
mascotas = [Perro("Rex"), Gato("Luna"), Perro("Max")]
for mascota in mascotas:
print(mascota.hablar())Perro y Gato heredan __init__ de Animal, así que no necesitan el propio. Anulan hablar() con su comportamiento específico.
Perro y Gato reutilizan `__init__` de `Animal` y solo redefinen `hablar()`. super()
super() llama a un método de la clase padre. Úsalo cuando quieras extender el comportamiento del padre en lugar de reemplazarlo completamente: llama a __init__ del padre para ejecutar su configuración, luego agrega cualquier cosa que tu subclase necesite encima.
class Animal:
def __init__(self, nombre, sonido):
self.nombre = nombre
self.sonido = sonido
class Perro(Animal):
def __init__(self, nombre):
super().__init__(nombre, "Guau") # llama a Animal.__init__
self.trucos = [] # agrega algo extra
def aprender(self, truco):
self.trucos.append(truco)
rex = Perro("Rex")
rex.aprender("sentarse")
print(rex.trucos) # ["sentarse"]Siempre llama a super().__init__() cuando tu subclase tiene su propio __init__ y el padre también.
super() alcanza la clase padre, así que super().__init__() ejecuta la configuración del padre antes de agregar la tuya. Úsalo cuando tu subclase escribe su propio `__init__` y el padre tiene uno también. Sáltalo y la configuración del padre nunca se ejecuta, dejando el objeto medio construido. Métodos de clase y métodos estáticos
@classmethod crea un método que recibe la clase misma en lugar de una instancia. Es útil para constructores alternativos: crear una instancia desde una cadena, un archivo u otro formato. @staticmethod es una función simple que vive dentro de la clase por razones organizacionales; no recibe ni la instancia ni la clase.
class Jugador:
def __init__(self, nombre, puntaje):
self.nombre = nombre
self.puntaje = puntaje
@classmethod
def desde_cadena(cls, datos):
nombre, puntaje = datos.split(",")
return cls(nombre, int(puntaje))
maría = Jugador.desde_cadena("María,87")class Jugador:
@staticmethod
def es_nombre_válido(nombre):
return nombre.isalpha() and len(nombre) >= 2
Jugador.es_nombre_válido("María") # True
Jugador.es_nombre_válido("M1") # FalseUsa @classmethod para constructores alternativos. Usa @staticmethod para funciones de utilidad que lógicamente pertenecen a la clase pero no necesitan datos de instancia o clase.
@classmethod te entrega la clase en lugar de una instancia, que la hace la opción de ir para constructores alternativos: construye un `Jugador` desde una cadena, un archivo, cualquier formato que tengas. @staticmethod es una función ordinaria metida dentro de la clase para limpieza; no obtiene ni la clase ni la instancia. Los métodos simples tocan datos de un objeto, estos dos no. @property
@property te permite acceder a un método como un atributo, sin paréntesis necesarios. Úsalo para valores que se calculan a partir de otros atributos y se sienten naturales de leer como acceso de atributo simple.
class Círculo:
def __init__(self, radio):
self.radio = radio
@property
def área(self):
return 3.14159 * self.radio ** 2
@property
def diámetro(self):
return self.radio * 2
c = Círculo(5)
print(c.área) # 78.53975 (se parece a un atributo, corre como un método)
print(c.diámetro) # 10Las propiedades son útiles para valores calculados: cosas derivadas de otros atributos que se sienten naturales de acceder sin ().
@property te permite leer un método como un atributo, sin paréntesis: c.área en lugar de c.área(). Se adapta a valores que se elaboran a partir de otros atributos y se sienten naturales de leer como datos simples. Detrás de escenas aún ejecuta tu método cada vez que lo accedes. En práctica
Una clase Jugador con atributos de instancia, métodos, una @property, y __str__:
class Jugador:
max_vidas = 3
def __init__(self, nombre: str):
self.nombre = nombre
self.puntaje = 0
self.vidas = Jugador.max_vidas
def ganar_puntos(self, cantidad: int) -> None:
self.puntaje += cantidad
def recibir_golpe(self) -> bool:
self.vidas -= 1
return self.vidas > 0
@property
def está_vivo(self) -> bool:
return self.vidas > 0
def __str__(self) -> str:
return f"{self.nombre} | Puntaje: {self.puntaje} | Vidas: {self.vidas}"
maría = Jugador("María")
maría.ganar_puntos(50)
maría.recibir_golpe()
print(maría) # "María | Puntaje: 50 | Vidas: 2"
print(maría.está_vivo) # True
