Python & AI Tutorials Logo
Programación Python

30. Introducción a las clases y los objetos

30.1) La idea de la programación orientada a objetos (construir tus propios tipos)

A lo largo de este libro, has estado trabajando con los tipos integrados de Python: enteros, cadenas, listas, diccionarios y más. Cada tipo agrupa datos (como los caracteres de una cadena) con operaciones que puedes realizar sobre esos datos (como .upper() o .split()). Esta combinación de datos y comportamiento es poderosa: te permite pensar en las cadenas como entidades completas con sus propias capacidades, no solo como secuencias de caracteres sin procesar.

La programación orientada a objetos (OOP) amplía esta idea: te permite crear tus propios tipos personalizados, llamados clases, que agrupan datos y comportamiento específicos de tu dominio del problema. Del mismo modo que Python proporciona un tipo str para trabajar con texto y un tipo list para trabajar con secuencias, puedes crear un tipo BankAccount para gestionar transacciones financieras, un tipo Student para llevar registros académicos o un tipo Product para un sistema de inventario.

¿Por qué crear tus propios tipos?

Considera la gestión de información sobre estudiantes en un sistema escolar. Sin clases, podrías usar variables separadas o diccionarios:

python
# Usar variables separadas: se vuelve un caos rápidamente
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
 
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
 
# O usar diccionarios: mejor, pero aún limitado
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}

Este enfoque funciona para casos simples, pero tiene limitaciones:

  1. Sin validación: Nada evita que establezcas gpa en un valor no válido como -5.0 o "excellent"
  2. Sin comportamiento relacionado: Operaciones como calcular el estado de honores o dar formato a la información del estudiante son funciones separadas dispersas por todo tu código
  3. Sin verificación de tipos: Un diccionario que representa a un estudiante se ve idéntico a cualquier otro diccionario; Python no puede ayudarte a detectar errores en los que usas accidentalmente un diccionario de producto donde se esperaba un diccionario de estudiante

Las clases resuelven estos problemas al permitirte definir un tipo nuevo que representa exactamente qué es un estudiante y qué operaciones tienen sentido para estudiantes:

python
# Avanzaremos hacia esto: una clase Student que agrupa datos y comportamiento
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def display_info(self):
        status = "Honors" if self.is_honors() else "Regular"
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
 
# Ahora podemos crear objetos student
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.display_info())  # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors())       # Output: True

Este capítulo te enseñará cómo construir clases como esta desde cero. Empezaremos con las clases más simples posibles y les iremos añadiendo funcionalidades poco a poco hasta que puedas crear tipos personalizados ricos y útiles.

Clases vs. instancias: la analogía del plano

Comprender la distinción entre una clase y una instancia es fundamental para la programación orientada a objetos:

  • Una clase es como un plano o plantilla. Define qué datos contendrá un tipo de objeto y qué operaciones puede realizar. La clase en sí no es un estudiante específico; es la definición de lo que significa ser un estudiante.

  • Una instancia (también llamada objeto) es un ejemplo específico creado a partir de ese plano. Cuando creas alice = Student("Alice Johnson", "S12345", 3.8), estás creando una instancia específica de estudiante con los datos particulares de Alice.

Clase Student
Plano

instancia alice
Nombre: Alice Johnson
ID: S12345
GPA: 3.8

instancia bob
Nombre: Bob Smith
ID: S12346
GPA: 3.5

instancia carol
Nombre: Carol Davis
ID: S12347
GPA: 3.9

Puedes crear tantas instancias como necesites a partir de una sola clase, igual que un arquitecto puede usar un plano para construir muchas casas. Cada instancia tiene sus propios datos (el GPA de Alice es distinto del de Bob), pero todas comparten la misma estructura y capacidades definidas por la clase.

Lo que aprenderás en este capítulo

Este capítulo introduce los conceptos centrales de la programación orientada a objetos en Python:

  1. Definir clases (classes) con la palabra clave class
  2. Crear instancias (instances) y acceder a sus atributos
  3. Añadir métodos (methods) que operan sobre datos de instancia
  4. Entender self y cómo los métodos acceden a los datos de instancia
  5. Inicializar instancias con el método __init__
  6. Controlar representaciones de cadena con __str__ y __repr__
  7. Crear múltiples instancias independientes a partir de la misma clase

Al final de este capítulo, podrás diseñar e implementar tus propios tipos personalizados que hagan tus programas más organizados, mantenibles y expresivos. Construiremos sobre estas bases en el Capítulo 31 con características de clase más avanzadas, y en el Capítulo 32 con herencia y polimorfismo.

30.2) Definir clases simples con class

Empecemos creando la clase más simple posible: una que solo define un tipo nuevo sin datos ni comportamiento todavía.

La palabra clave class

Defines una clase usando la palabra clave class, seguida del nombre de la clase y dos puntos:

python
class Student:
    pass  # Clase vacía por ahora
 
# Crear una instancia
alice = Student()
print(alice)  # Output: <__main__.Student object at 0x...>
print(type(alice))  # Output: <class '__main__.Student'>

Incluso esta clase mínima es útil: crea un tipo nuevo llamado Student. Cuando creas una instancia con alice = Student(), Python crea un objeto nuevo de tipo Student. La salida muestra que alice es efectivamente un objeto Student, aunque todavía no hace nada interesante.

Convenciones de nombres de clases

Los nombres de clases en Python siguen una convención específica llamada CapWords o PascalCase: cada palabra empieza con mayúscula, sin guiones bajos entre palabras:

python
class BankAccount:      # Bien: CapWords
    pass
 
class ProductInventory:  # Bien: CapWords
    pass
 
class HTTPRequest:      # Bien: los acrónimos van en mayúsculas
    pass
 
# Evita estos estilos para clases:
# class bank_account:   # Wrong: snake_case is for functions/variables
# class bankaccount:    # Wrong: hard to read
# class BANKACCOUNT:    # Wrong: ALL_CAPS is for constants

Esta convención ayuda a distinguir las clases de las funciones y variables (que usan snake_case) cuando lees código.

Crear instancias

Crear una instancia a partir de una clase se ve como llamar a una función: usas el nombre de la clase seguido de paréntesis:

python
class Product:
    pass
 
# Crear tres instancias de producto diferentes
item1 = Product()
item2 = Product()
item3 = Product()
 
# Cada instancia es un objeto separado
print(item1)  # Output: <__main__.Product object at 0x...>
print(item2)  # Output: <__main__.Product object at 0x...>
print(item3)  # Output: <__main__.Product object at 0x...>
 
# Son objetos diferentes, aunque sean del mismo tipo
print(item1 is item2)  # Output: False
print(type(item1) is type(item2))  # Output: True

Cada llamada a Product() crea una instancia nueva e independiente. Las direcciones de memoria (la parte 0x...) son diferentes, lo que confirma que son objetos separados en memoria.

¿Por qué empezar con clases vacías?

Puede que te preguntes por qué estamos empezando con clases que no hacen nada. Hay dos razones:

  1. Claridad conceptual: Entender que una clase es solo un tipo nuevo, separado de sus datos y comportamiento, te ayuda a captar el concepto fundamental antes de añadir complejidad.

  2. Uso práctico: Incluso las clases vacías pueden ser útiles como marcadores o placeholders. Por ejemplo, podrías definir tipos de excepción personalizados:

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# Estas clases vacías sirven como tipos de error distintos

Sin embargo, las clases vacías son raras en código real. Añadamos algunos datos para que nuestras clases sean útiles.

30.3) Crear instancias y acceder a atributos

Las clases se vuelven útiles cuando contienen datos. En Python, puedes añadir atributos (attributes) (datos adjuntos a una instancia) en cualquier momento simplemente asignándolos.

Añadir atributos a instancias

Puedes añadir atributos a una instancia usando la notación de punto:

python
class Student:
    pass
 
# Crear una instancia
alice = Student()
 
# Añadir atributos
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
 
# Acceder a atributos
print(alice.name)        # Output: Alice Johnson
print(alice.student_id)  # Output: S12345
print(alice.gpa)         # Output: 3.8

El operador punto (.) accede a atributos: alice.name significa “obtén el atributo name del objeto alice”. Esta es la misma sintaxis que has estado usando con cadenas (como text.upper()) y listas (como numbers.append(5)): eso accede a métodos y atributos de esos objetos.

Cada instancia tiene sus propios atributos

Instancias diferentes de la misma clase tienen atributos independientes:

python
class Student:
    pass
 
# Crear dos estudiantes
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Cada instancia tiene sus propios datos
print(alice.name)  # Output: Alice Johnson
print(bob.name)    # Output: Bob Smith
 
# Cambiar una no afecta a la otra
alice.gpa = 3.9
print(alice.gpa)  # Output: 3.9
print(bob.gpa)    # Output: 3.5 (unchanged)

Esta independencia es crucial: alice y bob son objetos separados con datos separados. Modificar alice.gpa no afecta bob.gpa.

Los atributos pueden ser de cualquier tipo

Los atributos no se limitan a tipos simples: pueden contener cualquier valor de Python:

python
class Student:
    pass
 
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90]  # Atributo de lista
student.contact = {                 # Atributo de diccionario
    "email": "carol@example.com",
    "phone": "555-0123"
}
student.is_active = True            # Atributo booleano
 
# Acceder a datos anidados
print(student.grades[0])           # Output: 95
print(student.contact["email"])    # Output: carol@example.com

Esta flexibilidad te permite modelar entidades del mundo real complejas con estructuras de datos ricas.

Acceder a atributos inexistentes

Intentar acceder a un atributo que no existe genera un AttributeError:

python
class Student:
    pass
 
student = Student()
student.name = "David Lee"
 
print(student.name)  # Output: David Lee
# print(student.age)  # AttributeError: 'Student' object has no attribute 'age'

Este error es útil: detecta errores tipográficos y errores de lógica cuando esperas que exista un atributo, pero no existe.

El problema de la asignación manual de atributos

Aunque puedes añadir atributos manualmente después de crear una instancia, este enfoque tiene desventajas serias:

python
class Student:
    pass
 
# Es fácil olvidar atributos o escribirlos mal
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# ¡Se olvidó establecer gpa!
 
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346"  # Error tipográfico: 'stuent' en lugar de 'student'
bob.gpa = 3.5
 
# Ahora a alice le falta gpa, y bob tiene un error tipográfico
# print(alice.gpa)  # AttributeError
# print(bob.student_id)  # AttributeError

Esto es propenso a errores y tedioso. Necesitas una forma de garantizar que cada instancia comience con los atributos correctos. Ahí es donde entra el método __init__, que veremos en la sección 30.5. Pero primero, aprendamos sobre métodos: funciones que pertenecen a una clase.

30.4) Añadir métodos de instancia: comprender self

Los métodos son funciones definidas dentro de una clase que operan sobre los datos de instancia. Les dan a tus clases comportamiento, no solo datos.

Definir un método simple

Añadamos un método a nuestra clase Student:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Crear una instancia y añadir atributos
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Llamar al método
alice.display_info()  # Output: Alice Johnson - GPA: 3.8

El método display_info se define dentro de la clase usando def, igual que las funciones normales. La diferencia clave es el primer parámetro: self.

Comprender self

El parámetro self es cómo un método accede a la instancia específica sobre la que está operando. Cuando llamas alice.display_info(), Python pasa automáticamente alice como el primer argumento al método. Dentro del método, self se refiere a alice, así que self.name accede a alice.name y self.gpa accede a alice.gpa.

Esto es lo que sucede entre bastidores:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Estas dos llamadas son equivalentes:
alice.display_info()           # Forma normal
Student.display_info(alice)    # Lo que Python realmente hace
 
# Both output: Alice Johnson - GPA: 3.8

Cuando escribes alice.display_info(), Python lo traduce a Student.display_info(alice). La instancia (alice) se convierte en el parámetro self dentro del método.

¿Por qué "self"?

El nombre self es una convención, no una palabra clave. Técnicamente podrías usar cualquier nombre:

python
class Student:
    def display_info(this):  # Funciona, pero no hagas esto
        print(f"{this.name} - GPA: {this.gpa}")

Sin embargo, usa siempre self. Es una convención universal de Python que hace que tu código sea legible para otros programadores de Python. Usar cualquier otro nombre confundirá a quien lee y viola los estándares de la comunidad.

Métodos con múltiples instancias

El poder de self se vuelve claro cuando tienes múltiples instancias:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Crear dos estudiantes
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Mismo método, datos diferentes
alice.display_info()  # Output: Alice Johnson - GPA: 3.8
bob.display_info()    # Output: Bob Smith - GPA: 3.5

Cuando llamas alice.display_info(), self es alice. Cuando llamas bob.display_info(), self es bob. El mismo código de método funciona para cualquier instancia porque self se adapta a la instancia que lo llamó.

alice.display_info

self = alice

bob.display_info

self = bob

Acceder a alice.name
alice.gpa

Acceder a bob.name
bob.gpa

Los métodos pueden aceptar parámetros adicionales

Los métodos pueden aceptar parámetros además de self:

python
class Student:
    def update_gpa(self, new_gpa):
        self.gpa = new_gpa
        print(f"Se ha actualizado el GPA de {self.name} a {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
alice.update_gpa(3.9)  # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa)       # Output: 3.9

Cuando llamas alice.update_gpa(3.9), Python pasa alice como self y 3.9 como new_gpa. La firma del método es def update_gpa(self, new_gpa), pero solo pasas un argumento al llamarlo: Python gestiona self automáticamente.

Los métodos pueden devolver valores

Los métodos pueden devolver valores igual que las funciones normales:

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def get_status(self):
        if self.is_honors():
            return "Honors Student"
        else:
            return "Regular Student"
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
 
print(alice.get_status())  # Output: Honors Student
print(bob.get_status())    # Output: Regular Student

Observa cómo get_status llama a otro método (is_honors) usando self.is_honors(). Los métodos pueden llamar a otros métodos en la misma instancia.

Métodos vs. funciones: cuándo usar cada uno

Puede que te preguntes cuándo conviene usar un método y cuándo una función independiente. Aquí tienes la guía:

Usa un método cuando la operación:

  • Necesite acceso a datos de instancia (self.name, self.gpa, etc.)
  • Pertenezca lógicamente al tipo (es algo que un Student hace o es)
  • Modifique el estado de la instancia

Usa una función independiente cuando la operación:

  • No necesite datos de instancia
  • Funcione con múltiples tipos
  • Sea una utilidad general
python
class Student:
    # Método: necesita datos de instancia
    def is_honors(self):
        return self.gpa >= 3.5
 
# Función: utilidad general, funciona con cualquier valor de GPA
def calculate_letter_grade(gpa):
    if gpa >= 3.7:
        return "A"
    elif gpa >= 3.0:
        return "B"
    elif gpa >= 2.0:
        return "C"
    else:
        return "D"
 
alice = Student()
alice.gpa = 3.8
 
# Usar el método para comprobaciones específicas de la instancia
print(alice.is_honors())  # Output: True
 
# Usar la función para cálculos generales
print(calculate_letter_grade(alice.gpa))  # Output: A
print(calculate_letter_grade(2.5))        # Output: C

Patrones comunes de métodos

Aquí tienes algunos patrones comunes que usarás con frecuencia:

Métodos getter (recuperan información calculada):

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

Métodos setter (modifican atributos con validación):

python
class Student:
    def set_gpa(self, new_gpa):
        if 0.0 <= new_gpa <= 4.0:
            self.gpa = new_gpa
        else:
            print("Invalid GPA: must be between 0.0 and 4.0")

Métodos de consulta (responden preguntas de sí/no):

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

Métodos de acción (realizan operaciones):

python
class Student:
    def add_grade(self, grade):
        self.grades.append(grade)
        # Recalcular el GPA basado en todas las calificaciones
        self.gpa = sum(self.grades) / len(self.grades)

30.5) Inicializar instancias con __init__

Establecer atributos manualmente después de crear una instancia es tedioso y propenso a errores. El método __init__ resuelve esto al permitirte inicializar instancias con datos cuando se crean.

El método __init__

El método __init__ (se pronuncia "dunder init" o "init") es un método especial que Python llama automáticamente cuando creas una instancia nueva:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Crear instancias con datos iniciales
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.name)  # Output: Alice Johnson
print(bob.gpa)     # Output: 3.5

Cuando escribes Student("Alice Johnson", "S12345", 3.8), Python:

  1. Crea una instancia Student nueva y vacía
  2. Llama a __init__ con esa instancia como self y tus argumentos
  3. Devuelve la instancia inicializada

El método __init__ no devuelve explícitamente un valor: modifica la instancia in situ estableciendo sus atributos. Si intentas devolver un valor desde __init__, Python lanzará un TypeError.

python
class Student:
    def __init__(self, name):
        self.name = name
        # No devuelvas nada desde __init__
        # return self  # Wrong! TypeError: __init__() should return None, not 'Student'

Cómo funciona __init__

Desglosemos lo que ocurre paso a paso:

python
class Student:
    def __init__(self, name, student_id, gpa):
        print(f"Initializing student: {name}")
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
        print(f"Initialization complete")
 
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
 
print(alice.name)  # Output: Alice Johnson

Los parámetros después de self (name, student_id, gpa) se convierten en argumentos obligatorios al crear una instancia. Si no los proporcionas, Python lanza un TypeError:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# student = Student()  # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice")  # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8)  # Correct

Esto es mucho mejor que la asignación manual de atributos: Python obliga a que cada instancia comience con los datos necesarios.

Valores de parámetro por defecto en __init__

Puedes usar valores de parámetro por defecto en __init__, igual que en las funciones normales:

python
class Student:
    def __init__(self, name, student_id, gpa=0.0):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# El GPA es opcional, por defecto es 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346")  # Usa el valor por defecto gpa=0.0
 
print(alice.gpa)  # Output: 3.8
print(bob.gpa)    # Output: 0.0

Esto es útil para atributos que tienen valores por defecto razonables, pero que se pueden personalizar cuando sea necesario.

Validación en __init__

Puedes validar la entrada en __init__ para asegurar que las instancias comiencen en un estado válido:

python
class Student:
    def __init__(self, name, student_id, gpa):
        if not name:
            print("Error: Name cannot be empty")
            self.name = "Unknown"
        else:
            self.name = name
        
        self.student_id = student_id
        
        if 0.0 <= gpa <= 4.0:
            self.gpa = gpa
        else:
            print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
            self.gpa = 0.0
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa)  # Output: 3.8
 
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name)  # Output: Unknown
print(bob.gpa)   # Output: 0.0

Esto garantiza que, incluso si alguien pasa datos no válidos, la instancia termina en un estado razonable.

30.6) Representaciones de cadena con __str__ y __repr__

Cuando imprimes una instancia con print() o la ves en la shell interactiva, Python necesita convertirla a una cadena. Por defecto, obtienes algo poco útil:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: <__main__.Student object at 0x...>

La salida por defecto muestra el nombre de la clase y la dirección de memoria, pero nada sobre los datos reales de Alice. Puedes personalizar esto con los métodos especiales __str__ y __repr__.

El método __str__

El método __str__ define cómo tus instancias se convierten a cadenas mediante print() y str():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice))  # Output: Alice Johnson (S12345) - GPA: 3.8

El método __str__ debería devolver una cadena legible e informativa para usuarios finales. Piensa en ello como la representación “amigable”.

El método __repr__

El método __repr__ define la representación de cadena “oficial” de tus instancias, utilizada por el REPL y repr():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice))  # Output: Student('Alice Johnson', 'S12345', 3.8)

En el REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

El método __repr__ debería devolver una cadena que parezca código Python válido para recrear el objeto. Piensa en ello como la representación para “desarrolladores”: debe ser inequívoca y útil para depurar.

Usar __str__ y __repr__

Puedes definir ambos métodos para propósitos distintos:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        # Formato amigable y legible
        return f"{self.name} - GPA: {self.gpa}"
    
    def __repr__(self):
        # Formato inequívoco, similar a código
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
 
print(alice)        # Usa __str__
# Output: Alice Johnson - GPA: 3.8
 
print(repr(alice))  # Usa __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)

En el REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice  # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice)  # Uses __str__
Alice Johnson - GPA: 3.8

Cuándo definir cada método

Aquí tienes la guía:

  • Define siempre __repr__: lo usan el REPL y las herramientas de depuración. Si solo defines uno, define este.
  • Define __str__ cuando necesites un formato fácil de usar: si tu clase se imprimirá para usuarios finales, proporciona un __str__ legible.
  • Si solo defines __repr__: Python lo usa para repr(), y str() recurre a __repr__ también (así que print() también lo usa).
  • Si solo defines __str__: print() usa __str__, pero repr() y el REPL usan el __repr__ por defecto (mostrando la dirección de memoria). Por eso definir __repr__ suele ser más importante.
python
# Solo se define __repr__
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
 
item = Product("Laptop", 999.99)
print(item)        # Usa __repr__ como fallback
# Output: Product('Laptop', 999.99)
print(repr(item))  # Usa __repr__
# Output: Product('Laptop', 999.99)

Representación de cadenas en colecciones

Cuando las instancias están dentro de colecciones (listas, dicts, etc.), Python usa __repr__ para mostrarlas, no __str__:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name}: {self.gpa}"
    
    def __repr__(self):
        return f"Student('{self.name}', {self.gpa})"
 
students = [
    Student("Alice", 3.8),
    Student("Bob", 3.5),
    Student("Carol", 3.9)
]
 
# Imprimir la lista usa __repr__ para cada estudiante
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
 
# Imprimir estudiantes individuales usa __str__
for student in students:
    print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9

Por eso __repr__ debe ser inequívoco: te ayuda a entender qué hay en tus estructuras de datos durante la depuración. Cuando imprimes una lista, Python esencialmente llama a repr() en cada elemento para mostrar la estructura con claridad.

30.7) Crear múltiples instancias independientes

Uno de los aspectos más potentes de las clases es que puedes crear muchas instancias independientes, cada una con sus propios datos. Exploremos esto en profundidad.

Cada instancia tiene sus propios datos

Cuando creas múltiples instancias a partir de la misma clase, cada una mantiene sus propios atributos separados:

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0.0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
            return True
        else:
            print(f"Insufficient funds. Balance: ${self.balance:.2f}")
            return False
    
    def __str__(self):
        return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
 
# Crear tres cuentas independientes
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
 
# Las operaciones en una cuenta no afectan a las otras
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
 
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
 
# Cada cuenta mantiene su propio saldo
print(alice_account)  # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account)    # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account)  # Output: Carol Davis's account (ACC-003): $2000.00

Esta independencia es fundamental para la programación orientada a objetos. Cada instancia es una entidad separada con su propio estado.

Instancias en colecciones

Puedes almacenar instancias en listas, diccionarios o cualquier otra colección:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
# Crear una lista de estudiantes
students = [
    Student("Alice Johnson", "S12345", 3.8),
    Student("Bob Smith", "S12346", 3.2),
    Student("Carol Davis", "S12347", 3.9),
    Student("David Lee", "S12348", 3.4)
]
 
# Encontrar todos los estudiantes con honores
honors_students = []
for student in students:
    if student.is_honors():
        honors_students.append(student)
 
print("Honors students:")
for student in honors_students:
    print(f"  {student.name}: {student.gpa}")
# Output:
# Honors students:
#   Alice Johnson: 3.8
#   Carol Davis: 3.9
 
# Calcular el GPA promedio
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}")  # Output: Average GPA: 3.58

Este es un patrón común: crear múltiples instancias, almacenarlas en una colección y luego procesarlas con bucles(loop) y comprensiones.

Las instancias pueden referenciar otras instancias

Las instancias pueden tener atributos que referencian a otras instancias, creando relaciones entre objetos:

python
class Course:
    def __init__(self, course_code, course_name):
        self.course_code = course_code
        self.course_name = course_name
    
    def __str__(self):
        return f"{self.course_code}: {self.course_name}"
 
class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.courses = []  # Lista de instancias Course
    
    def enroll(self, course):
        self.courses.append(course)
        print(f"{self.name} enrolled in {course.course_name}")
    
    def list_courses(self):
        print(f"{self.name}'s courses:")
        for course in self.courses:
            print(f"  {course}")
 
# Crear cursos
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
 
# Crear estudiantes e inscribirlos en cursos
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
 
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
 
# Listar los cursos de cada estudiante
alice.list_courses()
# Output:
# Alice Johnson's courses:
#   CS101: Introduction to Python
#   CS102: Data Structures
 
bob.list_courses()
# Output:
# Bob Smith's courses:
#   CS101: Introduction to Python
#   CS103: Web Development

Observa que tanto Alice como Bob están inscritos en python_course: están referenciando la misma instancia de Course. Esto modela la relación del mundo real en la que varios estudiantes pueden tomar el mismo curso.

Identidad e igualdad de instancias

Cada instancia es un objeto único, incluso si tiene los mismos datos que otra instancia:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
 
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
 
# Objetos diferentes, incluso con datos idénticos
print(alice1 is alice2)  # Output: False
print(id(alice1) == id(alice2))  # Output: False

Por defecto, == también comprueba la identidad (si son el mismo objeto), no si tienen los mismos datos. En el Capítulo 31, aprenderemos a personalizar la comparación de igualdad con el método especial __eq__.


Este capítulo te ha introducido en los fundamentos de la programación orientada a objetos en Python. Has aprendido a definir clases, crear instancias, añadir métodos, inicializar instancias con __init__, controlar representaciones de cadena y trabajar con múltiples instancias independientes. Estos conceptos forman la base para características de POO más avanzadas que exploraremos en los Capítulos 31 y 32.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai