DEV Community

Cover image for Conceitos de POO em Python para Programadores C++98
Vinnícius Cedraz Ribeiro
Vinnícius Cedraz Ribeiro

Posted on • Edited on

Conceitos de POO em Python para Programadores C++98

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;
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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) {}
};
Enter fullscreen mode Exit fullscreen mode

Python

class Pet:
    def is_vaccinated(self):
        return True

class DomesticDog(Dog, Pet):
    pass
Enter fullscreen mode Exit fullscreen mode

Classe Abstrata

c++98

class Shape {
public:
    virtual ~Shape() {}
    virtual double area() const = 0;
};
Enter fullscreen mode Exit fullscreen mode

Python

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
Enter fullscreen mode Exit fullscreen mode

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 []
}
Enter fullscreen mode Exit fullscreen mode

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__
Enter fullscreen mode Exit fullscreen mode

Tópico Especial

O Problema da Herança Diamante

                             Animal

                          .    '    ,
                            _______
                       _  .`_|___|_`.  _
                   Pet     \ \   / /     WorkingAnimal
                            \ ' ' /
                             \ " /   
                              \./

                          DomesticDog
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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;
}
Enter fullscreen mode Exit fullscreen mode

Comportamento Esperado

Animal constructor
Pet constructor
WorkingAnimal constructor
DomesticDog constructor
Pet sound
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 chama make_sound, o compilador não sabe se deve usar Pet::make_sound ou WorkingAnimal::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:

  1. Ordem de Declaração: A MRO começa pela classe mais derivada e segue a ordem em que as classes base são declaradas.
  2. 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.
  3. Ordem de Resolução: Você pode verificar a MRO imprimindo o atributo __mro__:
print(DomesticDog.__mro__)
Enter fullscreen mode Exit fullscreen mode

Exemplo de Saída da MRO

(<class '__main__.DomesticDog'>, <class '__main__.Pet'>, <class
'__main__.WorkingAnimal'>, <class '__main__.Animal'>, <class 'object'>)
Enter fullscreen mode Exit fullscreen mode

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)