Como usar os métodos __init__, __str__ e __repr__ em Python

PythonBeginner
Pratique Agora

Introdução

As funcionalidades de programação orientada a objetos (POO) do Python oferecem ferramentas poderosas para criar objetos expressivos e personalizados. Dentre estas, os métodos especiais __init__, __str__ e __repr__ desempenham papéis cruciais na definição do comportamento e da representação de seus objetos Python.

Neste tutorial, exploraremos esses métodos especiais, entenderemos seus propósitos e aprenderemos como implementá-los efetivamente em suas classes Python. Ao final deste laboratório, você será capaz de criar objetos Python mais intuitivos e fáceis de usar.

Criando Classes com o Método __init__

O primeiro método especial que exploraremos é o __init__, que é chamado quando um objeto é criado. Este método permite que você inicialize os atributos do seu objeto.

Entendendo o Método __init__

O método __init__, também conhecido como construtor (constructor), é chamado automaticamente quando você cria uma nova instância de uma classe. Seu objetivo principal é configurar o estado inicial do objeto, atribuindo valores aos atributos.

Vamos começar criando uma classe Python simples com o método __init__:

  1. Abra o terminal no seu ambiente LabEx.

  2. Navegue até o diretório do projeto:

    cd ~/project
  3. Crie um novo arquivo Python chamado person.py usando o editor de código:

  4. No editor, adicione o seguinte 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. Salve o arquivo.

  6. Execute o script Python:

    python3 person.py

Você deve ver a seguinte saída:

Name: Alice
Age: 30

Explicação do Método __init__

No código acima:

  • Definimos uma classe Person com um método __init__ que recebe três parâmetros: self, name e age.
  • O parâmetro self se refere à instância que está sendo criada e é passado automaticamente quando um objeto é criado.
  • Dentro do método __init__, atribuímos os valores de name e age aos atributos do objeto usando self.name e self.age.
  • Quando criamos um novo objeto Person com person1 = Person("Alice", 30), o método __init__ é chamado automaticamente.
  • Podemos então acessar os atributos usando a notação de ponto: person1.name e person1.age.

Adicionando Mais Funcionalidade

Vamos aprimorar nossa classe Person adicionando um método para calcular o ano de nascimento com base no ano atual e na idade da pessoa:

  1. Abra person.py no editor.

  2. Atualize o código para incluir um método para calcular o ano de nascimento:

    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. Salve o arquivo.

  4. Execute o script Python:

    python3 person.py

A saída agora deve incluir o ano de nascimento calculado:

Name: Alice
Age: 30
Birth Year: 1993

(O ano de nascimento real dependerá do ano atual quando você executar o script)

Agora você criou uma classe Python com o método __init__ para inicializar os atributos do objeto e adicionou um método para realizar cálculos com base nesses atributos.

Implementando o Método __str__

Agora que entendemos como criar uma classe com o método __init__, vamos explorar outro método especial chamado __str__. Este método nos permite definir como um objeto deve ser representado como uma string.

Entendendo o Método __str__

O método __str__ é chamado quando você usa a função str() em um objeto ou quando você imprime um objeto usando a função print(). Ele deve retornar uma representação de string legível por humanos do objeto.

Vamos atualizar nossa classe Person para incluir um método __str__:

  1. Abra person.py no editor.

  2. Atualize o código para incluir um 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. Salve o arquivo.

  4. Execute o script Python:

    python3 person.py

Você deve ver uma saída semelhante 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

Como __str__ Funciona

No código acima:

  • Definimos um método __str__ que retorna uma string formatada com o nome e a idade da pessoa.
  • Quando chamamos print(person1), o Python chama automaticamente o método __str__ para determinar o que exibir.
  • Também podemos converter explicitamente um objeto em uma string usando str(person1), que também chama o método __str__.

O Que Acontece Sem __str__

Para entender a importância do método __str__, vamos ver o que acontece quando não o definimos:

  1. Crie um novo arquivo chamado without_str.py:

  2. Adicione o seguinte código:

    class SimpleClass:
        def __init__(self, value):
            self.value = value
    
    ## Create an object
    obj = SimpleClass(42)
    
    ## Print the object
    print(obj)
  3. Salve o arquivo.

  4. Execute o script:

    python3 without_str.py

Você deve ver uma saída como:

<__main__.SimpleClass object at 0x7f2d8c3e9d90>

A saída não é muito informativa. Ela mostra o nome da classe e o endereço de memória do objeto, mas não seu conteúdo. É por isso que implementar um método __str__ adequado é importante para tornar seus objetos mais fáceis de usar.

Exercício Prático

Vamos criar uma nova classe chamada Book com um método __str__:

  1. Crie um novo arquivo chamado book.py:

  2. Adicione o seguinte 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. Salve o arquivo.

  4. Execute o script:

    python3 book.py

A saída deve ser:

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

Agora você entende como usar o método __str__ para criar representações de string legíveis por humanos de seus objetos.

Implementando o Método __repr__

Além de __str__, o Python fornece outro método especial para representação de string: __repr__. Enquanto __str__ tem como objetivo fornecer uma representação legível por humanos, __repr__ se destina a fornecer uma representação inequívoca de um objeto que pode ser usada para recriar o objeto, se possível.

Entendendo o Método __repr__

O método __repr__ é chamado quando você usa a função repr() em um objeto ou quando você exibe um objeto em uma sessão interativa. Ele deve retornar uma string que, quando passada para eval(), criaria um objeto equivalente (quando possível).

Vamos atualizar nossa classe Book para incluir um método __repr__:

  1. Abra book.py no editor.

  2. Atualize o código para incluir um 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. Salve o arquivo.

  4. Execute o script:

    python3 book.py

Você deve ver uma saída 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)

Diferenças Entre __str__ e __repr__

As principais diferenças entre __str__ e __repr__ são:

  • __str__ é destinado à saída legível por humanos, enquanto __repr__ é destinado a desenvolvedores e depuração.
  • Se __str__ não for definido, mas __repr__ for, o Python usará __repr__ como um fallback para str() ou print().
  • __repr__ deve, idealmente, retornar uma string que possa recriar o objeto quando passada para eval(), embora isso nem sempre seja possível ou necessário.

A Função eval() com __repr__

Quando implementado corretamente, a string retornada por __repr__ pode ser usada com eval() para recriar o objeto. Vamos ver isso em ação:

  1. Crie um novo arquivo chamado repr_eval.py:

  2. Adicione o seguinte 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. Salve o arquivo.

  4. Execute o script:

    python3 repr_eval.py

Você deve ver uma saída como:

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

Isso demonstra que podemos recriar o objeto original usando a string retornada por __repr__ e a função eval().

Quando Usar Cada Método

  • Use __init__ para configurar o estado inicial de seus objetos.
  • Use __str__ para fornecer uma representação legível por humanos para usuários finais.
  • Use __repr__ para fornecer uma representação precisa e inequívoca para desenvolvedores e depuração.

Exercício Prático: Um Exemplo Completo

Vamos juntar tudo criando uma classe Rectangle com todos os três métodos especiais:

  1. Crie um novo arquivo chamado rectangle.py:

  2. Adicione o seguinte 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. Salve o arquivo.

  4. Execute o script:

    python3 rectangle.py

Você deve ver uma saída 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 exemplo demonstra como todos os três métodos especiais (__init__, __str__ e __repr__) trabalham juntos para criar uma classe bem projetada.

Criando uma Aplicação Prática

Agora que entendemos os três métodos especiais __init__, __str__ e __repr__, vamos criar uma aplicação mais prática que demonstra seu uso em um cenário do mundo real. Criaremos um sistema bancário simples com uma classe BankAccount.

Construindo uma Classe Bank Account

  1. Crie um novo arquivo chamado bank_account.py:

  2. Adicione o seguinte 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. Salve o arquivo.

Testando a Classe Bank Account

Agora vamos testar nossa classe BankAccount:

  1. Crie um novo arquivo chamado bank_test.py:

  2. Adicione o seguinte 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. Salve o arquivo.

  4. Execute o script:

    python3 bank_test.py

Você deve ver uma saída semelhante 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

Entendendo a Implementação

Nesta aplicação bancária:

  1. O método __init__ inicializa uma conta bancária com um número de conta, nome do proprietário e um saldo inicial opcional. Ele também cria uma lista vazia para rastrear as transações.

  2. Os métodos deposit e withdraw lidam com as transações e atualizam o saldo e o histórico de transações.

  3. O método __str__ fornece uma representação amigável da conta, mostrando o número da conta, o nome do proprietário e o saldo atual.

  4. O método __repr__ fornece uma string que pode recriar o objeto da conta (embora observe que o histórico de transações não é preservado nesta implementação).

Este exemplo demonstra como esses métodos especiais podem ser usados em uma aplicação prática para criar objetos mais intuitivos e fáceis de usar.

Estendendo a Aplicação

Como um exercício final, vamos criar um sistema bancário simples que gerencia várias contas:

  1. Crie um novo arquivo chamado banking_system.py:

  2. Adicione o seguinte 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. Salve o arquivo.

  4. Execute o script:

    python3 banking_system.py

Você deve ver uma saída semelhante 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 exemplo demonstra como usar a classe BankAccount que criamos anteriormente em uma aplicação mais completa. Ele mostra como os métodos especiais __init__, __str__ e __repr__ fornecem uma base sólida para criar classes intuitivas e fáceis de usar.

Resumo

Neste laboratório, você aprendeu sobre três métodos especiais essenciais em Python que ajudam a criar objetos mais expressivos e fáceis de usar:

  1. __init__: O método construtor que inicializa os atributos de um objeto quando ele é criado.

  2. __str__: Um método que fornece uma representação de string legível por humanos de um objeto, principalmente para usuários finais.

  3. __repr__: Um método que fornece uma representação de string inequívoca de um objeto, principalmente para desenvolvedores e depuração.

Você implementou esses métodos em vários exemplos, desde classes simples como Person e Book até aplicações mais complexas como um sistema bancário. Você também aprendeu as diferenças entre __str__ e __repr__ e quando usar cada um.

Ao dominar esses métodos especiais, você pode criar objetos Python mais intuitivos e personalizados que se integram perfeitamente com os recursos embutidos da linguagem. Esse conhecimento forma uma parte essencial da programação orientada a objetos em Python e o ajudará a escrever um código mais limpo e de fácil manutenção.

Ao continuar sua jornada em Python, lembre-se de que existem muitos outros métodos especiais disponíveis que permitem personalizar ainda mais o comportamento de seus objetos. Explorar esses métodos lhe dará ainda mais controle sobre como seus objetos se comportam em diferentes contextos.