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

1

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

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more