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:
# 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:
- Sin validación: Nada evita que establezcas
gpaen un valor no válido como-5.0o"excellent" - 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
- 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:
# 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: TrueEste 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.
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:
- Definir clases (classes) con la palabra clave
class - Crear instancias (instances) y acceder a sus atributos
- Añadir métodos (methods) que operan sobre datos de instancia
- Entender
selfy cómo los métodos acceden a los datos de instancia - Inicializar instancias con el método
__init__ - Controlar representaciones de cadena con
__str__y__repr__ - 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:
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:
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 constantsEsta 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:
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: TrueCada 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:
-
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.
-
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:
class InvalidGradeError:
pass
class StudentNotFoundError:
pass
# Estas clases vacías sirven como tipos de error distintosSin 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:
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.8El 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:
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:
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.comEsta 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:
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:
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) # AttributeErrorEsto 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:
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.8El 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:
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.8Cuando 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:
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:
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.5Cuando 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ó.
Los métodos pueden aceptar parámetros adicionales
Los métodos pueden aceptar parámetros además de self:
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.9Cuando 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:
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 StudentObserva 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
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: CPatrones comunes de métodos
Aquí tienes algunos patrones comunes que usarás con frecuencia:
Métodos getter (recuperan información calculada):
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):
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):
class Student:
def is_honors(self):
return self.gpa >= 3.5
def is_failing(self):
return self.gpa < 2.0Métodos de acción (realizan operaciones):
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:
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.5Cuando escribes Student("Alice Johnson", "S12345", 3.8), Python:
- Crea una instancia
Studentnueva y vacía - Llama a
__init__con esa instancia comoselfy tus argumentos - 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.
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:
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 JohnsonLos 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:
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) # CorrectEsto 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:
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.0Esto 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:
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.0Esto 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:
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():
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.8El 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():
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:
>>> 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:
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:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice) # Uses __str__
Alice Johnson - GPA: 3.8Cuá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 pararepr(), ystr()recurre a__repr__también (así queprint()también lo usa). - Si solo defines
__str__:print()usa__str__, perorepr()y el REPL usan el__repr__por defecto (mostrando la dirección de memoria). Por eso definir__repr__suele ser más importante.
# 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__:
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.9Por 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:
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.00Esta 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:
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.58Este 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:
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 DevelopmentObserva 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:
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: FalsePor 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.