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.
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!
<div class="pt-24 -mt-20 relative -z-10 anchor-item" id="implementar-heran%C3%A7a-e-polimorfismo"></div>
## 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:
```python
## 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.
<div class="pt-24 -mt-20 relative -z-10 anchor-item" id="praticar-heran%C3%A7a-m%C3%BAltipla"></div>
## 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:
```python
## 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 deParentAporqueParentAvem primeiro no MRO deChild_AB.child2.speak()chama o método deParentBporqueParentBvem primeiro no MRO deChild_BA.child2.common_method()chama a versão definida diretamente emChild_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.



