Explore Métodos Especiais em Classes Python

PythonPythonBeginner
Pratique Agora

Introdução

Neste laboratório, você explorará alguns dos métodos especiais do Python, frequentemente chamados de métodos "dunder" devido aos seus nomes com duplo sublinhado (__). Você obterá uma compreensão prática de como esses métodos permitem personalizar o comportamento de suas classes e objetos.

Você aprenderá sobre o método __new__ para controlar a criação de instâncias e o método __del__ para a destruição de objetos. Você também verá como usar __slots__ para otimizar o uso de memória e restringir atributos, e como tornar as instâncias de sua classe invocáveis como funções com o método __call__. Através de exemplos práticos, você aprenderá a escrever um código Python mais eficiente e expressivo.

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 iniciante com uma taxa de conclusão de 100%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Entender e Usar o Método __new__

Nesta etapa, você explorará o método __new__. Enquanto __init__ é comumente usado para inicializar os atributos de um objeto após ele ter sido criado, __new__ é o método que, na verdade, cria a instância em primeiro lugar. Ele é chamado antes de __init__.

Aqui estão as principais diferenças:

  • __new__ é um método estático que recebe a classe (cls) como seu primeiro argumento. Ele é responsável por criar e retornar uma nova instância da classe.
  • __init__ é um método de instância que recebe a instância (self) como seu primeiro argumento. Ele inicializa o objeto recém-criado e não retorna nada.

Geralmente, você não precisa sobrescrever __new__, pois a implementação padrão da classe object é suficiente. No entanto, ele é útil para casos avançados, como a implementação do padrão Singleton ou a criação de instâncias de tipos imutáveis.

Vamos ver o __new__ em ação. Você criará uma classe Dog que imprime uma mensagem durante a criação da instância.

Primeiro, abra o arquivo dog_cat.py no explorador de arquivos no lado esquerdo do IDE.

Adicione o seguinte código ao arquivo dog_cat.py. Este código define uma classe base Animal e uma subclasse Dog que sobrescreve o método __new__.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name
        print(f'Initializing {self._name} in Animal.')

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    ## O primeiro parâmetro é cls, que se refere à classe em si.
    ## Ele também deve aceitar quaisquer argumentos passados para o construtor.
    def __new__(cls, name, age):
        print('A new Dog instance is being created.')
        ## Chama o método __new__ da classe pai para criar a instância.
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print(f'Initializing {name} in Dog.')
        ## O __init__ da classe pai é chamado para definir o nome.
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

## Cria uma instância de Dog
print("Creating a Dog object...")
d = Dog('Buddy', 5)
print("Dog object created.")
print(f"Dog's name: {d._name}, Age: {d.age}")

Salve o arquivo (Ctrl+S ou Cmd+S).

Agora, abra um terminal no seu IDE (você pode usar o menu Terminal > New Terminal). Execute o script para observar a ordem das chamadas de método.

python ~/project/dog_cat.py

Você verá a seguinte saída. Observe que __new__ é chamado primeiro para criar a instância, seguido pelos métodos __init__ para inicializá-la.

Creating a Dog object...
A new Dog instance is being created.
Initializing Buddy in Dog.
Initializing Buddy in Animal.
Dog object created.
Dog's name: Buddy, Age: 5

Isso demonstra que __new__ controla a criação do objeto, e __init__ o configura posteriormente.

Implementar e Testar o Método __del__

Nesta etapa, você aprenderá sobre o método __del__. Este método é chamado de finalizador ou destrutor. Ele é invocado quando a contagem de referências de um objeto cai para zero, o que significa que ele está prestes a ser destruído pelo coletor de lixo (garbage collector) do Python. É frequentemente usado para tarefas de limpeza, como fechar conexões de rede ou handles de arquivos.

Você pode remover uma referência a um objeto usando a instrução del. Quando a última referência desaparece, __del__ é chamado automaticamente.

Vamos adicionar um método __del__ à nossa classe Dog para ver quando um objeto é destruído.

Abra o arquivo dog_cat.py novamente. Substitua todo o conteúdo do arquivo pelo código a seguir. Esta versão remove o código que cria uma instância de Dog (para evitar criá-la quando o módulo é importado) e adiciona o método __del__ à classe Dog.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    def __new__(cls, name, age):
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

    ## Adicione este método à classe Dog
    def __del__(self):
        print(f'The Dog object {self._name} is being deleted.')

Salve o arquivo dog_cat.py.

Agora, vamos criar um script separado para testar esse comportamento. Abra o arquivo test_del.py no explorador de arquivos.

Adicione o seguinte código ao test_del.py. Este script criará duas instâncias de Dog e depois excluirá explicitamente uma delas.

## File Name: test_del.py

from dog_cat import Dog
import time

print("Creating two Dog objects: d1 and d2.")
d1 = Dog('Tom', 3)
d2 = Dog('John', 5)

print("\nDeleting reference to d1...")
del d1
print("Reference to d1 deleted.")

## O coletor de lixo pode não ser executado imediatamente.
## Adicionamos um pequeno atraso para dar tempo para ele rodar.
time.sleep(1)

print("\nScript is about to end. d2 will be deleted automatically.")

Salve o arquivo. Agora, execute o script test_del.py no terminal.

python ~/project/test_del.py

Observe a saída. A mensagem __del__ para Tom aparece após del d1 ser chamado. A mensagem para John aparece no final, pois o objeto d2 é coletado como lixo quando o script termina.

Creating two Dog objects: d1 and d2.

Deleting reference to d1...
The Dog object Tom is being deleted.
Reference to d1 deleted.

Script is about to end. d2 will be deleted automatically.
The Dog object John is being deleted.

Nota: O tempo exato da coleta de lixo pode variar. __del__ é chamado quando o objeto é coletado, não necessariamente imediatamente após o uso de del.

Controlar Atributos com __slots__

Nesta etapa, você aprenderá sobre __slots__. Por padrão, o Python armazena os atributos de instância em um dicionário especial chamado __dict__. Isso permite que você adicione novos atributos a um objeto a qualquer momento. No entanto, essa flexibilidade consome memória extra.

Ao definir um atributo __slots__ em sua classe, você pode especificar uma lista fixa de atributos que as instâncias podem ter. Isso tem dois efeitos principais:

  1. Economia de Memória: O Python usa uma estrutura interna mais compacta em vez de um __dict__ para cada instância, o que pode reduzir significativamente o uso de memória, especialmente ao criar muitos objetos.
  2. Restrição de Atributos: Você não pode mais adicionar atributos a uma instância que não estejam listados em __slots__. Isso ajuda a prevenir erros de digitação e a impor uma estrutura de objeto estrita.

Vamos criar um exemplo para ver como __slots__ funciona. Abra o arquivo slots_example.py no explorador de arquivos.

Adicione o seguinte código ao slots_example.py:

## File Name: slots_example.py

class Player:
    ## Define os atributos permitidos usando __slots__
    __slots__ = ('name', 'level')

    def __init__(self, name, level):
        self.name = name
        self.level = level

## Cria uma instância de Player
p1 = Player('Hero', 10)

## Acessa os atributos permitidos
print(f"Player name: {p1.name}")
print(f"Player level: {p1.level}")

## Agora, tenta adicionar um novo atributo que NÃO está em __slots__
print("\nTrying to add a 'score' attribute...")
try:
    p1.score = 100
    print(f"Player score: {p1.score}")
except AttributeError as e:
    print(f"Caught an error: {e}")

## Também, verifica se a instância possui um atributo __dict__
print("\nChecking for __dict__...")
try:
    print(p1.__dict__)
except AttributeError as e:
    print(f"Caught an error: {e}")

Salve o arquivo. Agora, execute o script slots_example.py no terminal.

python ~/project/slots_example.py

A saída mostra que você pode atribuir valores a name e level, mas tentar atribuir um valor a score gera um AttributeError. Também confirma que a instância não possui um __dict__.

Player name: Hero
Player level: 10

Trying to add a 'score' attribute...
Caught an error: 'Player' object has no attribute 'score'

Checking for __dict__...
Caught an error: 'Player' object has no attribute '__dict__'

Isso demonstra como __slots__ pode impor um conjunto fixo de atributos e otimizar a memória ao eliminar o dicionário de instância.

Tornar Instâncias Chamáveis com __call__

Nesta etapa, você explorará o método __call__. Em Python, objetos que podem ser "chamados" usando parênteses () como funções são conhecidos como objetos callable (chamáveis). Funções e métodos são naturalmente chamáveis.

Por padrão, as instâncias de classe não são chamáveis. No entanto, se você definir o método especial __call__ em uma classe, suas instâncias se tornam chamáveis. Quando você chama uma instância como se fosse uma função, o código dentro do seu método __call__ é executado. Isso é útil para criar objetos que se comportam como funções, mas que também podem manter seu próprio estado interno.

Vamos criar uma classe cujas instâncias possam ser chamadas. Abra o arquivo callable_instance.py no explorador de arquivos.

Adicione o seguinte código ao callable_instance.py:

## File Name: callable_instance.py

class Greeter:
    def __init__(self, greeting):
        ## Este estado é armazenado com a instância
        self.greeting = greeting
        print(f'Greeter initialized with "{self.greeting}"')

    ## Define o método __call__ para tornar as instâncias chamáveis
    def __call__(self, name):
        ## Este código é executado quando a instância é chamada
        print(f"{self.greeting}, {name}!")

## Cria uma instância de Greeter
hello_greeter = Greeter("Hello")

## Verifica se a instância é chamável usando a função built-in callable()
print(f"Is hello_greeter callable? {callable(hello_greeter)}")

## Agora, chama a instância como se fosse uma função
print("\nCalling the instance:")
hello_greeter("Alice")
hello_greeter("Bob")

## Cria outra instância com um estado diferente
goodbye_greeter = Greeter("Goodbye")
print("\nCalling the second instance:")
goodbye_greeter("Charlie")

Salve o arquivo. Agora, execute o script callable_instance.py no terminal.

python ~/project/callable_instance.py

A saída mostra que a instância hello_greeter é de fato chamável. Cada vez que você a chama, o método __call__ é executado, utilizando o estado (self.greeting) que foi definido durante a inicialização.

Greeter initialized with "Hello"
Is hello_greeter callable? True

Calling the instance:
Hello, Alice!
Hello, Bob!
Greeter initialized with "Goodbye"

Calling the second instance:
Goodbye, Charlie!

Isso demonstra como __call__ permite criar objetos com estado que se assemelham a funções, o que é um recurso poderoso na programação orientada a objetos.

Resumo

Neste laboratório, você explorou vários métodos especiais poderosos em Python. Você aprendeu a usar __new__ para controlar o processo de criação de instâncias, fornecendo um ponto de intervenção antes que __init__ seja chamado. Você implementou o método __del__ para definir a lógica de limpeza que é executada quando um objeto é coletado pelo garbage collector. Você também usou __slots__ para otimizar a memória e impor um modelo de atributo estrito, impedindo a criação de um __dict__ de instância. Finalmente, você fez seus objetos se comportarem como funções implementando o método __call__. Ao dominar esses métodos dunder (métodos especiais), você pode escrever classes mais flexíveis, eficientes e Pythonicas.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.