Compreendendo os Recursos de Classe em Python

PythonBeginner
Pratique Agora

Introdução

Neste laboratório prático (lab), você obterá uma compreensão prática dos principais conceitos de Programação Orientada a Objetos (OOP) em Python. Começaremos com encapsulamento, aprendendo a agrupar dados e métodos dentro de uma classe e a controlar o acesso aos dados usando atributos privados.

Em seguida, você implementará herança para construir relações entre classes, o que promove a reutilização de código. Também exploraremos polimorfismo, que permite que objetos de diferentes classes sejam tratados de forma uniforme. Finalmente, você usará o método super() para chamar efetivamente métodos de uma classe pai e praticará herança múltipla para ver como uma classe pode herdar de várias classes pai.

Este é um Laboratório Guiado (Guided Lab), que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e obter 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%. Ele recebeu uma taxa de avaliação positiva de 100% dos alunos.

Explorar o Encapsulamento com Classes Básicas

Nesta etapa, exploraremos o encapsulamento, um princípio fundamental da OOP. Encapsulamento envolve agrupar dados (atributos) e os métodos que operam sobre esses dados em uma única unidade, a classe. Ele também restringe o acesso direto ao estado interno de um objeto, o que ajuda a prevenir modificações acidentais nos dados.

Em Python, usamos uma convenção de nomenclatura para indicar que um atributo é "privado". Antecipar um atributo com um único sublinhado (underscore) (ex: _nome) sinaliza que ele se destina a uso interno. Embora não seja estritamente imposto, é uma convenção forte que os desenvolvedores respeitam.

Começaremos criando duas classes separadas, Dog e Cat, para ver como elas podem ser estruturadas.

Primeiro, localize o arquivo animal_classes.py no explorador de arquivos no lado esquerdo do WebIDE. Abra-o e adicione o seguinte código Python. Este código define uma classe Dog e uma classe Cat, cada uma com um atributo privado _name e métodos para interagir com ele.

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## O prefixo de um único sublinhado indica um atributo "privado".
        self._name = name

    ## Método público para obter o valor do atributo privado.
    def get_name(self):
        return self._name

    ## Método público para definir o valor do atributo privado.
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

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

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## Este bloco será executado apenas quando o script for executado diretamente.
if __name__ == "__main__":
    ## Cria uma instância da classe Dog
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## Altera o nome do cachorro usando o método setter
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## Cria uma instância da classe Cat
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

Após adicionar o código, salve o arquivo.

Agora, vamos executar o script para ver o encapsulamento em ação. Abra o terminal no WebIDE e execute o seguinte comando:

python animal_classes.py

Você verá a seguinte saída, que demonstra que estamos interagindo com o atributo privado _name através dos nossos métodos públicos get_name e set_name.

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

Implementar Herança e Polimorfismo

Na etapa anterior, você deve ter notado que as classes Dog e Cat compartilhavam muito código idêntico (__init__, get_name, set_name). Esta é uma oportunidade perfeita para usar a herança (inheritance). A herança permite que uma nova classe (a classe filha ou subclasse) herde atributos e métodos de uma classe existente (a classe pai ou superclasse), promovendo a reutilização de código.

Também introduziremos o polimorfismo (polymorphism), que significa "muitas formas". Em OOP, refere-se à capacidade de classes diferentes responderem à mesma chamada de método de maneiras únicas para cada uma.

Vamos refatorar nosso código. Criaremos uma classe pai Animal para conter o código comum e faremos com que Dog e Cat herdem dela. O método say, que é diferente para cada uma, demonstrará o polimorfismo.

Abra o arquivo animal_classes.py e substitua todo o seu conteúdo pelo código a seguir:

## File: animal_classes.py

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

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog herda de Animal
class Dog(Animal):
    ## Isso sobrescreve (overrides) o método say() da classe Animal
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat herda de Animal
class Cat(Animal):
    ## Isso também sobrescreve o método say()
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- Calling say() on each object ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- Demonstrating Polymorphism ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

Salve o arquivo. Observe como as classes Dog e Cat estão agora muito mais simples. Elas herdam os métodos __init__, get_name e set_name da classe Animal. Cada uma fornece sua própria versão do método say, o que é um exemplo de sobrescrita de método (method overriding).

Agora, execute o script atualizado no terminal:

python animal_classes.py

A saída será:

--- Calling say() on each object ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- Demonstrating Polymorphism ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

A função make_animal_speak aceita qualquer objeto que possua um método say. Mesmo que passemos a ela objetos de tipos diferentes (Animal, Dog, Cat), ela funciona corretamente porque cada objeto sabe como executar a ação say à sua própria maneira. Este é o poder do polimorfismo.

Utilizar o Método super() para Estender Funcionalidades

Quando uma classe filha sobrescreve um método de sua classe pai, ela às vezes precisa estender o método do pai, e não apenas substituí-lo. A função super() fornece uma maneira de chamar o método da classe pai de dentro da classe filha.

Isso é muito comum no método __init__. Uma classe filha frequentemente precisa realizar suas próprias etapas de inicialização, além da inicialização realizada por seu pai.

Vamos adicionar atributos exclusivos às nossas classes Dog e Cat. O Dog terá uma age (idade), e o Cat terá uma color (cor). Usaremos super() para garantir que o método __init__ da classe pai Animal ainda seja chamado para definir o _name.

Modifique o arquivo animal_classes.py substituindo seu conteúdo pelo código a seguir:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## Chama o método __init__ do pai para lidar com o atributo 'name'
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## Também podemos usar super() para chamar o método say() do pai
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## Chama o método __init__ do pai
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

Salve o arquivo. Nesta versão, Dog.__init__ e Cat.__init__ primeiro chamam super().__init__(name). Isso executa o código em Animal.__init__, que define o atributo _name. Depois disso, eles prosseguem com suas próprias inicializações específicas (self.age = age e self.color = color).

Execute o script no terminal:

python animal_classes.py

A saída demonstra a cadeia de chamadas de __init__ e os métodos say estendidos:

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

Praticar Herança Múltipla

Python permite que uma classe herde de mais de uma classe pai. Isso é chamado de herança múltipla (multiple inheritance). Pode ser uma ferramenta poderosa para misturar funcionalidades de diferentes fontes, mas também introduz complexidade, particularmente na forma como o Python decide qual método do pai usar se eles tiverem o mesmo nome.

Essa ordem de busca é chamada de Ordem de Resolução de Métodos (Method Resolution Order - MRO). Python usa um algoritmo chamado linearização C3 para determinar um MRO consistente e previsível.

Vamos explorar isso com um novo exemplo. Abra o arquivo multiple_inheritance.py no explorador de arquivos e adicione o seguinte código:

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child herda de A, depois de B
class Child_AB(ParentA, ParentB):
    pass

## Child herda de B, depois de A
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Investigating Child_AB (ParentA, ParentB) ---")
    child1.speak()
    child1.common_method()
    ## O método .mro() mostra a Ordem de Resolução de Métodos
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Investigating Child_BA (ParentB, ParentA) ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

Salve o arquivo. Aqui, Child_AB herda de ParentA e depois de ParentB. Child_BA herda na ordem inversa. Quando um método é chamado, o Python o procura na ordem especificada pelo MRO.

Execute o script no terminal:

python multiple_inheritance.py

Você verá a seguinte saída:

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

A partir da saída, você pode observar:

  • child1.speak() chama o método de ParentA porque ParentA vem primeiro no MRO de Child_AB.
  • child2.speak() chama o método de ParentB porque ParentB vem primeiro no MRO de Child_BA.
  • child2.common_method() chama a versão definida diretamente em Child_BA, pois o Python a encontra lá primeiro antes de verificar os pais.

Compreender o MRO é crucial para prever o comportamento em cenários de herança múltipla.

Resumo

Neste laboratório, você adquiriu experiência prática com quatro conceitos fundamentais de programação orientada a objetos em Python.

Você começou com encapsulamento (encapsulation), aprendendo a proteger os dados da classe por convenção, usando atributos privados e fornecendo métodos públicos para acesso. Em seguida, você refatorou seu código para usar herança (inheritance), criando uma classe pai Animal para reduzir a duplicação de código nas subclasses Dog e Cat.

Ao implementar a herança, você viu o polimorfismo (polymorphism) em ação, pois os objetos Dog e Cat responderam de maneira diferente à mesma chamada de método say(). Você aprendeu a usar o método super() para chamar e estender a funcionalidade da classe pai, especialmente dentro do método __init__. Finalmente, você explorou a herança múltipla (multiple inheritance) e a importância da Ordem de Resolução de Métodos (MRO) na determinação de qual método do pai é chamado.