Clases y objetos
Todos los tipos con los que has trabajado hasta ahora (strings, listas, diccionarios) son en realidad clases. Cuando llamas a "hello".upper(), estás llamando a un método de un objeto string. Las clases te permiten definir tus propios tipos con su propia información y comportamiento. Una clase Player puede guardar un nombre, un puntaje y un nivel, y saber cómo mostrarse a sí misma.
Plano y instancias
Una clase es un plano de construcción. Una instancia es una cosa específica creada a partir de ese plano. Puedes crear tantas instancias como necesites, cada una con su propia información pero compartiendo los mismos métodos definidos en la clase.
class Dog:
def bark(self):
print("Woof!")
rex = Dog()
luna = Dog()
rex.bark() # "Woof!"
luna.bark() # "Woof!"Dog es la clase. rex y luna son instancias: dos perros distintos, cada uno compartiendo el mismo comportamiento definido en la clase.
Dog(), y obtienes una instancia nueva. Cada instancia comparte los métodos de la clase pero mantiene su propia información, así que rex y luna pueden comportarse igual siendo perros distintos. __init__ y self
__init__ es el método que Python llama automáticamente cuando creas una nueva instancia. Es donde configuras los datos iniciales del objeto. self es la forma en que un método se refiere a la instancia específica sobre la que está operando, y siempre es el primer parámetro.
class Player:
def __init__(self, name, score=0):
self.name = name
self.score = score
def add_points(self, points):
self.score += points
def display(self):
print(f"{self.name}: {self.score} points")
alice = Player("Alice")
bob = Player("Bob", score=50)
alice.add_points(30)
alice.display() # "Alice: 30 points"
bob.display() # "Bob: 50 points"self.name y self.score son atributos de instancia: pertenecen al objeto específico, no a la clase en sí. Cada instancia de Player tiene su propio name y score.
__init__ se ejecuta en el momento en que creas una instancia, así que ahí es donde configuras los datos iniciales con self.name = value. self es la instancia con la que Python está trabajando, y siempre es el primer parámetro de un método, entregado automáticamente. Nunca pasas self tú mismo cuando llamas a alice.display(). Métodos
Cualquier función definida dentro de una clase es un método. Los métodos de instancia siempre tienen self como primer parámetro; Python lo pasa automáticamente. Los métodos pueden leer y modificar la información de la instancia a través de self.
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def scale(self, factor):
self.radius *= factor
return self # devolver self permite encadenar: c.scale(2).scale(0.5)
c = Circle(5)
print(c.area()) # 78.53975
c.scale(2)
print(c.area()) # 314.159self, la instancia con la que está trabajando. Python pasa self por ti, así que llamas a c.area() sin nada extra. A través de self, un método lee y modifica la información propia 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 que se configuran en self dentro de __init__ son variables de instancia, únicas para cada objeto.
class Player:
max_lives = 3 # variable de clase, igual para todo Player
def __init__(self, name):
self.name = name # variable de instancia, única para cada Player
self.lives = Player.max_lives
def die(self):
self.lives -= 1
alice = Player("Alice")
bob = Player("Bob")
Player.max_lives = 5 # cambia para todas las instancias actuales y futurasUsa variables de clase para valores compartidos entre todas las instancias: constantes, contadores, valores por defecto. Usa variables de instancia para información que difiere por objeto.
self dentro de __init__ pertenece a ese único objeto. Escribir self.attr = value siempre crea o actualiza la copia propia de la instancia. Usa variables de clase cuando un valor es igual para todos, y variables de instancia cuando difiere por objeto. __str__ y __repr__
__str__ controla lo que print() y los f-strings muestran de tu objeto. __repr__ controla la vista para desarrolladores que se muestra en la consola y para depuración. Define siempre __repr__. Define __str__ cuando quieras una presentación limpia para el usuario, distinta de la vista de depuración.
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
def __str__(self):
return f"{self.name} ({self.score} pts)"
def __repr__(self):
return f"Player(name={self.name!r}, score={self.score})"
alice = Player("Alice", 87)
print(alice) # "Alice (87 pts)" (usa __str__)
repr(alice) # "Player(name='Alice', score=87)" (usa __repr__)Define siempre __repr__. Define __str__ cuando quieras una representación limpia para el usuario, distinta de la vista de depuración. Si solo se define __repr__, Python lo usa para ambos casos.
__str__ es lo que muestran print() y los f-strings, la versión amigable. __repr__ es la vista para desarrolladores que ves en la consola. Escribe siempre __repr__; es el que cumple su función incluso cuando olvidas __str__. Agrega __str__ solo cuando el texto orientado al usuario deba leerse distinto. Convención de privacidad
Python no tiene variables realmente privadas, pero un solo guion bajo al inicio de un nombre (_balance) es una convención que señala "esto es interno, no lo uses directamente desde fuera de la clase". No lo impone el lenguaje; es una forma de comunicárselo a otros desarrolladores.
class BankAccount:
def __init__(self, balance):
self._balance = balance # _ significa "no tocar"
def deposit(self, amount):
if amount > 0:
self._balance += amount
def balance(self):
return self._balanceUn doble guion bajo (__name) activa el name mangling; Python renombra el atributo como _ClassName__name para evitar conflictos en subclases. Rara vez se necesita. El guion bajo simple es la convención en la mayoría del código.
_balance) es la señal acordada para "interno, no tocar desde afuera". Nada te impide acceder a ella, es un mensaje para otros desarrolladores, incluyendo tu propio yo futuro. El doble guion bajo es una herramienta más rara para evitar choques de nombres en subclases; el guion bajo simple es lo que usarás en el día a día. Herencia
Una clase puede heredar de otra clase, obteniendo automáticamente todos sus atributos y métodos. Luego puedes sobrescribir métodos específicos en la subclase para cambiar su comportamiento. Esto te permite reutilizar una base común y especializarte donde sea necesario.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
print(pet.speak())Dog y Cat heredan __init__ de Animal, así que no necesitan el suyo propio. Sobrescriben speak() con su comportamiento específico.
Dog y Cat reutilizan el __init__ de Animal y solo redefinen speak(). super()
super() llama a un método de la clase padre. Úsalo cuando quieras extender el comportamiento del padre en vez de reemplazarlo por completo: llama al __init__ del padre para que ejecute su configuración, y luego agrega lo que tu subclase necesite además de eso.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
class Dog(Animal):
def __init__(self, name):
super().__init__(name, "Woof") # llama a Animal.__init__
self.tricks = [] # agrega algo extra
def learn(self, trick):
self.tricks.append(trick)
rex = Dog("Rex")
rex.learn("sit")
print(rex.tricks) # ["sit"]Llama siempre a super().__init__() cuando tu subclase tiene su propio __init__ y el padre también tiene uno.
super() te lleva a la clase padre, así que super().__init__() ejecuta la configuración del padre antes de que agregues la tuya. Úsalo cuando tu subclase escriba su propio __init__ y el padre también tenga uno. Si lo omites, la configuración del padre nunca se ejecuta y el objeto queda a medio construir. Métodos de clase y métodos estáticos
@classmethod crea un método que recibe la clase en sí en vez de una instancia. Es útil para constructores alternativos: crear una instancia a partir de una cadena, un archivo u otro formato. @staticmethod es una función normal que vive dentro de la clase por razones de organización; no recibe ni la instancia ni la clase.
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
@classmethod
def from_string(cls, data):
name, score = data.split(",")
return cls(name, int(score))
alice = Player.from_string("Alice,87")class Player:
@staticmethod
def is_valid_name(name):
return name.isalpha() and len(name) >= 2
Player.is_valid_name("Alice") # True
Player.is_valid_name("A1") # FalseUsa @classmethod para constructores alternativos. Usa @staticmethod para funciones utilitarias que lógicamente pertenecen a la clase pero no necesitan información de la instancia ni de la clase.
@classmethod te entrega la clase en vez de una instancia, lo cual lo convierte en la opción para constructores alternativos: construir un Player a partir de una cadena, un archivo, cualquier formato que tengas. @staticmethod es una función común guardada dentro de la clase por prolijidad; no recibe ni la clase ni la instancia. Los métodos normales tocan la información de un objeto, estos dos no. @property
@property te permite acceder a un método como si fuera un atributo, sin necesidad de paréntesis. Úsalo para valores que se calculan a partir de otros atributos y se leen naturalmente como un simple acceso a un atributo.
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(c.area) # 78.53975 (se ve como un atributo, funciona como un método)
print(c.diameter) # 10Las properties son útiles para valores calculados: cosas derivadas de otros atributos y que se leen naturalmente sin ().
@property te permite leer un método como si fuera un atributo, sin paréntesis: c.area en vez de c.area(). Le queda bien a valores que se calculan a partir de otros atributos y se leen naturalmente como datos simples. Detrás de escena, sigue ejecutando tu método cada vez que accedes. En la práctica
Una clase Player con atributos de instancia, métodos, una @property y __str__:
class Player:
max_lives = 3
def __init__(self, name: str):
self.name = name
self.score = 0
self.lives = Player.max_lives
def earn_points(self, amount: int) -> None:
self.score += amount
def take_hit(self) -> bool:
self.lives -= 1
return self.lives > 0
@property
def is_alive(self) -> bool:
return self.lives > 0
def __str__(self) -> str:
return f"{self.name} | Score: {self.score} | Lives: {self.lives}"
alice = Player("Alice")
alice.earn_points(50)
alice.take_hit()
print(alice) # "Alice | Score: 50 | Lives: 2"
print(alice.is_alive) # True
