Aqui vai uma demonstração de alguns conceitos de POO em Python para um programador c++98.
A primeira parte desse artigo se consiste de dois grandes blocos de código comentado. Eles são duas implementaçẽs de uma classe "Animal" (em c++ e depois em Python) que devem se comportar da mesma forma. Assumindo que você já conhece c++, apenas observar as classes uma em seguida da outra já será o suficiente para entender a maior parte daquilo que está escrito em Python.
Mesmo assim, teremos blocos menores onde serão explicados alguns conceitos chave de forma isolada: Herança, Herança Múltipla e Classe Abstrata.
Além disso, incluí um conceito a mais para os curiosos (fica, vai ter bolo).
c++98
#include <iostream>
#include <string>
#include <sstream>
class Animal {
public:
static int species_count;
Animal(const std::string& name) : name(name), _age(0), __id(++id_counter) { // construtor
++species_count;
}
~Animal() { // destrutor
--species_count;
}
virtual void make_sound() = 0; // Método não implementável na classe base (virtual/abstrato)
static std::string get_kingdom() {
return "Animalia";
}
// static methods podem ser utilizados sem instanciar uma classe e têm
// acesso às propriedades estáticas da classe:
static int get_species_count() {
return species_count;
}
// getter:
int get_age() const {
return _age;
}
// setter:
void set_age(int age) {
if (age >= 0) {
_age = age;
}
}
// Implementação dos métodos especiais que vimos em python:
std::string to_string() const {
return "Animal named " + name;
}
std::string repr() const {
std::ostringstream oss;
oss << "Animal(name='" << name << "', age=" << _age << ", id=" << __id << ")";
return oss.str();
}
bool operator==(const Animal& other) const {
return name == other.name;
}
// Sobrecarga do operador []
std::string operator[](const std::string& key) const {
if (key == "name") {
return name;
}
throw std::out_of_range("Invalid key");
}
// Método isinstance
template <typename T>
bool isinstance() const {
return dynamic_cast<const T*>(this) != nullptr;
}
protected:
std::string name;
int _age;
private:
int __id;
static int id_counter;
};
// variáveis estáticas de classe são compartilhadas por todas as instâncias e
// precisam ser inicializadas fora da declaração da classe.
int Animal::species_count = 0;
int Animal::id_counter = 0;
Python
# Privado por convenção: _underscore_simples
# "Realmente privado": __underscore_duplo (name mangling)
# Público: sem underscore
from abc import abstractmethod
class Animal(ABC):
# Em python, variáveis declaradas no escopo da classe e não dentro de um
# método específico, são automaticamente compartilhadas por todas instâncias.
species_count = 0 # além disso, elas podem ser inicializadas diretamente dentro da classe.
# Construtor
def __init__(self, name):
# Variáveis de instância
self.name = name # público
self._age = 0 # protegido por convenção
self.__id = id(self) # privado (mas você consegue acessar com name mangling)
Animal.species_count += 1
# Destrutor
def __del__(self):
Animal.species_count -= 1
@abstractmethod
def make_sound(self):
pass # apenas classes filhas o implementarão
# Método estático (não precisa da instância para ser utilizado, nem utiliza seus atributos)
@staticmethod
def get_kingdom():
return "Animalia"
# Método de classe (recebe a classe como primeiro argumento, pode acessar atributos da classe)
@classmethod
def get_species_count(cls):
return cls.species_count
@property # Forma de criar um getter que pode ser acessado como se fosse uma variáviel comum
def age(self):
return self._age
# Decorador de propriedade (setter)
@age.setter
def age(self, value):
if value >= 0:
self._age = value
# Métodos especiais (sobrecarga de operadores)
def __str__(self): # Como toString() - para string legível
return f"Animal named {self.name}"
def __repr__(self): # Para debugging
return f"Animal(name='{self.name}')"
def __eq__(self, other): # Operador de comparação ==
return isinstance(other, Animal) and self.name == other.name
def __len__(self): # Função len()
return self._age
def __getitem__(self, key): # Operador de acesso []
if key == 'name':
return self.name
raise KeyError(key)
Observações sobre os exemplos acima:
1 - Decorators:
- Se
@abstractmethod
é a primeira vez que você viu um arroba em python, saiba que é a maneira pela qual nós criamos um decorator. Se essa é a primeira vez que você ouve falar de um decorator, saiba apenas o necessário por enquanto: ele modifica o comportamento de uma função. Pra quem quiser saber mais sobre decorators, vou deixar um link no final do artigo.
2 - Class Methods vs Static Methods:
- Note que em Python existe a distinção entre @classmethod e @staticmethod mas em c++ temos apenas o static method.
Agora alguns conceitos chave de forma isolada
Herança
c++98
class Dog : public Animal {
public:
Dog(const std::string& name, const std::string& breed) : Animal(name), breed(breed) {}
void make_sound() override {
std::cout << "Woof!" << std::endl;
}
private:
std::string breed;
};
Python
class Dog(Animal):
def __init__(self, name, breed):
# Chama o construtor da classe pai
super().__init__(name)
self.breed = breed
# Sobrescreve o método da classe pai
def make_sound(self):
return "Woof!"
Herança Múltipla
c++98
class Pet {
public:
bool is_vaccinated() const {
return true;
}
};
class DomesticDog : public Dog, public Pet {
public:
DomesticDog(const std::string& name, const std::string& breed) : Dog(name, breed) {}
};
Python
class Pet:
def is_vaccinated(self):
return True
class DomesticDog(Dog, Pet):
pass
Classe Abstrata
c++98
class Shape {
public:
virtual ~Shape() {}
virtual double area() const = 0;
};
Python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
Instanciando as Classes
c++98
int main() {
// Cria objetos
Dog dog("Rex", "Golden Retriever");
// Acessa atributos
std::cout << dog.name << std::endl; // Público
std::cout << dog.get_age() << std::endl; // Protegido (ainda acessível)
std::cout << dog.__id << std::endl; // Isso falhará (privado)
// Propriedades
dog.set_age(5); // Usa setter
std::cout << dog.get_age() << std::endl; // Usa getter
// Métodos estáticos e de classe
std::cout << Animal::get_kingdom() << std::endl;
std::cout << Animal::get_species_count() << std::endl;
// Equivalente aos "métodos especiais":
// Verifica herança
if (dog.isinstance<Animal>()) {
std::cout << "dog é uma instância de Animal" << std::endl;
}
std::cout << dog.to_string() << std::endl; // Usa to_string
std::cout << dog.repr() << std::endl; // Usa repr
std::cout << dog["name"] << std::endl; // Usa operador []
}
Python
if __name__ == "__main__":
# Cria objetos
dog = Dog("Rex", "Golden Retriever")
# Acessa atributos
print(dog.name) # Público
print(dog._age) # Protegido (ainda acessível)
# print(dog.__id) # Isso falhará
print(dog._Animal__id) # Isso funciona (acessando attribute privado com name mangling)
# Propriedades
dog.age = 5 # Usa setter automaticamente
print(dog.age) # Usa getter automaticamente
# Métodos estáticos e de classe
print(Animal.get_kingdom())
print(Animal.get_species_count())
# Verifica herança
print(isinstance(dog, Animal)) # True
print(issubclass(Dog, Animal)) # True
# Métodos especiais em ação
print(str(dog)) # Usa __str__
print(repr(dog)) # Usa __repr__
print(len(dog)) # Usa __len__
print(dog['name']) # Usa __getitem__
Tópico Especial
O Problema da Herança Diamante
Animal
. ' ,
_______
_ .`_|___|_`. _
Pet \ \ / / WorkingAnimal
\ ' ' /
\ " /
\./
DomesticDog
A herança diamante ocorre quando uma classe herda de duas classes que, por sua vez, herdam de uma classe base comum. Em C++, isso pode causar vários problemas:
- Ambiguidade: Métodos e atributos da classe base comum podem se tornar ambíguos, pois o compilador não sabe qual caminho seguir para acessar a classe base.
- Duplicação de Dados: Cada classe derivada pode ter sua própria cópia dos membros da classe base comum, levando a duplicação de dados e múltiplas chamadas aos construtores da classe base.
Exemplo de Herança Diamante em c++98
class Animal {
public:
Animal() {
std::cout << "Animal constructor" << std::endl;
}
virtual void make_sound() {
std::cout << "Some generic animal sound" << std::endl;
}
};
class Pet : public Animal {
public:
Pet() : Animal() {
std::cout << "Pet constructor" << std::endl;
}
void make_sound() override {
std::cout << "Pet sound" << std::endl;
}
};
class WorkingAnimal : public Animal {
public:
WorkingAnimal() : Animal() {
std::cout << "WorkingAnimal constructor" << std::endl;
}
void make_sound() override {
std::cout << "Working animal sound" << std::endl;
}
};
class DomesticDog : public Pet, public WorkingAnimal {
public:
DomesticDog() : Animal(), Pet(), WorkingAnimal() {
std::cout << "DomesticDog constructor" << std::endl;
}
void make_sound() override {
Pet::make_sound(); // Ou WorkingAnimal::make_sound(), dependendo da preferência do programador
}
};
int main() {
DomesticDog dog;
dog.make_sound();
return 0;
}
Comportamento Esperado
Animal constructor
Pet constructor
WorkingAnimal constructor
DomesticDog constructor
Pet sound
Neste exemplo, DomesticDog
herda de Pet
e WorkingAnimal
, ambos herdando de Animal
. Isso cria uma herança diamante. A herança virtual é usada para evitar duplicação de dados e ambiguidade.
Como Python Previne Automaticamente a Herança Diamante
Python usa a Ordem de Resolução de Métodos (MRO) com linearização C3 para resolver automaticamente os problemas de herança diamante. A MRO determina a ordem em que as classes são verificadas quando se procura por um método ou atributo.
Exemplo de Herança Diamante em Python
class Animal:
def make_sound(self):
print("Some generic animal sound")
class Pet(Animal):
def make_sound(self):
print("Pet sound")
class WorkingAnimal(Animal):
def make_sound(self):
print("Working animal sound")
class DomesticDog(Pet, WorkingAnimal):
pass
dog = DomesticDog()
dog.make_sound() # "Pet sound"
A MRO em Python garante que DomesticDog
herde corretamente de Pet
e
WorkingAnimal
, e que Animal
seja resolvida antes de object
.
Diferença na Ordem de Resolução de Métodos em c++ e Python
c++:
-
Ordem dos Construtores: Em c++, a ordem dos construtores é determinada pela ordem em que as classes base são listadas na declaração da classe derivada. Cada classe base direta chama o construtor da classe base comum (
Animal
), resultando em múltiplas chamadas ao construtor da classe base comum. -
Ambiguidade de Métodos: Quando
DomesticDog
chamamake_sound
, o compilador não sabe se deve usarPet::make_sound
ouWorkingAnimal::make_sound
. O programador precisa especificar explicitamente qual método chamar para resolver a ambiguidade.
Python:
-
MRO com Linearização C3: Python usa a MRO com linearização C3 para garantir que cada classe apareça antes de suas superclasses e que a ordem de herança seja mantida. A classe base comum (
Animal
) é chamada apenas uma vez, evitando duplicação. - Resolução Automática de Métodos: A MRO em Python resolve automaticamente qual método chamar, seguindo a ordem definida pela linearização C3. Isso elimina a necessidade de o programador especificar explicitamente qual método chamar, evitando ambiguidades.
Explicação da MRO do Python:
- Ordem de Declaração: A MRO começa pela classe mais derivada e segue a ordem em que as classes base são declaradas.
- Linearização C3: É um algoritmo que garante que cada classe apareça antes de suas superclasses e que a ordem de herança seja mantida.
-
Ordem de Resolução: Você pode verificar a MRO imprimindo o atributo
__mro__
:
print(DomesticDog.__mro__)
Exemplo de Saída da MRO
(<class '__main__.DomesticDog'>, <class '__main__.Pet'>, <class
'__main__.WorkingAnimal'>, <class '__main__.Animal'>, <class 'object'>)
Essa saída mostra que Python primeiro procura em DomesticDog
, depois em
Pet
, seguido por WorkingAnimal
, Animal
, e finalmente na classe base
object
.
Obrigado por acompanhar este guia sobre POO em Python para programadores c++98!
Alguns links úteis (e outros nem tanto):
Decorators: https://realpython.com/primer-on-python-decorators/
C3 Linearization Algorithm: https://medium.com/@Unbalanced-Tree/mro-in-python-3-e2bcd2bd6851
Abstract Base Classes: https://docs.python.org/3/library/abc.html
Python Meme: https://br.pinterest.com/pin/619315386256580093/
Pet Sounds: https://www.youtube.com/watch?v=f9keMETFIbk
Top comments (0)