image

Acesse bootcamps ilimitados e +650 cursos

50
%OFF
Article image
Stella Costa
Stella Costa24/08/2023 13:18
Compartilhe

Desvendando a Programação Orientada a Objetos com Python e Krypto, o Supercão 🐶🦸🏽‍♂️

  • #Python
  • #POO

Se você tem a curiosidade sobre o que exatamente é Programação Orientada a Objetos (POO) e por que ela é tão falada, você está no lugar certo! Neste guia descobriremos e desvendaremos alguns segredinhos sobre POO em Python 🐍. Afinal, Python é sempre a segunda melhor linguagem para se fazer e aprender sobre qualquer coisa! 🤣

POO é mais do que um termo técnico; é uma abordagem que revolucionará a maneira como você programa. Com a POO, podemos pensar no código como se estivéssemos montando um quebra-cabeça. Cada objeto é como uma peça única, com sua própria forma e características. E assim como em um quebra-cabeça, onde cada peça tem seu lugar específico para completar a imagem, no mundo da programação, cada objeto tem sua função determinada. Isso torna tudo mais estruturado e, acredite, muito mais intuitivo! Então, da próxima vez que você ouvir falar em POO, imagine-a como o desafio mais envolvente do mundo da programação, onde cada linha de código é uma peça se encaixando perfeitamente! 🧩🤓🖥️.

Conceitos Básicos de POO

Bom começaremos do começo 😅. Antes da POO os programadores utilizavam uma abordagem chamada programação procedural, ou programação estruturada, esse tipo de programação envolve a construção de um conjunto de funções (procedimentos) e a transmissão de dados por meio da chamada de cada uma dessas funções. Um exemplo de função simples em python é:

def soma(a, b):
  return a + b

Nessa função, temos dois parâmetros, `a` e `b`, e o retorno da função é a soma desses dois parâmetros. Para chamar essa função, basta fazer:

soma(1, 2)

E teremos o retorno:

>>>  3

Certamente! As funções em Python são fantásticas, ajudando a manter nosso código organizado e facilitando sua reutilização. No entanto, quando nos deparamos com desafios mais complexos, como simular um banco com clientes, contas e transações, apenas funções podem não ser a solução ideal. E é aqui que a magia da POO entra em cena! ✨

Com a Programação Orientada a Objetos, começamos a visualizar nosso código em termos de “objetos”. Estes objetos são como pequenas entidades, cada uma com suas próprias características (atributos) e ações (métodos). Em vez de depender apenas de funções soltas, temos uma organização que combina dados e ações de maneira coesa. Isso nos proporciona uma abordagem mais alinhada com a realidade, tornando a modelagem de sistemas intrincados mais intuitiva.

A POO também nos introduz a conceitos avançados como herança, polimorfismo e encapsulamento. Estes conceitos nos ajudam a desenvolver códigos mais estruturados, adaptáveis e de fácil manutenção. Embora as funções sejam essenciais e tenham seu valor, a POO nos leva a um nível superior de programação, possibilitando a criação de sistemas mais sofisticados e compreensíveis. 🚀🖥️🧠

🕵🏽‍♀️ De onde vem? A Programação Orientada a Objetos (POO) ganhou destaque com Alan Kay, o criador da linguagem Smalltalk. No entanto, antes mesmo do surgimento do Smalltalk, conceitos de POO já estavam em prática. A linguagem Simula 67, desenvolvida por Ole-Johan Dahl e Kristen Nygaard em 1967, foi a pioneira em adotar essas ideias. Contudo, foi apenas na década de 90 que a POO se consolidou e foi amplamente aceita pelas grandes empresas de desenvolvimento de software.

Fundamentos de Classes e Objetos

Definindo Classes e Objetos

Todo mundo sabe o que são objetos: uma caneta, um carro, uma laranja, um cubo mágico, etc. Mas o que exatamente é uma classe? 🤔 Bom, uma classe é como um blueprint ou modelo para se criar um objeto. A classe nos diz quais características (atributos) ou que ações ele pode realizar (métodos).

Por exemplo, imagine que uma classe chamada “Cubo_Mágico” tenha os seguintes atributos (características): modelo, tamanho, peso e marca. E os seguintes métodos (ações): girar, embaralhar, resolver… Agora, se temos um cubo mágico real em nossas mãos (objeto), ele terá todas essas características e poderá realizar todas essas ações. 🧙🏽‍♂️🧙🏽‍♀️

Então sempre que ouvirmos falar em classes, podemos pensar nelas como um esboço detalhadamente descrito de um objeto. E quando falamos em objetos, podemos pensar neles como instâncias de uma classe. 🤯

Calma! Pode parecer um pouco confuso no começo, mas vamos ver um exemplo prático para ficar mais claro. Vamos criar uma classe chamada “Cubo_Mágico” e instanciá-la em um objeto chamado “cubo_1”. Para isso, basta fazer:

class Cubo_Mágico:
  def __init__(self, modelo, tamanho, peso, marca):
      self.modelo = modelo
      self.tamanho = tamanho
      self.peso = peso
      self.marca = marca

  def girar(self):
      print("Girando o cubo mágico...")

  def embaralhar(self):
      print("Embaralhando o cubo mágico...")

Agora instanciemos essa classe em um objeto chamado “cubo_1”:

cubo_1 = Cubo_Mágico("3x3", "pequeno", "leve", "Rubik's")

E se quisermos girar e embaralhar o cubo_1? Simples!

cubo_1.girar()
cubo_1.embaralhar()

Nesse exemplo a classe “Cubo_Mágico” é como um blueprint para se criar um objeto. E o objeto “cubo_1” é uma instância dessa classe. 🧩🧙🏽

Além disso, temos métodos para girar e embaralhar o cubo mágico. E esses métodos são chamados de métodos de instância, pois só podem ser chamados por uma instância da classe. 🧙🏽‍♂️🧙🏽‍♀️

Veremos mais exemplos de classes e objetos ao longo desse texto, não se preocupe! 😉

Natureza dos objetos em Python

Em Python tudo é um objeto! 🤯 Isso mesmo, tudo! E não precisa acreditar em mim, basta fazer:

 x = 10
 print(type(x))

E teremos o retorno:

 >>> <class 'int'>

Nesse exemplo, a variável `x` é um objeto do tipo `int` (inteiro). E se fizermos:

y = "Python"
print(type(y))

Teremos o retorno:

>>>  <class 'str'>

Nesse exemplo, a variável `y` é um objeto do tipo `str` (string).

Isso significa que sempre que criamos uma variável em Python, estamos criando um objeto. E esse objeto tem um tipo, definido pela classe que o criou. 🤯 Em Python, os objetos são “cidadãos de primeira classe”. Isso significa que eles podem ser passados como argumentos para funções, retornados como valores, atribuídos a variáveis e assim por diante.

Construindo Classes

Sei que já construímos nossa primeira classe no tópico anterior, mas veremos mais alguns exemplos para fixar o conceito. 🧩

Criando uma classe

Quando falamos de classes em Python, estamos falando de criar um molde. A ideia é que, com esse molde, possamos criar várias “cópias” ou instâncias que seguem esse padrão. E para começar a construir uma classe, basta usar a palavra reservada `class` seguida do nome da classe. Por exemplo, criemos uma classe chamada “Cachorro”:

 class Cachorro:
  pass
🔍 O `pass` é só um jeitinho de dizer: “Ei, Python, eu ainda não coloquei nada aqui, mas está tudo bem!”. É como reservar um espaço para algo que faremos depois.
🐍 Classes em Python são escritas em CamelCase, ou seja, a primeira letra de cada palavra é maiúscula. E se você quiser saber mais sobre CamelCase, pode dar uma olhada aqui.

Introdução ao construtor

Após termos o molde pronto, queremos dar características a ele. E é aí que entra o famoso `__init__`. No nosso caso, escolheremos o nome e a idade do nosso cachorro: 🐶

Vamos agora definir o construtor da classe “Cachorro”:

 class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade

Aqui, o `__init__` é o que chamamos de construtor. Ele nos ajuda a dar vida ao nosso cachorro, definindo seu nome e idade. E esses nome e idade entre parênteses? São os parâmetros! Eles nos dizem o que precisamos para criar nosso cachorro.

Parâmetros? Sim, parâmetros. Eles são como as especificações de um produto. Você define quais são necessários (na definição da função) e depois fornece essas informações (quando chama a função). No nosso cachorro, as especificações são o nome e a idade! 🐾

Instanciando Objetos

Agora que temos nosso molde (a classe “Cachorro”), podemos criar nosso primeiro cachorro virtual! No mundo da programação, chamamos isso de “instanciar um objeto”. E é super simples:

cachorro_1 = Cachorro("Krypto", 2)

Aqui, estamos dizendo: “Oi, Python! Quero um cachorro chamado Krypto que tem 2 anos”. E o Python, como o bom assistente que é, cria esse cachorro para nós.

E o mais legal? Podemos conversar com nosso cachorro e perguntar coisas sobre ele:

print(cachorro_1.nome)
print(cachorro_1.idade)
>>>  Krypto
>>>  2

Assim, sempre que quisermos saber algo sobre o “cachorro_1”, é só perguntar! E se quisermos mais cachorros? Basta instanciar mais objetos. Cada um com sua personalidade e características. 🐶🎉

Métodos

Métodos são como as habilidades ou ações que nossos objetos podem realizar. Se pensarmos no nosso cachorro, os métodos seriam as instruções para ele latir, sentar ou buscar a bola. No mundo da programação, os métodos são funções que pertencem a uma classe e que geralmente atuam ou modificam os atributos dessa classe.

Introdução aos métodos

Continuaremos trabalhando com nosso objeto cachorro_1. E adicionaremos alguns métodos a ele. 🐶

Suponhamos que o Krypto sabe latir. Então, adicionemos esse método à nossa classe:

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade

  def latir(self):
      print("Au au!")

Para fazer com que ele execute essa ação, basta chamar o método:

cachorro_1 = Cachorro("Krypto", 2)
cachorro_1.latir()
>>>  Au au!
🐍 O `self` é uma referência ao próprio objeto. Ele é usado para acessar os atributos e métodos do objeto. E se você quiser saber mais sobre o `self`, pode dar uma olhada aqui.

Trabalhando com vários métodos

Imagine que nosso cachorro, além de latir, também pode fazer alguns truques legais, como sentar e rolar. Proponho ensinarmos esses truques a ele!

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.nome} fez o truque e rolou!")

Agora, que tal vermos o Krypto realizando um truque?

cachorro_1 = Cachorro("Krypto", 2)
cachorro_1.rolar()
>>> Olha só! Krypto fez o truque e rolou!

E se quisermos fazer uma pequena apresentação e mostrar o Krypto rolando várias vezes? Nada que um loop não resolva!

cachorro_1 = Cachorro("Krypto", 2)
for i in range(5):
  cachorro_1.rolar()
>>>  Olha só! Krypto fez o truque e rolou!
>>>  Olha só! Krypto fez o truque e rolou!
>>>  Olha só! Krypto fez o truque e rolou!
>>>  Olha só! Krypto fez o truque e rolou!
>>>  Olha só! Krypto fez o truque e rolou!
🐍 A função `range()` nos permite repetir uma ação por um número específico de vezes. É como dizer “Krypto, faça isso 5 vezes!”. E ele, sendo o bom cachorro que é, obedece! 🐶🎉

Gerenciamento de Memória

Entendendo o Garbage Collection

Imagine que, após uma grande festa, você precisa limpar a bagunça e descartar o que não é mais necessário. O “Garbage Collection” (ou Coletor de Lixo) no mundo da programação tem uma função semelhante. Ele identifica e elimina os objetos que não são mais utilizados, garantindo que a memória do computador seja otimizada. Em linguagens como Python e Java, esse processo é automático, e o programador não precisa gerenciá-lo diretamente.

Referências de Objetos

Quando um objeto é criado em Python, ele é armazenado em um local específico da memória. Para acessar e manipular esse objeto, o Python fornece uma referência, que funciona como um endereço de memória.🏷️

Veja este exemplo:

cachorro_1 = Cachorro("Krypto", 2)
cachorro_2 = cachorro_1

Neste caso, `cachorro_1` e `cachorro_2` são referências que apontam para o mesmo objeto, o Krypto. Para confirmar isso, podemos verificar os endereços de memória:

print(id(cachorro_1))
print(id(cachorro_2))
>>> 1843757270480
>>> 1843757270480

Ambas as referências, `cachorro_1` e `cachorro_2`, apontam para o mesmo local de memória, indicando que elas se referem ao mesmo objeto.

"Desreferenciando" Objetos

Se, em algum momento, decidirmos que não precisamos mais de uma referência, podemos removê-la utilizando a palavra-chave `None`:

cachorro_1 = None

No exemplo acima, ao dizer `cachorro_1 = None`, é como se estivéssemos dizendo: “Ei, Python, não estou mais brincando com o Krypto agora!”.

Assim, `cachorro_1` não aponta mais para o Krypto. No entanto, Krypto ainda existe na memória, pois `cachorro_2` ainda mantém uma referência a ele. Somente quando todas as referências a um objeto são removidas é que o Coletor de Lixo 🗑️ pode descartá-lo, otimizando a memória utilizada.

Introdução ao Encapsulamento

Você já colocou uma coleira no seu cachorro para um passeio? O encapsulamento é tipo isso, mas no mundo da programação. É a nossa maneira mágica de proteger nosso “cachorro virtual” de correr atrás de qualquer carro que passe. 🐶🚗

Com o encapsulamento, podemos definir quais atributos e métodos são públicos ou privados. Os atributos e métodos públicos podem ser acessados por qualquer objeto, enquanto os privados só podem ser acessados pelo próprio objeto. Vejamos como isso funciona na prática! 🐍

Trabalhando com Atributos Privados

Lembra do Krypto? Então, vamos supor que ele tenha uma identidade secreta. Vamos chamá-lo de Super Cão! 🦸‍♂️💥 Mas não queremos que ninguém saiba disso, então tornaremos esse atributo privado. Para isso basta adicionar dois underscores antes do nome do atributo:

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade
      self.__identidade_secreta = "Super Cão"

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.nome} fez o truque e rolou!")

Agora se tentarmos acessar o atributo `__identidade_secreta` diretamente, receberemos um erro:

cachorro_1 = Cachorro("Krypto", 2)
print(cachorro_1.__identidade_secreta)
>>> AttributeError: 'Cachorro' object has no attribute '__identidade_secreta'

Mas, se quisermos acessar esse atributo, podemos criar um método público que nos permita fazer isso:

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade
      self.__identidade_secreta = "Super Cão"

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

Agora podemos acessar a identidade secreta do Krypto:

cachorro_1 = Cachorro("Krypto", 2)
cachorro_1.mostrar_identidade_secreta()
>>> Eu sou o Super Cão!

Métodos Privados

Imagine agora que, além de sua identidade como “Super Cão”, Krypto tenha outras identidades secretas ao longo do tempo. No entanto, não queremos que qualquer pessoa possa simplesmente mudar essa identidade. Para isso, podemos criar um método privado que nos permita alterar essa identidade, mas somente na classe.

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade
      self.__identidade_secreta = "Super Cão"

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

Neste exemplo, o método `__mudar_identidade_secreta` é privado, o que significa que só pode ser chamado na classe `Cachorro`. No entanto, criamos um método público, `atualizar_identidade`, que pode ser usado para alterar a identidade secreta do Krypto, mas ele faz isso chamando o método privado. Isso nos dá um controle mais granular sobre como e quando a identidade secreta pode ser alterada.

Para atualizar a identidade secreta do Krypto, podemos fazer o seguinte:

cachorro_1 = Cachorro("Krypto", 2)
cachorro_1.atualizar_identidade("Super Cão 2.0")
cachorro_1.mostrar_identidade_secreta()
>>> Eu sou o Super Cão 2.0!

Isso significa que o Krypto, orgulhosamente, nos revelou que sua nova identidade secreta é “Super Cão 2.0”! 🎉

Perceba que o método `atualizar_identidade` é público, mas o método `__mudar_identidade_secreta` é privado. Isso significa que podemos chamar o método `atualizar_identidade` de qualquer lugar, mas não podemos chamar o método `__mudar_identidade_secreta` fora da classe `Cachorro`.

Utilizando Getters e Setters

Na POO, os métodos que nos permitem acessar e alterar atributos privados são chamados de getters e setters. Vejamos como isso funciona na prática! 🐍

- Getters: são métodos que nos permitem acessar atributos.

- Setters: são métodos que nos permitem alterar atributos.

Por exemplo, suponhamos que queremos garantir que ninguém possa alterar a idade do Krypto para um número negativo. Para isso, podemos criar um método `setter` que nos permita alterar a idade, mas que verifique se o novo valor é maior que zero:

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade
      self.__identidade_secreta = "Super Cão"

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  def set_idade(self, nova_idade):
      if nova_idade > 0:
          self.idade = nova_idade
      else:
          print("A idade deve ser maior que zero!")

Agora, como exemplo para o uso do `getter` imaginaremos que o Krypto tem um nível de energia que vai de 0 a 100. Para isso, podemos criar um método `getter` que nos permita acessar esse atributo:

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade
      self.__identidade_secreta = "Super Cão"
      self.__energia = 50

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  def set_idade(self, nova_idade):
      if nova_idade > 0:
          self.idade = nova_idade
      else:
          print("A idade deve ser maior que zero!")

  def get_energia(self):
      if self.__energia > 80:
          return f"🐶 {self.nome} está super energético e pronto para brincar!"
      elif 50 < self.__energia <= 80:
          return f"🐕 {self.nome} está se sentindo bem e pronto para um passeio."
      elif 20 < self.__energia <= 50:
          return f"😴 {self.nome} está um pouco cansado, talvez precise de uma soneca."
      else:
          return f"💤 {self.nome} está exausto e precisa descansar."

Agora, quando perguntamos sobre a energia do Krypto, em vez de obter um número, obtemos uma resposta amigável sobre como ele se sente:

cachorro_1 = Cachorro("Krypto", 2)
print(cachorro_1.get_energia())
>>> 🐕 Krypto está se sentindo bem e pronto para um passeio.

E aí está! Usamos o getter para transformar um valor interno (nível de energia) em uma resposta mais compreensível e amigável. 🌟🎉

Introdução às Property

Você já deve ter notado que, ao usar getters e setters, nosso código pode ficar um pouco mais extenso. Mas, e se eu te disser que o Python tem uma maneira super legal de simplificar isso? 🌟 É aqui que entra o conceito de property!

Property em Python é uma maneira de definir métodos em uma classe que se comportam como atributos. Isso significa que podemos acessar esses métodos como se fossem atributos, sem precisar usar parênteses. Vejamos como isso funciona na prática! 🐍

class Cachorro:
  def __init__(self, nome, idade):
      self.__nome = nome
      self.__idade = idade
      self.__identidade_secreta = "Super Cão"
      self.__energia = 50

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.__nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.__nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  def set_idade(self, nova_idade):
      if nova_idade > 0:
          self.idade = nova_idade
      else:
          print("A idade deve ser maior que zero!")

  def get_energia(self):
      if self.__energia > 80:
          return f"🐶 {self.nome} está super energético e pronto para brincar!"
      elif 50 < self.__energia <= 80:
          return f"🐕 {self.nome} está se sentindo bem e pronto para um passeio."
      elif 20 < self.__energia <= 50:
          return f"😴 {self.nome} está um pouco cansado, talvez precise de uma soneca."
      else:
          return f"💤 {self.nome} está exausto e precisa descansar."

  @property
  def idade(self):
      return self.__idade

  @idade.setter
  def idade(self, nova_idade):
      if nova_idade > 0:
          self.__idade = nova_idade
          print(f"Idade atualizada para {self.__idade} anos.")
      else:
          print("A idade deve ser maior que zero!")
cachorro_1 = Cachorro("Krypto", 2)
cachorro_1.idade = -5
>>> A idade deve ser maior que zero!

No nosso exemplo, o decorador @property transforma o método idade em uma propriedade que pode ser acessada como um atributo. Isso significa que podemos pegar a idade do Krypto simplesmente usando cachorro_1.idade em vez de `cachorro_1.get_idade()`. Bem mais simples, né?

E o `@idade.setter`? Ele é um decorador que nos permite definir um setter para a propriedade idade. Assim, podemos atualizar a idade do Krypto de uma forma controlada, garantindo que ele não tenha uma idade negativa.

Vamos adicionar mais uma property para a nossa classe `Cachorro`:

class Cachorro:
  def __init__(self, nome, idade):
      self.__nome = nome
      self.__idade = idade
      self.__identidade_secreta = "Super Cão"
      self.__energia = 50

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.__nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.__nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  def set_idade(self, nova_idade):
      if nova_idade > 0:
          self.idade = nova_idade
      else:
          print("A idade deve ser maior que zero!")

  def get_energia(self):
      if self.__energia > 80:
          return f"🐶 {self.nome} está super energético e pronto para brincar!"
      elif 50 < self.__energia <= 80:
          return f"🐕 {self.nome} está se sentindo bem e pronto para um passeio."
      elif 20 < self.__energia <= 50:
          return f"😴 {self.nome} está um pouco cansado, talvez precise de uma soneca."
      else:
          return f"💤 {self.nome} está exausto e precisa descansar."

  @property
  def idade(self):
      return self.__idade

  @idade.setter
  def idade(self, nova_idade):
      if nova_idade > 0:
          self.__idade = nova_idade
          print(f"Idade atualizada para {self.__idade} anos.")
      else:
          print("A idade deve ser maior que zero!")

  @property
  def superpoder(self):
      return self.__superpoder

  @superpoder.setter
  def superpoder(self, poder):
      if poder in ["voar", "superforça", "raios laser"]:
          self.__superpoder = poder
          print(f"Superpoder do {self.__nome} atualizado para: {poder}!")
      else:
          print(f"Ops! {poder} não é um superpoder reconhecido.")

Agora, podemos facilmente definir e obter o superpoder do Krypto:

cachorro_1.superpoder = "voar"
print(cachorro_1.superpoder)
>>> Superpoder do Krypto atualizado para: voar!
>>> voar

E estes são exemplos de como podemos usar o conceito de property para simplificar nosso código. 🌟

Métodos e atributos protegidos

Você já ouviu falar de métodos e atributos privados, que são completamente escondidos de fora da classe. Mas e se quiséssemos algo no meio termo? Algo que sinalizasse aos desenvolvedores que “Ei, tenha cuidado ao usar isso, mas se você realmente precisar, vá em frente!” 🤔 Para isso temos os métodos e atributos protegidos.

Em Python, atributos protegidos são prefixados com um único sublinhado `_`. Vamos ver isso em ação com nosso amigo Krypto:

class Cachorro:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade
      self._raça = "Desconhecida"  # Atributo protegido

  def mostrar_raça(self):
      print(f"{self.nome} é da raça {self._raça}.")

Aqui, _raça é um atributo protegido. Isso significa que, embora possamos acessá-lo e modificá-lo de fora da classe, isso não é recomendado. É uma maneira de dizer: “Este é um detalhe interno da classe, então pense duas vezes antes de mexer nele!” 😉

Mas se quisermos acessá-lo e alterar seu valor basta fazer como no exemplo abaixo:

cachorro_1 = Cachorro("Krypto", 2)
cachorro_1._raça = "Kryptoniano"
cachorro_1.mostrar_raça()
>>> Krypto é da raça Kryptoniano.

Embora tenhamos conseguido modificar o atributo protegido, é uma boa prática evitar fazer isso, a menos que haja uma razão muito boa.

Da mesma forma, podemos ter métodos protegidos prefixando-os com um único sublinhado. Eles funcionam da mesma forma que os atributos protegidos, servindo como um sinal para outros desenvolvedores.

class Cachorro:
  def __init__(self, nome, idade):
      self.__nome = nome
      self.__idade = idade
      self.__identidade_secreta = "Super Cão"
      self.__energia = 50
      self.__superpoder = None

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.__nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.__nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  def set_idade(self, nova_idade):
      if nova_idade > 0:
          self.idade = nova_idade
      else:
          print("A idade deve ser maior que zero!")

  def get_energia(self):
      if self.__energia > 80:
          return f"🐶 {self.nome} está super energético e pronto para brincar!"
      elif 50 < self.__energia <= 80:
          return f"🐕 {self.nome} está se sentindo bem e pronto para um passeio."
      elif 20 < self.__energia <= 50:
          return f"😴 {self.nome} está um pouco cansado, talvez precise de uma soneca."
      else:
          return f"💤 {self.nome} está exausto e precisa descansar."

  @property
  def idade(self):
      return self.__idade

  @idade.setter
  def idade(self, nova_idade):
      if nova_idade > 0:
          self.__idade = nova_idade
          print(f"Idade atualizada para {self.__idade} anos.")
      else:
          print("A idade deve ser maior que zero!")

  @property
  def superpoder(self):
      return self.__superpoder

  @superpoder.setter
  def superpoder(self, poder):
      if poder in ["voar", "superforça", "raios laser"]:
          self.__superpoder = poder
          print(f"Superpoder do {self.__nome} atualizado para: {poder}!")
      else:
          print(f"Ops! {poder} não é um superpoder reconhecido.")

  def _encontrar_osso(self):  # Método protegido
      print(f"{self.__nome} encontrou um osso!")

  @staticmethod
  def numero_de_patas():
      return 4
print(Cachorro.numero_de_patas())
>>> 4

Lembre-se de usar métodos estáticos quando a funcionalidade for relevante para uma classe como um todo e não para instâncias específicas. 😉

Podemos ter também o que chamamos de métodos de classe. Eles são semelhantes aos métodos estáticos, mas recebem a classe como primeiro argumento.

Métodos estáticos usam o decorador `@staticmethod`, enquanto métodos de classe usam o decorador `@classmethod`.

Métodos de classe são uteis quando queremos realizar uma ação que envolva a classe em si e não uma instância específica. Por exemplo, vamos supor que queremos ter um contador que rastreia a quantidade de instâncias da classe `Cachorro` que foram criadas. Para isso, podemos criar um método de classe que atualiza esse contador toda vez que uma nova instância é criada:

class Cachorro:

  contador = 0

  def __init__(self, nome, idade):
      self.__nome = nome
      self.__idade = idade
      self.__identidade_secreta = "Super Cão"
      self.__energia = 50
      self.__superpoder = None
      Cachorro.contador += 1

  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.__nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.__nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  def set_idade(self, nova_idade):
      if nova_idade > 0:
          self.idade = nova_idade
      else:
          print("A idade deve ser maior que zero!")

  def get_energia(self):
      if self.__energia > 80:
          return f"🐶 {self.nome} está super energético e pronto para brincar!"
      elif 50 < self.__energia <= 80:
          return f"🐕 {self.nome} está se sentindo bem e pronto para um passeio."
      elif 20 < self.__energia <= 50:
          return f"😴 {self.nome} está um pouco cansado, talvez precise de uma soneca."
      else:
          return f"💤 {self.nome} está exausto e precisa descansar."

  @property
  def idade(self):
      return self.__idade

  @idade.setter
  def idade(self, nova_idade):
      if nova_idade > 0:
          self.__idade = nova_idade
          print(f"Idade atualizada para {self.__idade} anos.")
      else:
          print("A idade deve ser maior que zero!")

  @property
  def superpoder(self):
      return self.__superpoder

  @superpoder.setter
  def superpoder(self, poder):
      if poder in ["voar", "superforça", "raios laser"]:
          self.__superpoder = poder
          print(f"Superpoder do {self.__nome} atualizado para: {poder}!")
      else:
          print(f"Ops! {poder} não é um superpoder reconhecido.")

  def _encontrar_osso(self):  # Método protegido
      print(f"{self.__nome} encontrou um osso!")

  @staticmethod
  def numero_de_patas():
      return 4

  @classmethod
  def numero_de_cachorros(cls):
      return cls.contador
🐍 A palavra-chave `cls` é usada para se referir à classe. Assim como o self, ela é apenas uma convenção, mas é uma convenção muito comum.

Para chamar o nosso contador basta fazer o seguinte:

rex = Cachorro("Krypto", 3)
buddy = Cachorro("Buddy", 2)

print(Cachorro.numero_de_cachorros())
print(Cachorro.contador
>>> 2
>>> 2

Agora toda vez que um novo objeto da classe Cachorro for criado o contador será atualizado. 🎉

🐍 A principal diferença entre métodos e métodos de classe é que enquanto métodos estáticos não tem nenhum atributo ou método da classe como argumento, métodos de classe recebem a classe como primeiro argumento.

Tanto métodos estáticos quanto métodos de classe são ferramentas valiosas em OOP. Eles permitem que você defina funcionalidades relevantes para a classe na totalidade, em vez de para instâncias individuais. Ao projetar suas classes, pense cuidadosamente sobre qual tipo de método é mais apropriado para a funcionalidade que você deseja implementar. E lembre-se, a prática leva à perfeição! 🐕🚀🎉

Métodos Slots e sua Utilidade

Em Python toda vez que criamos uma instância de uma classe, um diciário é criado para armazenar os atributos dessa instância. Isso significa que, se tivermos milhares de instâncias de uma classe, teremos milhares de dicionários ocupando espaço na memória. 🤔

Mas e se houvesse uma maneira de otimizar isso? 🤔

Aí que entram os métodos slots! 🎉 `__slots__` é um mecanismo que permite declarar explicitamente quais atributos a instância da classe pode ter, eliminando a necessidade do dicionário e assim economizando espaço na memória. 🐍

Para usar `__slots__` devemos simplesmente definir um atributo de classe com esse nome e atribuir a ele uma lista de strings com os nomes dos atributos que queremos definir para a classe. Olha só! 🐶

class Cachorro:
  __slots__ = ['nome', 'idade']

  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade

Aqui estamos dizendo que um objeto da classe Cachorro só pode ter os atributos nome e idade. Se tentarmos definir um atributo que não esteja na lista, receberemos um erro.

Por que usar slots? 🤷‍♂️

Economia de Memória: Como mencionado anteriormente, slots pode resultar em uma economia significativa de memória porque evita a criação do dicionário dict para cada instância.

Prevenção de erros: Ao usar slots, você impede que novos atributos sejam adicionados dinamicamente, o que pode ajudar a evitar erros em programas grandes ou quando trabalha em equipe.

Acesso mais rápido: Acessar atributos em uma classe que define slots pode ser ligeiramente mais rápido porque eles são armazenados em um array e não em um dicionário.

Considerações ao usar slots 🚫

- Ao usar slots, você perde algumas funcionalidades fornecidas pelo dicionário dict, como a capacidade de adicionar atributos em tempo de execução.

- Se você tentar adicionar um atributo que não está definido em slots, receberá um AttributeError.

- slots não é herdado. Se você criar uma subclasse, ela terá um dicionário dict a menos que você também defina slots nela.

Slots é uma ferramenta poderosa que pode ajudar a otimizar nosso código em termos de memória e velocidade. No entanto, é importante lembrar que, como qualquer ferramenta, ela tem suas limitações. 🐍 Em muitos casos, a flexibilidade fornecida pelo dicionário dict é mais benéfica do que a economia de memória fornecida por slots. Como sempre, a chave é entender as necessidades e restrições do seu projeto específico e escolher as ferramentas adequadas para o trabalho! 🐍📘🚀

Heranças das Classes

A herança é um dos pilares da POO e permite que uma classe herde atributos e métodos de outra classe. A classe que herda é chamada de classe filha ou subclasse, e a classe herdada é chamada de classe pai ou superclasse. 🐶

Para ficar mais claro vamos outra classe chamada animal:

class Animal:
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade

  def comer(self):
      print(f"{self.nome} está comendo!")

Agora podemos modificar nossa classe `Cachorro` para que ela herde os atributos e métodos da classe `Animal`:

class Cachorro(Animal):
  def __init__(self, nome, idade, raça):
      super().__init__(nome, idade)
      self.raça = raça

  def fazer_som(self):
      print(f"{self.nome} diz: Au au!")

Note que para herdar os atributos e métodos da classe `Animal` basta colocar o nome da classe entre parênteses na definição da classe `Cachorro`. 🐶

E o construtor super serve para podermos acessar os atributos da classe pai. 🐶

Aplicaremos esse conhecimento ao nosso amigo Krypto. 🐶 Para isso criaremos uma nova classe chamada `SuperCao` que herda da classe `Cachorro`:


class SuperCao(Cachorro):
  def __init__(self, nome, idade, planeta_origem):
      super().__init__(nome, idade)
      self.__planeta_origem = planeta_origem

  # Métodos específicos do SuperCao
  def voar(self):
      print(f"{self._Cachorro__nome} está voando alto no céu!")

  def visao_raio_x(self, objeto):
      print(f"{self._Cachorro__nome} está usando sua visão de raio-x para olhar através do {objeto}!")

  def revelar_planeta_origem(self):
      print(f"{self._Cachorro__nome} é originalmente do planeta {self.__planeta_origem}!")

Perceba que a classe `SuperCao` herda todos os atributos e métodos da classe `Cachorro` e também tem seus próprios atributos e métodos. 🐶

Agora podemos criar uma instância da classe `SuperCao` e usar seus métodos:

supercao_1 = SuperCao("Krypto", 2, "Krypton")
supercao_1.voar()
supercao_1.visao_raio_x("muro")
>>> Krypto está voando alto no céu!
>>> Krypto está usando sua visão de raio-x para olhar através do muro!

E também podemos usar os métodos da classe `Cachorro`:

supercao_1.latir()
supercao_1.sentar()
>>> Au au!
>>> Krypto obedeceu e sentou!

Com isso podemos perceber que a herança nos permite criar uma hierarquia de classes, onde podemos reutilizar código bem como adicionar novos atributos e métodos. 🌟🐾

Polimorfismo

O polimorfismo é outro pilar da POO e significa que um objeto pode assumir diferentes formas. O termo “polimorfismo” vem do grego, onde “poli” significa “muitos” e “morph” significa “formas”. No contexto da programação, isso se traduz na capacidade de uma única função ou método ser usado para diferentes tipos de objetos.

Usaremos o Krypto novamente como exemplo. Krypto é uma instância da classe `SuperCao` que por sua vez é uma subclasse da classe `Cachorro`. Isso significa que Krypto é um cachorro, mas também é um supercão. 🦸🏽‍♂️🐶⭐

Tanto a classe cachorro quanto a classe supercão tem um método chamado latir. Mas o que acontece quando chamamos esse método em Krypto? 🤔

class Cachorro:
  contador = 0

  def __init__(self, nome, idade):
      self.__nome = nome
      self.__idade = idade
      self.__identidade_secreta = "Super Cão"
      self.__energia = 50
      self.__superpoder = None
      Cachorro.contador += 1

  # Métodos básicos
  def latir(self):
      print("Au au!")

  def sentar(self):
      print(f"{self.__nome} obedeceu e sentou!")

  def rolar(self):
      print(f"Olha só! {self.__nome} fez o truque e rolou!")

  def mostrar_identidade_secreta(self):
      print(f"Eu sou o {self.__identidade_secreta}!")

  # Métodos relacionados à identidade secreta
  def __mudar_identidade_secreta(self, nova_identidade):
      self.__identidade_secreta = nova_identidade

  def atualizar_identidade(self, nova_identidade):
      self.__mudar_identidade_secreta(nova_identidade)

  # Métodos relacionados à energia
  def get_energia(self):
      if self.__energia > 80:
          return f"🐶 {self.__nome} está super energético!"
      elif 50 < self.__energia <= 80:
          return f"🐕 {self.__nome} está se sentindo bem!"
      elif 20 < self.__energia <= 50:
          return f"😴 {self.__nome} está um pouco cansado."
      else:
          return f"💤 {self.__nome} está exausto."

  # Propriedades e setters
  @property
  def idade(self):
      return self.__idade

  @idade.setter
  def idade(self, nova_idade):
      if nova_idade > 0:
          self.__idade = nova_idade
          print(f"Idade atualizada para {self.__idade} anos.")
      else:
          print("A idade deve ser maior que zero!")

  @property
  def superpoder(self):
      return self.__superpoder

  @superpoder.setter
  def superpoder(self, poder):
      if poder in ["voar", "superforça", "raios laser"]:
          self.__superpoder = poder
          print(f"Superpoder do {self.__nome} atualizado para: {poder}!")
      else:
          print(f"Ops! {poder} não é um superpoder reconhecido.")

  # Método protegido
  def _encontrar_osso(self):
      print(f"{self.__nome} encontrou um osso!")

  # Métodos estáticos e de classe
  @staticmethod
  def numero_de_patas():
      return 4

  @classmethod
  def numero_de_cachorros(cls):
      return cls.contador

class SuperCao(Cachorro):
  def __init__(self, nome, idade, planeta_origem):
      super().__init__(nome, idade)
      self.__planeta_origem = planeta_origem

  # Métodos específicos do SuperCao
  def voar(self):
      print(f"{self._Cachorro__nome} está voando alto no céu!")

  def visao_raio_x(self, objeto):
      print(f"{self._Cachorro__nome} está usando sua visão de raio-x para olhar através do {objeto}!")

  def revelar_planeta_origem(self):
      print(f"{self._Cachorro__nome} é originalmente do planeta {self.__planeta_origem}!")

  def latir(self):
      print(f"{self._Cachorro__nome} diz: eu sou um supercão!")

Perceba que a classe `SuperCao` tem um método latir que sobrescreve o método latir da classe `Cachorro`. Isso significa que quando chamamos o método latir na classe `SuperCao` o método da classe `Cachorro` é ignorado. 🤯

supercao_1 = SuperCao("Krypto", 2, "Krypton")
supercao_1.latir()
>>> Krypto diz: eu sou um supercão!

Isso é o polimorfismo em ação! 🎉

O polimorfismo nos permite usar o mesmo nome de método em diferentes classes, mas com comportamentos adaptados para cada classe. Isso torna nosso código mais flexível e fácil de manter, pois podemos introduzir novas classes sem ter que reescrever funções ou métodos existentes.

Função hasattr

A função hasattr é uma ferramenta nativa do Python que nos ajuda a descobrir se um objeto específico possui um atributo ou método. Se o objeto em questão tiver o atributo ou método desejado, ela nos dará um retorno True. Caso contrário, nos dirá False. 🐍

supercao_1 = SuperCao("Krypto", 2, "Krypton")
print(hasattr(supercao_1, "latir"))
print(hasattr(supercao_1, "voar"))
>>> True
>>> True

Neste exemplo, queremos saber se o supercao_1 possui os métodos latir e voar. E sim, ele tem ambos! Por isso, hasattr nos confirma com um True em ambas as verificações. 🌟

Função isinstance

A função isinstance é outra joia do Python que nos permite verificar se um objeto pertence a uma classe específica. Ela nos dá um True se o objeto for uma instância da classe em questão e False se não for. 🐍

supercao_1 = SuperCao("Krypto", 2, "Krypton")
print(isinstance(supercao_1, SuperCao))
print(isinstance(supercao_1, Cachorro))
>>> True
>>> True

Aqui, estamos curiosos para saber se supercao_1 é uma instância das classes SuperCao e Cachorro. E a resposta é sim para as duas! Isso porque SuperCao é uma subclasse de Cachorro, então isinstance nos dá um aceno positivo para ambas as verificações. 🌈🎉

Duck Typing: Se Parece com um Pato e Soa como um Pato

Duck typing é um conceito que diz que um objeto é definido por seu comportamento, não por sua classe ou tipo. 🐍

Isso permite que os desenvolvedores escrevam funções e métodos que são mais genéricos e flexíveis. Se uma função espera um objeto que responda ao método voar, não importa se esse objeto é uma instância da classe Pato, SuperCao ou Avião, contanto que ele tenha um método voar.

Vamos lá! Imagine que temos várias classes de animais e super-heróis, e todos eles têm diferentes habilidades. Alguns podem voar, outros podem nadar, e alguns têm superforça.

Se quisermos criar uma função que faça um objeto voar, em vez de verificar se o objeto é de uma classe específica, simplesmente tentamos fazer o objeto voar:

def fazer_voar(objeto):
  objeto.voar()

Aqui, estamos dizendo que o objeto deve ter um método voar. Se tiver, a função funcionará. Se não tiver, receberemos um erro. 🤔

Se passarmos o Krypto (um SuperCao, e tem um método voar) para essa função, ele voará. Se passarmos um pato (que também tem um método voar), ele também voará. No entanto, se passarmos um peixe (que não tem um método voar), obteremos um erro.

supercao_1 = SuperCao("Krypto", 2, "Krypton")
fazer_voar(supercao_1)
>>> Krypto está voando alto no céu!
peixe_1 = Peixe("Nemo", 1)
fazer_voar(peixe_1)
>>> AttributeError: 'Peixe' object has no attribute 'voar'

Classe Abstrata

No mundo da POO, muitas vezes nos deparamos com situações em que queremos definir uma classe que não pode ser instanciada. Por exemplo, imagine que queremos criar uma classe Animal que tenha um método comer, mas não queremos que ninguém crie uma instância dessa classe. 🤔

Para isso, podemos usar o conceito de classe abstrata. Uma classe abstrata é uma classe que não pode ser instanciada. Ela serve como uma classe base para outras classes que podem ser instanciadas. 🐍

Para criar uma classe abstrata, precisamos importar o módulo abc (que significa Abstract Base Class) e usar o decorador @abstractmethod em qualquer método que queremos que seja abstrato. 🌟

from abc import ABC, abstractmethod

class Animal(ABC):
  def __init__(self, nome, idade):
      self.nome = nome
      self.idade = idade

  @abstractmethod
  def comer(self):
      pass

Aqui, definimos a classe Animal como uma classe abstrata e o método comer como um método abstrato. Isso significa que não podemos criar uma instância da classe Animal e que qualquer classe que herde da classe Animal deve implementar o método comer. 🐶

Para usarmos a classe Animal devemos criar uma subclasse que herde dela e implemente o método comer:

class Cachorro(Animal):
  def __init__(self, nome, idade):
      super().__init__(nome, idade)

  def comer(self):
      print(f"{self.nome} está comendo!")

Assim, podemos criar uma instância da classe Cachorro e usar o método comer:

cachorro_1 = Cachorro("Krypto", 2)
cachorro_1.comer()
>>> Krypto está comendo!

Mas se tentarmos criar uma instância da classe Animal, receberemos um erro:

animal_1 = Animal("Rex", 3)
>>> TypeError: Can't instantiate abstract class Animal with abstract methods comer

Conceito de interface em Python

Quando falamos de programação orientada a objetos, a ideia de interfaces pode parecer um pouco técnica. Mas, pense nelas como um contrato. Esse contrato diz: “Ei, se você quiser ser parte deste clube, precisa saber fazer essas coisas!” 📜

Em Python, não temos uma “palavra mágica” chamada interface. Mas, temos uma maneira legal de fazer algo parecido usando algo chamado classes abstratas. 🎩✨

Pensemos em pássaros. Todos os pássaros voam, certo? (Ok, quase todos! 🐧) Então, podemos criar um “contrato” para todos os animais que voam:

from abc import ABC, abstractmethod

class Voador(ABC):

  @abstractmethod
  def voar(self):
      pass

Aqui, estamos dizendo: “Se você é um Voador, precisa saber como voar!”.

Agora, vamos criar um pássaro que segue esse contrato:

class Pássaro(Voador):
  def __init__(self, nome):
      self.nome = nome

  def voar(self):
      print(f"Olha! {self.nome} está voando alto no céu!")

E é isso! Criamos uma interface usando uma classe abstrata e fizemos nosso Pássaro concordar com ele. Agora, sempre que vemos um Pássaro, sabemos que ele sabe voar! 🐦🌤️

Herança Múltipla

A herança múltipla é um conceito que permite que uma classe herde atributos e métodos de mais de uma classe. 🐍 Em Python, quando uma classe herda de múltiplas classes, ela tenta buscar o método ou atributo na primeira classe base listada e, se não encontrar, passa para a próxima, e assim por diante. A ordem na qual as classes-base são listadas é crucial para determinar a ordem de busca.

class Cao:
  def latir(self):
      print("Au au!")

class SuperHeroi:
  def voar(self):
      print("Estou voando alto no céu!")

Aqui temos duas classes, Cao e SuperHeroi. Cao tem um método latir e SuperHeroi tem um método voar. Agora criemos uma classe SuperCao que herda de Cao e SuperHeroi:

class SuperCao(Cao, SuperHeroi):
  def __init__(self, nome):
      self.nome = nome
supercao_1 = SuperCao("Krypto")
supercao_1.latir()
>>> Au au!

Funcionou! 🎉

Resolvendo Ambiguidades

Mas e se as classes-base tiverem métodos com o mesmo nome? 🤔 Isso pode criar uma situação de ambiguidade. Em Python, quando duas classes-base têm métodos com o mesmo nome, o método da primeira classe listada na herança é o que será usado. No entanto, se quisermos ter mais controle sobre qual método de qual classe base queremos acessar, podemos usar o método super().

O método super() nos permite acessar os métodos da classe base de maneira ordenada, respeitando a ordem de herança. Além disso, se quisermos especificar de qual classe base queremos chamar o método, podemos fazê-lo explicitamente.

class Cao:
  def latir(self):
      print("Au au!")

class SuperHeroi:
  def latir(self):
      print("Krypto diz: Estou pronto para salvar o dia!")

class SuperCao(Cao, SuperHeroi):
  def __init__(self, nome):
      self.nome = nome

  def latir(self):
      print(f"{self.nome} diz:")
      super().latir()  # Isso chamará o método latir() da classe Cao, pois é a primeira na lista de herança.

Se quiséssemos chamar o método latir() da classe SuperHeroi em vez da classe Cao, teríamos que ajustar a ordem de herança ou chamar o método explicitamente:

class Cao:
  def latir(self):
      print("Au au!")

class SuperHeroi:
  def latir(self):
      print("Krypto diz: Estou pronto para salvar o dia!")

class SuperCao(Cao, SuperHeroi):
  def __init__(self, nome):
      self.nome = nome

  def latir(self):
      print(f"{self.nome} diz:")
      SuperHeroi.latir(self)  # Chama o método latir() da classe SuperHeroi explicitamente.

Como você pode ver, ao chamar o método explicitamente, conseguimos acessar o método latir() da classe SuperHeroi, mesmo que Cao esteja listado primeiro na herança. Isso nos dá um controle mais granular sobre qual método queremos usar em situações de herança múltipla. 🐶📘

krypto = SuperCao("Krypto")
krypto.latir()
>>> Krypto diz:
>>> Krypto diz: Estou pronto para salvar o dia!

Conclusão

A Programação Orientada a Objetos (POO) é uma abordagem poderosa e flexível para desenvolver software. Ela nos permite modelar o mundo real de uma forma que é intuitiva e alinhada com a maneira como percebemos entidades e suas interações no dia a dia. Ao longo deste artigo, exploramos conceitos fundamentais da POO, como classes, objetos, herança, polimorfismo, e muito mais. Usamos o Krypto, nosso Supercão, como um exemplo prático para ilustrar esses conceitos e tornar a aprendizagem mais envolvente.

A herança múltipla, embora possa parecer complexa à primeira vista, é uma ferramenta valiosa quando usada com discernimento. Ela nos permite combinar comportamentos e características de várias classes, mas deve ser usada com cautela para evitar ambiguidades e conflitos.

Finalmente, é essencial lembrar que a POO é apenas uma das muitas abordagens em programação. Enquanto ela oferece muitos benefícios, especialmente em projetos grandes e complexos, há situações em que outras abordagens, como a programação funcional ou procedural, podem ser mais adequadas.

Obrigado por acompanhar este artigo! Espero que ele tenha proporcionado uma compreensão clara e aprofundada da Programação Orientada a Objetos em Python. Continue explorando, aprendendo e, acima de tudo, programando. O céu é o limite, especialmente se você tiver um Supercão que pode voar! 🐶🚀🌌

Compartilhe
Comentários (1)
Washington Pereira
Washington Pereira - 24/08/2023 13:26

Olá Stella !


Muito boa a publicação, ja programo em Python a um tempinho e pude ver que seu contéudo está bem completinho.


Meus Parabéns.