Redefinindo Métodos Especiais

Intermediate

This tutorial is from open-source community. Access the source code

Introdução

Neste laboratório, você aprenderá como personalizar o comportamento de objetos redefinindo métodos especiais. Você também mudará a maneira como objetos definidos pelo usuário são impressos e tornará os objetos comparáveis.

Adicionalmente, você aprenderá a criar um gerenciador de contexto (context manager). O arquivo a ser modificado neste laboratório é stock.py.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível intermediário com uma taxa de conclusão de 74%. Recebeu uma taxa de avaliações positivas de 92% dos estudantes.

Melhorando a Representação de Objetos com __repr__

Em Python, os objetos podem ser representados como strings de duas maneiras diferentes. Essas representações servem a propósitos diferentes e são úteis em vários cenários.

O primeiro tipo é a representação de string. Esta é criada pela função str(), que é chamada automaticamente quando você usa a função print(). A representação de string é projetada para ser legível por humanos. Ela apresenta o objeto em um formato que é fácil para nós entendermos e interpretarmos.

O segundo tipo é a representação de código. Esta é gerada pela função repr(). A representação de código mostra o código que você precisaria escrever para recriar o objeto. É mais sobre fornecer uma maneira precisa e inequívoca de representar o objeto em código.

Vamos olhar para um exemplo concreto usando a classe date embutida do Python. Isso ajudará você a ver a diferença entre as representações de string e código em ação.

>>> from datetime import date
>>> d = date(2008, 7, 5)
>>> print(d)              ## Uses str()
2008-07-05
>>> d                     ## Uses repr()
datetime.date(2008, 7, 5)

Neste exemplo, quando usamos print(d), o Python chama a função str() no objeto date d, e obtemos uma data legível em formato AAAA-MM-DD. Quando simplesmente digitamos d no shell interativo, o Python chama a função repr(), e vemos o código necessário para recriar o objeto date.

Você pode obter explicitamente a string repr() de várias maneiras. Aqui estão alguns exemplos:

>>> print('The date is', repr(d))
The date is datetime.date(2008, 7, 5)
>>> print(f'The date is {d!r}')
The date is datetime.date(2008, 7, 5)
>>> print('The date is %r' % d)
The date is datetime.date(2008, 7, 5)

Agora, vamos aplicar esse conceito à nossa classe Stock. Vamos melhorar a classe implementando o método __repr__. Este método especial é chamado pelo Python quando ele precisa da representação de código de um objeto.

Para fazer isso, abra o arquivo stock.py em seu editor. Em seguida, adicione o método __repr__ à classe Stock. O método __repr__ deve retornar uma string que mostra o código necessário para recriar o objeto Stock.

def __repr__(self):
    return f"Stock('{self.name}', {self.shares}, {self.price})"

Depois de adicionar o método __repr__, sua classe Stock completa agora deve se parecer com isto:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

Agora, vamos testar nossa classe Stock modificada. Abra um shell interativo do Python executando o seguinte comando em seu terminal:

python3

Depois que o shell interativo estiver aberto, tente os seguintes comandos:

>>> import stock
>>> goog = stock.Stock('GOOG', 100, 490.10)
>>> goog
Stock('GOOG', 100, 490.1)

Você também pode ver como o método __repr__ funciona com um portfólio de ações. Aqui está um exemplo:

>>> import reader
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> portfolio
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44), Stock('MSFT', 200, 51.23), Stock('GE', 95, 40.37), Stock('MSFT', 50, 65.1), Stock('IBM', 100, 70.44)]

Como você pode ver, o método __repr__ tornou nossos objetos Stock muito mais informativos quando exibidos no shell interativo ou no depurador. Agora ele mostra o código necessário para recriar cada objeto, o que é muito útil para depurar e entender o estado dos objetos.

Quando terminar de testar, você pode sair do interpretador Python executando o seguinte comando:

>>> exit()

Tornando Objetos Comparáveis com __eq__

Em Python, quando você usa o operador == para comparar dois objetos, o Python na verdade chama o método especial __eq__. Por padrão, este método compara as identidades dos objetos, o que significa que ele verifica se eles estão armazenados no mesmo endereço de memória, em vez de comparar seus conteúdos.

Vamos dar uma olhada em um exemplo. Suponha que tenhamos uma classe Stock e criemos dois objetos Stock com os mesmos valores. Então, tentamos compará-los usando o operador ==. Veja como você pode fazer isso no interpretador Python:

Primeiro, inicie o interpretador Python executando o seguinte comando em seu terminal:

python3

Em seguida, no interpretador Python, execute o seguinte código:

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
False

Como você pode ver, embora os dois objetos Stock a e b tenham os mesmos valores para seus atributos (name, shares e price), o Python os considera objetos diferentes porque eles estão armazenados em locais de memória diferentes.

Para corrigir esse problema, podemos implementar o método __eq__ em nossa classe Stock. Este método será chamado toda vez que o operador == for usado em objetos da classe Stock.

Agora, abra o arquivo stock.py novamente. Dentro da classe Stock, adicione o seguinte método __eq__:

def __eq__(self, other):
    return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                         (other.name, other.shares, other.price))

Vamos detalhar o que este método faz:

  1. Primeiro, ele usa a função isinstance para verificar se o objeto other é uma instância da classe Stock. Isso é importante porque só queremos comparar objetos Stock com outros objetos Stock.
  2. Se other for um objeto Stock, ele então compara os atributos (name, shares e price) tanto do objeto self quanto do objeto other.
  3. Ele retorna True somente se ambos os objetos forem instâncias Stock e seus atributos forem idênticos.

Depois de adicionar o método __eq__, sua classe Stock completa deve se parecer com isto:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

    def __eq__(self, other):
        return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                             (other.name, other.shares, other.price))

Agora, vamos testar nossa classe Stock aprimorada. Inicie o interpretador Python novamente:

python3

Em seguida, execute o seguinte código no interpretador Python:

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
True
>>> c = stock.Stock('GOOG', 200, 490.1)
>>> a == c
False

Ótimo! Agora nossos objetos Stock podem ser comparados corretamente com base em seu conteúdo, em vez de seus endereços de memória.

A verificação isinstance no método __eq__ é crucial. Ela garante que estamos apenas comparando objetos Stock. Se não tivéssemos essa verificação, comparar um objeto Stock com algo que não é um objeto Stock poderia gerar erros.

Quando terminar de testar, você pode sair do interpretador Python executando o seguinte comando:

>>> exit()

Criando um Gerenciador de Contexto

Um gerenciador de contexto é um tipo especial de objeto em Python. Em Python, os objetos podem ter diferentes métodos que definem seu comportamento. Um gerenciador de contexto define especificamente dois métodos importantes: __enter__ e __exit__. Esses métodos trabalham juntos com a instrução with. A instrução with é usada para configurar um contexto específico para um bloco de código. Pense nisso como a criação de um pequeno ambiente onde certas coisas acontecem e, quando o bloco de código é finalizado, o gerenciador de contexto cuida da limpeza.

Nesta etapa, vamos criar um gerenciador de contexto que tem uma função muito útil. Ele redirecionará temporariamente a saída padrão (sys.stdout). A saída padrão é onde a saída normal do seu programa Python vai, geralmente o console. Ao redirecioná-la, podemos enviar a saída para um arquivo em vez disso. Isso é útil quando você deseja salvar a saída que, de outra forma, seria exibida apenas no console.

Primeiro, precisamos criar um novo arquivo para escrever nosso código do gerenciador de contexto. Vamos nomear este arquivo redirect.py. Você pode criá-lo usando o seguinte comando no terminal:

touch /home/labex/project/redirect.py

Agora que o arquivo foi criado, abra-o em um editor. Depois de aberto, adicione o seguinte código Python ao arquivo:

import sys

class redirect_stdout:
    def __init__(self, out_file):
        self.out_file = out_file

    def __enter__(self):
        self.stdout = sys.stdout
        sys.stdout = self.out_file
        return self.out_file

    def __exit__(self, ty, val, tb):
        sys.stdout = self.stdout

Vamos detalhar o que este gerenciador de contexto faz:

  1. __init__: Este é o método de inicialização. Quando criamos uma instância da classe redirect_stdout, passamos um objeto de arquivo. Este método armazena esse objeto de arquivo na variável de instância self.out_file. Portanto, ele lembra para onde queremos redirecionar a saída.
  2. __enter__:
    • Primeiro, ele salva o sys.stdout atual. Isso é importante porque precisamos restaurá-lo mais tarde.
    • Em seguida, ele substitui o sys.stdout atual pelo nosso objeto de arquivo. A partir deste ponto, qualquer saída que normalmente iria para o console irá para o arquivo.
    • Finalmente, ele retorna o objeto de arquivo. Isso é útil porque podemos querer usar o objeto de arquivo dentro do bloco with.
  3. __exit__:
    • Este método restaura o sys.stdout original. Portanto, após o bloco with ser finalizado, a saída voltará para o console como normal.
    • Ele recebe três parâmetros: tipo de exceção (ty), valor da exceção (val) e traceback (tb). Esses parâmetros são exigidos pelo protocolo do gerenciador de contexto. Eles são usados para lidar com quaisquer exceções que possam ocorrer dentro do bloco with.

Agora, vamos testar nosso gerenciador de contexto. Vamos usá-lo para redirecionar a saída de uma tabela para um arquivo. Primeiro, inicie o interpretador Python:

python3

Em seguida, execute o seguinte código Python no interpretador:

>>> import stock, reader, tableformat
>>> from redirect import redirect_stdout
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> formatter = tableformat.create_formatter('text')
>>> with redirect_stdout(open('out.txt', 'w')) as file:
...     tableformat.print_table(portfolio, ['name','shares','price'], formatter)
...     file.close()
...
>>> ## Let's check the content of the output file
>>> print(open('out.txt').read())
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

Ótimo! Nosso gerenciador de contexto funcionou como esperado. Ele redirecionou com sucesso a saída da tabela para o arquivo out.txt.

Os gerenciadores de contexto são um recurso muito poderoso em Python. Eles ajudam você a gerenciar recursos adequadamente. Aqui estão alguns casos de uso comuns para gerenciadores de contexto:

  • Operações de arquivo: Quando você abre um arquivo, um gerenciador de contexto pode garantir que o arquivo seja fechado corretamente, mesmo que ocorra um erro.
  • Conexões de banco de dados: Ele pode garantir que a conexão do banco de dados seja fechada depois que você terminar de usá-la.
  • Locks em programas encadeados: Os gerenciadores de contexto podem lidar com o bloqueio e desbloqueio de recursos de forma segura.
  • Alterar temporariamente as configurações do ambiente: Você pode alterar algumas configurações para um bloco de código e, em seguida, restaurá-las automaticamente.

Este padrão é muito importante porque garante que os recursos sejam limpos adequadamente, mesmo que ocorra uma exceção dentro do bloco with.

Quando terminar de testar, você pode sair do interpretador Python:

>>> exit()

Resumo

Neste laboratório, você aprendeu como personalizar a representação de string de objetos usando o método __repr__, tornar objetos comparáveis com o método __eq__ e criar um gerenciador de contexto usando os métodos __enter__ e __exit__. Esses "métodos dunder" especiais são a pedra angular dos recursos orientados a objetos do Python.

Implementar esses métodos em suas classes permite que seus objetos se comportem como tipos embutidos e se integrem perfeitamente aos recursos da linguagem Python. Métodos especiais permitem várias funcionalidades, como representações de string personalizadas, comparação de objetos e gerenciamento de contexto. À medida que você avança em Python, descobrirá mais métodos especiais para aproveitar seu poderoso modelo de objetos.