Skip to content
This page has been auto-translated and may contain errors.View in English

Clases y objetos

Cada tipo con el que has trabajado hasta ahora (cadenas, listas, diccionarios) es en realidad una clase. Cuando llamas a "hola".upper(), estás llamando a un método sobre un objeto de cadena. Las clases te permiten definir tus propios tipos con sus propios datos y comportamiento. Una clase Player puede almacenar un nombre, una puntuación y un nivel, y saber cómo mostrarse a sí misma.

Las clases son el mecanismo para los tipos definidos por el usuario. Una clase define una plantilla: los datos que contiene cada instancia (atributos) y las operaciones que admite (métodos). En lugar de rastrear valores en variables paralelas y pasarlos por todas partes, los agrupas en un solo objeto cohesivo con una interfaz clara.

Una sentencia class crea un nuevo objeto de tipo en el espacio de nombres actual, usando una metaclase (por defecto type) para construirlo. Las instancias se crean llamando a la clase, lo que invoca __new__ para asignar el objeto y __init__ para inicializarlo. El modelo de objetos de Python es uniforme: todo es un objeto, incluidas las clases, las funciones y los tipos mismos.

Plano e instancias

Una clase es un plano. Una instancia es algo específico hecho a partir de ese plano. Puedes crear tantas instancias como necesites, cada una con sus propios datos pero compartiendo los mismos métodos definidos en la clase.

Una clase define estructura y comportamiento. Las instancias son objetos creados a partir de esa clase; cada una tiene su propio espacio de nombres de atributos pero comparte los objetos de método de la clase. Crear una instancia llama a la clase como si fuera una función: Dog() crea una nueva instancia de Dog.

Llamar a una clase invoca type.__call__, que llama a cls.__new__(cls) para asignar la instancia y a cls.__init__(instance, ...) para inicializarla. El __class__ del objeto resultante apunta de vuelta a la clase. La búsqueda de métodos sigue el MRO: __dict__ de la instancia, __dict__ de la clase y luego la cadena MRO.

python
class Dog:
    def bark(self):
        print("¡Guau!")

rex  = Dog()
luna = Dog()

rex.bark()    # "¡Guau!"
luna.bark()   # "¡Guau!"

Dog es la clase. rex y luna son instancias: dos perros distintos, cada uno compartiendo el mismo comportamiento definido en la clase.

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

__init__ inicializa una instancia recién asignada. self es un nombre convencional para el primer parámetro de cada método de instancia; Python pasa la instancia automáticamente cuando llamas a sofia.display(). Los atributos establecidos en self dentro de __init__ son atributos de instancia: cada instancia tiene su propia copia.

__init__ recibe la instancia ya asignada desde __new__ y la configura. self no es una palabra clave; es el primer parámetro posicional, pasado implícitamente por el protocolo de descriptores cuando llamas a instance.method(). Establecer self.attr = value llama a object.__setattr__, que escribe en el __dict__ de la instancia. Los atributos no establecidos en __init__ aún pueden establecerse posteriormente en cualquier instancia, ya que los diccionarios de Python son dinámicos.

python
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} puntos")

sofia  = Player("Sofía")
mateo  = Player("Mateo", score=50)

sofia.add_points(30)
sofia.display()   # "Sofía: 30 puntos"
mateo.display()   # "Mateo: 50 puntos"

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.

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 cambiar los datos de la instancia a través de self.

Los métodos de instancia son funciones regulares almacenadas en el espacio de nombres de la clase. El protocolo de descriptores transforma instance.method en un método ligado (bound method), vinculando self automáticamente. Devolver self desde un método permite encadenar métodos: obj.scale(2).rotate(90).

instance.method activa la búsqueda de descriptores: type.__getattribute__ encuentra method en el __dict__ de la clase, lo reconoce como una función (que implementa el protocolo de descriptores) y devuelve un objeto de método ligado que envuelve la función con la instancia preaplicada. Así es como self se pasa automáticamente. Devolver self permite interfaces fluidas.

python
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.159

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.

Las variables de clase viven en el __dict__ de la clase y son compartidas por todas las instancias. Las variables de instancia viven en el __dict__ de cada instancia. Cuando lees self.attr, Python verifica primero la instancia, luego la clase. Cuando escribes self.attr = value, siempre crea o actualiza el atributo de instancia, ocultando la variable de clase si existe.

La búsqueda de atributos en una instancia sigue este orden: __dict__ de la instancia, __dict__ de la clase y luego el MRO. Escribir en self.attr siempre apunta al __dict__ de la instancia (a menos que __setattr__ esté sobreescrito). Una variable de clase se comparte hasta que una escritura en self.attr crea una sombra de instancia. Las variables de clase mutables (como las listas) son particularmente delicadas: todas las instancias comparten el mismo objeto, por lo que la mutación a través de cualquier instancia es visible para todas.

python
class Player:
    max_lives = 3    # variable de clase, igual para cada 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

sofia = Player("Sofía")
mateo = Player("Mateo")

Player.max_lives = 5    # cambiar para todas las instancias actuales y futuras

Usa variables de clase para valores compartidos entre todas las instancias: constantes, contadores, valores por defecto. Usa variables de instancia para datos que difieren por objeto.

__str__ y __repr__

__str__ controla lo que print() y las f-strings muestran para tu objeto. __repr__ controla la vista de desarrollador mostrada en la consola y para depuración. Define siempre __repr__. Define __str__ cuando quieras una visualización limpia para el usuario separada de la vista de depuración.

__str__ es llamado por str(), print() y f-strings. __repr__ es llamado por repr() y se muestra en el REPL. Si solo se define __repr__, Python lo usa para ambos. La convención: __repr__ debe devolver una cadena que podría reconstruir el objeto; __str__ debe devolver un resumen legible para humanos.

str(obj) llama a type(obj).__str__(obj), recurriendo a __repr__ si __str__ no está definido. repr(obj) llama a type(obj).__repr__(obj). Dentro de las f-strings, {obj} llama a __format__, que por defecto llama a __str__. {obj!r} aplica repr() antes de formatear. La convención de __repr__: devolver una cadena de la forma ClassName(arg1=..., arg2=...) que, al evaluarse, reproduciría el objeto.

python
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})"

sofia = Player("Sofía", 87)
print(sofia)        # "Sofía (87 pts)"   (usa __str__)
repr(sofia)         # "Player(name='Sofía', score=87)"  (usa __repr__)

Define siempre __repr__. Define __str__ cuando quieras una representación limpia para el usuario separada de la vista de depuración. Si solo se define __repr__, Python lo usa para ambos.

Convención de privacidad

Python no tiene variables verdaderamente privadas, pero un guión bajo al inicio de un nombre (_balance) es una convención que indica "esto es interno, no lo uses directamente desde fuera de la clase". No lo impone el lenguaje; es una comunicación a otros desarrolladores.

Un solo guión bajo (_attr) es una convención que indica uso interno. Python no lo impone, pero todos los linters, IDEs y desarrolladores lo respetan. Un doble guión bajo (__attr) activa el mangling de nombres: Python lo reescribe como _ClassName__attr, lo que evita colisiones accidentales en las subclases. No es verdadera privacidad; es un mecanismo para evitar colisiones.

Un solo guión bajo: solo convención, sin imposición. Doble guión bajo: el mangling de nombres transforma __attr en _ClassName__attr en tiempo de compilación, no en tiempo de ejecución. Esto es una protección contra colisiones de subclases, no un mecanismo de privacidad; el nombre transformado sigue siendo accesible externamente. __slots__ es un control más fundamental: reemplaza el __dict__ de la instancia con un conjunto fijo de descriptores de slot, evitando la creación arbitraria de atributos y reduciendo la sobrecarga de memoria.

python
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._balance

Un doble guión bajo (__name) activa el mangling de nombres; Python renombra el atributo a _ClassName__name para evitar conflictos en las subclases. Rara vez es necesario. El guión bajo simple es la convención en la mayoría del código.

Herencia

Una clase puede heredar de otra clase, obteniendo automáticamente todos sus atributos y métodos. Luego puedes sobreescribir métodos específicos en la subclase para cambiar su comportamiento. Esto te permite reutilizar una base común y especializar donde sea necesario.

La herencia crea una relación "es-un". La subclase hereda todos los métodos y atributos del padre y puede sobreescribir cualquiera de ellos. El orden de resolución de métodos (MRO) define la secuencia de búsqueda de atributos. Python admite herencia múltiple; el MRO se calcula con el algoritmo de linearización C3.

El MRO se calcula mediante type.__mro_entries__ usando la linearización C3, accesible como ClassName.__mro__. La resolución de métodos sigue el MRO de izquierda a derecha. isinstance(obj, cls) recorre el MRO para verificar. La herencia múltiple es poderosa pero puede crear herencia en diamante; el MRO la resuelve de manera determinista. super() sigue el MRO, no solo la clase padre directa.

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name} dice ¡Guau!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} dice ¡Miau!"

pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
    print(pet.speak())

Dog y Cat heredan __init__ de Animal, por lo que no necesitan el suyo propio. Sobreescriben speak() con su comportamiento específico.

super()

super() llama a un método de la clase padre. Úsalo cuando quieras extender el comportamiento del padre en lugar de reemplazarlo por completo: llama al __init__ del padre para ejecutar su configuración, luego agrega encima cualquier cosa que necesite tu subclase.

super() devuelve un objeto proxy que delega las llamadas a métodos a la siguiente clase en el MRO. Siempre llama a super().__init__() desde el __init__ de una subclase cuando el padre tiene uno. Omitirlo significa que el código de configuración del padre no se ejecuta, lo que puede dejar el objeto en un estado roto.

super() sin argumentos (Python 3) usa __class__ (una variable de celda inyectada por el compilador) y el primer argumento del método circundante para determinar la posición del MRO. super(cls, self) es la forma explícita. El proxy resuelve la búsqueda de métodos comenzando desde la siguiente clase en type(self).__mro__ después de cls. Esto es esencial para la herencia múltiple correcta: super() encadena el __init__ de cada clase a través del MRO completo.

python
class Animal:
    def __init__(self, name, sound):
        self.name  = name
        self.sound = sound

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Guau")    # llamar a Animal.__init__
        self.tricks = []                  # agregar algo extra

    def learn(self, trick):
        self.tricks.append(trick)

rex = Dog("Rex")
rex.learn("sentarse")
print(rex.tricks)   # ["sentarse"]

Llama siempre a super().__init__() cuando tu subclase tenga su propio __init__ y el padre también lo tenga.

Métodos de clase y métodos estáticos

@classmethod crea un método que recibe la clase en sí en lugar 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 simple que vive dentro de la clase por razones organizativas; no recibe ni la instancia ni la clase.

@classmethod recibe cls (la clase) como su primer argumento, no una instancia. El uso principal son los constructores alternativos que crean instancias a partir de distintos formatos de entrada. @staticmethod es una función regular bajo el espacio de nombres de la clase; no tiene acceso a la clase ni a la instancia. Usa @classmethod para constructores, @staticmethod para funciones utilitarias lógicamente ligadas a la clase.

@classmethod usa el protocolo de descriptores: el descriptor classmethod devuelve un método ligado donde el primer argumento es la clase, no la instancia. cls es la clase real en el momento de la llamada, por lo que los métodos de subclase obtienen la subclase. @staticmethod es un descriptor más simple que devuelve la función subyacente sin cambios, sin primer argumento implícito. Ambos decoradores modifican el comportamiento del descriptor; ninguno afecta el objeto código de la función.

python
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))

sofia = Player.from_string("Sofía,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("Sofía")   # True
Player.is_valid_name("A1")      # False

Usa @classmethod para constructores alternativos. Usa @staticmethod para funciones utilitarias que lógicamente pertenecen a la clase pero no necesitan datos de instancia ni de clase.

@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 que se sienten naturales de leer como un acceso simple a atributos.

@property convierte un método en un atributo de solo lectura. El método se ejecuta cuando se accede al atributo. Esto es útil para valores calculados derivados de datos almacenados, y para agregar validación al acceso a atributos sin cambiar la interfaz pública. Un @name.setter emparejado hace que el atributo sea escribible.

@property es un descriptor: property(fget, fset, fdel, doc). Acceder a instance.attr llama a fget(instance). @name.setter establece fset. @name.deleter establece fdel. Las propiedades se buscan en la clase, no en la instancia, por lo que instance.__dict__[name] no oculta una propiedad (los descriptores de datos tienen prioridad sobre el __dict__ de la instancia). Las propiedades son la alternativa Pythónica a los métodos getter/setter al estilo Java.

python
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 (parece un atributo, se ejecuta como un método)
print(c.diameter)  # 10

Las propiedades son útiles para valores calculados: cosas derivadas de otros atributos que se sienten naturales de acceder sin ().

En la práctica

Una clase Player con atributos de instancia, métodos, un @property y __str__:

python
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} | Puntuación: {self.score} | Vidas: {self.lives}"

sofia = Player("Sofía")
sofia.earn_points(50)
sofia.take_hit()
print(sofia)            # "Sofía | Puntuación: 50 | Vidas: 2"
print(sofia.is_alive)   # True

Una clase User que usa un atributo privado con un getter @property, un método deactivate y un serializador to_dict:

python
class User:
    def __init__(self, user_id: int, username: str, email: str):
        self.id       = user_id
        self.username = username
        self.email    = email
        self._active  = True

    @property
    def active(self) -> bool:
        return self._active

    def deactivate(self) -> None:
        self._active = False

    def to_dict(self) -> dict:
        return {
            "id":       self.id,
            "username": self.username,
            "email":    self.email,
            "active":   self._active,
        }

    def __repr__(self) -> str:
        return f"User(id={self.id}, username={self.username!r})"

sofia = User(1, "sofia", "[email protected]")
print(sofia.to_dict())
sofia.deactivate()
print(sofia.active)   # False

Una clase DataSplit que encapsula el particionamiento de entrenamiento/validación detrás de propiedades, con __repr__ para una salida de depuración limpia:

python
class DataSplit:
    def __init__(self, data: list, train_ratio: float = 0.8):
        split       = int(len(data) * train_ratio)
        self._train = data[:split]
        self._val   = data[split:]

    @property
    def train(self) -> list:
        return self._train

    @property
    def val(self) -> list:
        return self._val

    @property
    def sizes(self) -> tuple[int, int]:
        return len(self._train), len(self._val)

    def __repr__(self) -> str:
        return f"DataSplit(train={len(self._train)}, val={len(self._val)})"

data  = list(range(100))
split = DataSplit(data, train_ratio=0.8)
print(split)         # DataSplit(train=80, val=20)
print(split.sizes)   # (80, 20)

El prefijo de guión bajo en _train y _val señala que quienes llamen deben pasar por las propiedades en lugar de mutar las listas crudas directamente. Python no lo impondrá, pero establece un contrato claro.