¿Cómo usar los métodos __init__, __str__ y __repr__ en Python?

PythonBeginner
Practicar Ahora

Introducción

Las características de la programación orientada a objetos (OOP, por sus siglas en inglés) de Python ofrecen herramientas poderosas para crear objetos expresivos y personalizados. Entre estas, los métodos especiales __init__, __str__ y __repr__ juegan roles cruciales en la definición del comportamiento y la representación de sus objetos Python.

En este tutorial, exploraremos estos métodos especiales, comprenderemos sus propósitos y aprenderemos cómo implementarlos eficazmente en sus clases Python. Al final de este laboratorio, podrá crear objetos Python más intuitivos y fáciles de usar.

Creación de Clases con el Método __init__

El primer método especial que exploraremos es __init__, que se llama cuando se crea un objeto. Este método le permite inicializar los atributos de su objeto.

Entendiendo el Método __init__

El método __init__, también conocido como constructor, se llama automáticamente cuando crea una nueva instancia de una clase. Su propósito principal es configurar el estado inicial del objeto asignando valores a los atributos.

Comencemos creando una clase Python simple con el método __init__:

  1. Abra la terminal en su entorno LabEx.

  2. Navegue al directorio del proyecto:

    cd ~/project
  3. Cree un nuevo archivo Python llamado person.py usando el editor de código:

  4. En el editor, agregue el siguiente código a person.py:

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    ## Create a Person object
    person1 = Person("Alice", 30)
    
    ## Access the attributes
    print(f"Name: {person1.name}")
    print(f"Age: {person1.age}")
  5. Guarde el archivo.

  6. Ejecute el script Python:

    python3 person.py

Debería ver la siguiente salida:

Name: Alice
Age: 30

Explicación del Método __init__

En el código anterior:

  • Definimos una clase Person con un método __init__ que toma tres parámetros: self, name y age.
  • El parámetro self se refiere a la instancia que se está creando y se pasa automáticamente cuando se crea un objeto.
  • Dentro del método __init__, asignamos los valores de name y age a los atributos del objeto usando self.name y self.age.
  • Cuando creamos un nuevo objeto Person con person1 = Person("Alice", 30), el método __init__ se llama automáticamente.
  • Luego podemos acceder a los atributos usando la notación de punto: person1.name y person1.age.

Añadiendo Más Funcionalidad

Mejoremos nuestra clase Person agregando un método para calcular el año de nacimiento basado en el año actual y la edad de la persona:

  1. Abra person.py en el editor.

  2. Actualice el código para incluir un método para calcular el año de nacimiento:

    import datetime
    
    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def birth_year(self):
            current_year = datetime.datetime.now().year
            return current_year - self.age
    
    ## Create a Person object
    person1 = Person("Alice", 30)
    
    ## Access the attributes and call the method
    print(f"Name: {person1.name}")
    print(f"Age: {person1.age}")
    print(f"Birth Year: {person1.birth_year()}")
  3. Guarde el archivo.

  4. Ejecute el script Python:

    python3 person.py

La salida ahora debería incluir el año de nacimiento calculado:

Name: Alice
Age: 30
Birth Year: 1993

(El año de nacimiento real dependerá del año actual cuando ejecute el script)

Ahora ha creado una clase Python con el método __init__ para inicializar los atributos del objeto y ha agregado un método para realizar cálculos basados en esos atributos.

Implementando el Método __str__

Ahora que entendemos cómo crear una clase con el método __init__, exploremos otro método especial llamado __str__. Este método nos permite definir cómo se debe representar un objeto como una cadena.

Entendiendo el Método __str__

El método __str__ se llama cuando usa la función str() en un objeto o cuando imprime un objeto usando la función print(). Debe devolver una representación de cadena legible por humanos del objeto.

Actualicemos nuestra clase Person para incluir un método __str__:

  1. Abra person.py en el editor.

  2. Actualice el código para incluir un método __str__:

    import datetime
    
    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def birth_year(self):
            current_year = datetime.datetime.now().year
            return current_year - self.age
    
        def __str__(self):
            return f"{self.name}, {self.age} years old"
    
    ## Create a Person object
    person1 = Person("Alice", 30)
    
    ## Access the attributes and call the method
    print(f"Name: {person1.name}")
    print(f"Age: {person1.age}")
    print(f"Birth Year: {person1.birth_year()}")
    
    ## Use the __str__ method implicitly
    print("\nString representation of the object:")
    print(person1)
    
    ## Use the __str__ method explicitly
    print("\nExplicit string conversion:")
    print(str(person1))
  3. Guarde el archivo.

  4. Ejecute el script Python:

    python3 person.py

Debería ver una salida similar a esta:

Name: Alice
Age: 30
Birth Year: 1993

String representation of the object:
Alice, 30 years old

Explicit string conversion:
Alice, 30 years old

Cómo Funciona __str__

En el código anterior:

  • Definimos un método __str__ que devuelve una cadena formateada con el nombre y la edad de la persona.
  • Cuando llamamos a print(person1), Python llama automáticamente al método __str__ para determinar qué mostrar.
  • También podemos convertir explícitamente un objeto a una cadena usando str(person1), que también llama al método __str__.

Qué Sucede Sin __str__

Para comprender la importancia del método __str__, veamos qué sucede cuando no lo definimos:

  1. Cree un nuevo archivo llamado without_str.py:

  2. Agregue el siguiente código:

    class SimpleClass:
        def __init__(self, value):
            self.value = value
    
    ## Create an object
    obj = SimpleClass(42)
    
    ## Print the object
    print(obj)
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 without_str.py

Debería ver una salida como:

<__main__.SimpleClass object at 0x7f2d8c3e9d90>

La salida no es muy informativa. Muestra el nombre de la clase y la dirección de memoria del objeto, pero no su contenido. Esta es la razón por la que implementar un método __str__ adecuado es importante para que sus objetos sean más fáciles de usar.

Ejercicio Práctico

Creemos una nueva clase llamada Book con un método __str__:

  1. Cree un nuevo archivo llamado book.py:

  2. Agregue el siguiente código:

    class Book:
        def __init__(self, title, author, pages):
            self.title = title
            self.author = author
            self.pages = pages
    
        def __str__(self):
            return f'"{self.title}" by {self.author} ({self.pages} pages)'
    
    ## Create book objects
    book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 180)
    book2 = Book("To Kill a Mockingbird", "Harper Lee", 281)
    
    ## Print the books
    print(book1)
    print(book2)
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 book.py

La salida debería ser:

"The Great Gatsby" by F. Scott Fitzgerald (180 pages)
"To Kill a Mockingbird" by Harper Lee (281 pages)

Ahora comprende cómo usar el método __str__ para crear representaciones de cadena legibles por humanos de sus objetos.

Implementando el Método __repr__

Además de __str__, Python proporciona otro método especial para la representación de cadenas: __repr__. Mientras que __str__ está destinado a proporcionar una representación legible por humanos, __repr__ tiene como objetivo proporcionar una representación inequívoca de un objeto que se puede usar para recrear el objeto si es posible.

Entendiendo el Método __repr__

El método __repr__ se llama cuando usa la función repr() en un objeto o cuando muestra un objeto en una sesión interactiva. Debe devolver una cadena que, cuando se pasa a eval(), crearía un objeto equivalente (cuando sea posible).

Actualicemos nuestra clase Book para incluir un método __repr__:

  1. Abra book.py en el editor.

  2. Actualice el código para incluir un método __repr__:

    class Book:
        def __init__(self, title, author, pages):
            self.title = title
            self.author = author
            self.pages = pages
    
        def __str__(self):
            return f'"{self.title}" by {self.author} ({self.pages} pages)'
    
        def __repr__(self):
            return f'Book("{self.title}", "{self.author}", {self.pages})'
    
    ## Create book objects
    book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 180)
    book2 = Book("To Kill a Mockingbird", "Harper Lee", 281)
    
    ## Print the books (uses __str__)
    print("String representation (using __str__):")
    print(book1)
    print(book2)
    
    ## Get the representation (uses __repr__)
    print("\nRepresentation (using __repr__):")
    print(repr(book1))
    print(repr(book2))
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 book.py

Debería ver una salida como:

String representation (using __str__):
"The Great Gatsby" by F. Scott Fitzgerald (180 pages)
"To Kill a Mockingbird" by Harper Lee (281 pages)

Representation (using __repr__):
Book("The Great Gatsby", "F. Scott Fitzgerald", 180)
Book("To Kill a Mockingbird", "Harper Lee", 281)

Diferencias entre __str__ y __repr__

Las principales diferencias entre __str__ y __repr__ son:

  • __str__ está destinado a la salida legible por humanos, mientras que __repr__ está destinado a desarrolladores y depuración.
  • Si __str__ no está definido pero __repr__ sí, Python usará __repr__ como respaldo para str() o print().
  • __repr__ idealmente debería devolver una cadena que pueda recrear el objeto cuando se pasa a eval(), aunque esto no siempre es posible o necesario.

La Función eval() con __repr__

Cuando se implementa correctamente, la cadena devuelta por __repr__ se puede usar con eval() para recrear el objeto. Veamos esto en acción:

  1. Cree un nuevo archivo llamado repr_eval.py:

  2. Agregue el siguiente código:

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __str__(self):
            return f"Point at ({self.x}, {self.y})"
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
    ## Create a point
    p1 = Point(3, 4)
    
    ## Get the repr string
    repr_str = repr(p1)
    print(f"Representation: {repr_str}")
    
    ## Use eval to recreate the object
    p2 = eval(repr_str)
    print(f"Recreated object: {p2}")
    
    ## Verify they have the same values
    print(f"p1.x = {p1.x}, p1.y = {p1.y}")
    print(f"p2.x = {p2.x}, p2.y = {p2.y}")
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 repr_eval.py

Debería ver una salida como:

Representation: Point(3, 4)
Recreated object: Point at (3, 4)
p1.x = 3, p1.y = 4
p2.x = 3, p2.y = 4

Esto demuestra que podemos recrear el objeto original usando la cadena devuelta por __repr__ y la función eval().

Cuándo Usar Cada Método

  • Use __init__ para configurar el estado inicial de sus objetos.
  • Use __str__ para proporcionar una representación legible por humanos para los usuarios finales.
  • Use __repr__ para proporcionar una representación precisa e inequívoca para desarrolladores y depuración.

Ejercicio Práctico: Un Ejemplo Completo

Pongamos todo junto creando una clase Rectangle con los tres métodos especiales:

  1. Cree un nuevo archivo llamado rectangle.py:

  2. Agregue el siguiente código:

    class Rectangle:
        def __init__(self, width, height):
            self.width = width
            self.height = height
    
        def area(self):
            return self.width * self.height
    
        def perimeter(self):
            return 2 * (self.width + self.height)
    
        def __str__(self):
            return f"Rectangle with width {self.width} and height {self.height}"
    
        def __repr__(self):
            return f"Rectangle({self.width}, {self.height})"
    
    ## Create rectangles
    rect1 = Rectangle(5, 10)
    rect2 = Rectangle(3, 7)
    
    ## Display information about the rectangles
    print(f"Rectangle 1: {rect1}")
    print(f"Area: {rect1.area()}")
    print(f"Perimeter: {rect1.perimeter()}")
    print(f"Representation: {repr(rect1)}")
    
    print("\nRectangle 2: {0}".format(rect2))
    print(f"Area: {rect2.area()}")
    print(f"Perimeter: {rect2.perimeter()}")
    print(f"Representation: {repr(rect2)}")
    
    ## Recreate a rectangle using eval
    rect3 = eval(repr(rect1))
    print(f"\nRecreated rectangle: {rect3}")
    print(f"Is it the same area? {rect3.area() == rect1.area()}")
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 rectangle.py

Debería ver una salida como:

Rectangle 1: Rectangle with width 5 and height 10
Area: 50
Perimeter: 30
Representation: Rectangle(5, 10)

Rectangle 2: Rectangle with width 3 and height 7
Area: 21
Perimeter: 20
Representation: Rectangle(3, 7)

Recreated rectangle: Rectangle with width 5 and height 10
Is it the same area? True

Este ejemplo demuestra cómo los tres métodos especiales (__init__, __str__ y __repr__) funcionan juntos para crear una clase bien diseñada.

Creando una Aplicación Práctica

Ahora que entendemos los tres métodos especiales __init__, __str__ y __repr__, creemos una aplicación más práctica que demuestre su uso en un escenario del mundo real. Crearemos un sistema bancario simple con una clase BankAccount.

Construyendo una Clase Bank Account

  1. Cree un nuevo archivo llamado bank_account.py:

  2. Agregue el siguiente código:

    class BankAccount:
        def __init__(self, account_number, owner_name, balance=0.0):
            self.account_number = account_number
            self.owner_name = owner_name
            self.balance = balance
            self.transactions = []
    
        def deposit(self, amount):
            if amount <= 0:
                print("Deposit amount must be positive")
                return False
    
            self.balance += amount
            self.transactions.append(f"Deposit: +${amount:.2f}")
            return True
    
        def withdraw(self, amount):
            if amount <= 0:
                print("Withdrawal amount must be positive")
                return False
    
            if amount > self.balance:
                print("Insufficient funds")
                return False
    
            self.balance -= amount
            self.transactions.append(f"Withdrawal: -${amount:.2f}")
            return True
    
        def get_transaction_history(self):
            return self.transactions
    
        def __str__(self):
            return f"Account {self.account_number} | Owner: {self.owner_name} | Balance: ${self.balance:.2f}"
    
        def __repr__(self):
            return f'BankAccount("{self.account_number}", "{self.owner_name}", {self.balance})'
  3. Guarde el archivo.

Probando la Clase Bank Account

Ahora probemos nuestra clase BankAccount:

  1. Cree un nuevo archivo llamado bank_test.py:

  2. Agregue el siguiente código:

    from bank_account import BankAccount
    
    ## Create bank accounts
    account1 = BankAccount("12345", "John Doe", 1000.0)
    account2 = BankAccount("67890", "Jane Smith", 500.0)
    
    ## Display initial account information
    print("Initial Account Status:")
    print(account1)
    print(account2)
    
    ## Perform transactions
    print("\nPerforming transactions...")
    
    ## Deposit to account1
    print("\nDepositing $250 to account1:")
    account1.deposit(250)
    print(account1)
    
    ## Withdraw from account2
    print("\nWithdrawing $100 from account2:")
    account2.withdraw(100)
    print(account2)
    
    ## Try to withdraw too much from account2
    print("\nTrying to withdraw $1000 from account2:")
    account2.withdraw(1000)
    print(account2)
    
    ## Try to deposit a negative amount to account1
    print("\nTrying to deposit -$50 to account1:")
    account1.deposit(-50)
    print(account1)
    
    ## Display transaction history
    print("\nTransaction history for account1:")
    for transaction in account1.get_transaction_history():
        print(f"- {transaction}")
    
    print("\nTransaction history for account2:")
    for transaction in account2.get_transaction_history():
        print(f"- {transaction}")
    
    ## Recreate an account using repr
    print("\nRecreating account1 using repr:")
    account1_repr = repr(account1)
    print(f"Representation: {account1_repr}")
    
    recreated_account = eval(account1_repr)
    print(f"Recreated account: {recreated_account}")
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 bank_test.py

Debería ver una salida similar a esta:

Initial Account Status:
Account 12345 | Owner: John Doe | Balance: $1000.00
Account 67890 | Owner: Jane Smith | Balance: $500.00

Performing transactions...

Depositing $250 to account1:
Account 12345 | Owner: John Doe | Balance: $1250.00

Withdrawing $100 from account2:
Account 67890 | Owner: Jane Smith | Balance: $400.00

Trying to withdraw $1000 from account2:
Insufficient funds
Account 67890 | Owner: Jane Smith | Balance: $400.00

Trying to deposit -$50 to account1:
Deposit amount must be positive
Account 12345 | Owner: John Doe | Balance: $1250.00

Transaction history for account1:
- Deposit: +$250.00

Transaction history for account2:
- Withdrawal: -$100.00

Recreating account1 using repr:
Representation: BankAccount("12345", "John Doe", 1250.0)
Recreated account: Account 12345 | Owner: John Doe | Balance: $1250.00

Entendiendo la Implementación

En esta aplicación bancaria:

  1. El método __init__ inicializa una cuenta bancaria con un número de cuenta, el nombre del propietario y un saldo inicial opcional. También crea una lista vacía para rastrear las transacciones.

  2. Los métodos deposit y withdraw manejan las transacciones y actualizan el saldo y el historial de transacciones.

  3. El método __str__ proporciona una representación fácil de usar de la cuenta, mostrando el número de cuenta, el nombre del propietario y el saldo actual.

  4. El método __repr__ proporciona una cadena que puede recrear el objeto de la cuenta (aunque tenga en cuenta que el historial de transacciones no se conserva en esta implementación).

Este ejemplo demuestra cómo estos métodos especiales se pueden usar en una aplicación práctica para crear objetos más intuitivos y fáciles de usar.

Extensión de la Aplicación

Como ejercicio final, creemos un sistema bancario simple que gestione múltiples cuentas:

  1. Cree un nuevo archivo llamado banking_system.py:

  2. Agregue el siguiente código:

    from bank_account import BankAccount
    
    class BankingSystem:
        def __init__(self, bank_name):
            self.bank_name = bank_name
            self.accounts = {}
    
        def create_account(self, account_number, owner_name, initial_balance=0.0):
            if account_number in self.accounts:
                print(f"Account {account_number} already exists")
                return None
    
            account = BankAccount(account_number, owner_name, initial_balance)
            self.accounts[account_number] = account
            return account
    
        def get_account(self, account_number):
            return self.accounts.get(account_number)
    
        def list_accounts(self):
            return list(self.accounts.values())
    
        def __str__(self):
            return f"{self.bank_name} - Managing {len(self.accounts)} accounts"
    
        def __repr__(self):
            return f'BankingSystem("{self.bank_name}")'
    
    ## Create a banking system
    bank = BankingSystem("Python First Bank")
    print(bank)
    
    ## Create some accounts
    bank.create_account("A001", "John Doe", 1000)
    bank.create_account("A002", "Jane Smith", 500)
    bank.create_account("A003", "Bob Johnson", 250)
    
    ## List all accounts
    print("\nAll accounts:")
    for account in bank.list_accounts():
        print(account)
    
    ## Make some transactions
    account = bank.get_account("A001")
    if account:
        print(f"\nBefore deposit: {account}")
        account.deposit(500)
        print(f"After deposit: {account}")
    
    account = bank.get_account("A002")
    if account:
        print(f"\nBefore withdrawal: {account}")
        account.withdraw(200)
        print(f"After withdrawal: {account}")
    
    ## Try to create an existing account
    print("\nTrying to create an existing account:")
    bank.create_account("A001", "Someone Else", 300)
    
    ## Final state of the banking system
    print(f"\n{bank}")
  3. Guarde el archivo.

  4. Ejecute el script:

    python3 banking_system.py

Debería ver una salida similar a:

Python First Bank - Managing 0 accounts

All accounts:
Account A001 | Owner: John Doe | Balance: $1000.00
Account A002 | Owner: Jane Smith | Balance: $500.00
Account A003 | Owner: Bob Johnson | Balance: $250.00

Before deposit: Account A001 | Owner: John Doe | Balance: $1000.00
After deposit: Account A001 | Owner: John Doe | Balance: $1500.00

Before withdrawal: Account A002 | Owner: Jane Smith | Balance: $500.00
After withdrawal: Account A002 | Owner: Jane Smith | Balance: $300.00

Trying to create an existing account:
Account A001 already exists

Python First Bank - Managing 3 accounts

Este ejemplo demuestra cómo usar la clase BankAccount que creamos anteriormente en una aplicación más completa. Muestra cómo los métodos especiales __init__, __str__ y __repr__ proporcionan una base sólida para crear clases intuitivas y fáciles de usar.

Resumen

En este laboratorio, aprendió sobre tres métodos especiales esenciales en Python que le ayudan a crear objetos más expresivos y fáciles de usar:

  1. __init__: El método constructor que inicializa los atributos de un objeto cuando se crea.

  2. __str__: Un método que proporciona una representación de cadena legible por humanos de un objeto, principalmente para usuarios finales.

  3. __repr__: Un método que proporciona una representación de cadena inequívoca de un objeto, principalmente para desarrolladores y depuración.

Implementó estos métodos en varios ejemplos, desde clases simples como Person y Book hasta aplicaciones más complejas como un sistema bancario. También aprendió las diferencias entre __str__ y __repr__ y cuándo usar cada uno.

Al dominar estos métodos especiales, puede crear objetos Python más intuitivos y personalizados que se integran a la perfección con las funciones integradas del lenguaje. Este conocimiento forma una parte esencial de la programación orientada a objetos en Python y le ayudará a escribir código más limpio y fácil de mantener.

A medida que continúa su viaje en Python, recuerde que hay muchos otros métodos especiales disponibles que le permiten personalizar aún más el comportamiento de sus objetos. Explorar estos métodos le dará aún más control sobre cómo se comportan sus objetos en diferentes contextos.