O que é Programação Orientada a Objetos


Programação orientada a objetos surgiu como uma alternativa ao paradigma de programação dominante até então, a chamada programação procedural (ou programação estruturada). Em programação procedural, o programa é uma sequência de passos: faça isso, depois faça aquilo, e assim por diante. Esse paradigma de programação funciona bem em muitos casos, mas apresenta desvantagens à medida em que programas crescem.

Imagine, por exemplo, que você está escrevendo um programa para exibir informações sobre formas geométricas na tela do computador. A princípio, seu programa precisa lidar com dois tipos de formas: círculos e quadrados, que estão armazenados em uma lista contendo todas as formas geométricas a serem exibidas na tela. Uma maneira de escrever um programa para realizar esta tarefa seria o mostrado abaixo. A princípio, iremos focar em exibir a área de cada forma geométrica.

for forma_geom in formas_geometricas:
    if circulo(forma_geom):
        area_circulo()
    elif quadrado(forma_geom):
        area_quadrado()

Agora suponha que seu programa precise ser estendido de modo a ser capaz de exibir outros tipos de formas geométricas como triângulos, retangulos e elipses. A maneira natural de alterar o código acima para fazer isso seria como mostrado no exemplo a seguir.

for forma_geom in formas_geometricas:
    if circulo(forma_geom):
        area_circulo()
    elif quadrado(forma_geom):
        area_quadrado()
    elif triangulo(forma_geom):
        area_triangulo()
    elif retangulo(forma_geom):
        area_retangulo()
    elif elipse(forma_geom):
        area_elipse()

O código acima parece razoável, mas note um problema. Toda vez que o programa tem que exibir um novo tipo de forma geométrica, precisamos fazer duas coisas: criar a funcionalidade para exibir a nova forma geométrica, e modificar o programa principal (que percorre a lista de formas e as exibe na tela), alterando os comandos if e elif para adicionar uma nova forma.

Idealmente, ao adicionar uma nova forma geométrica, deveríamos precisar somente criar a funcionalidade para exibir essa nova forma. O programa principal, que desenha as várias formas na tela, não deveria precisar ser modificado. Como fazer isso?

Perceba que o foco do programa acima é procedural: ele verifica cada tipo de forma geométrica para então decidir qual deverá ser exibida e invoca a função correspondente para exibir aquele tipo de forma geométrica. Em outras palavras, a lógica do programa é toda atrelada ao procedimento de selecionar e exibir uma dada forma geométrica.

No paradigma de programação orientada a objetos (OOP, da sigla em inglês), programas são coleções de objetos e a lógica de um programa passa a ser mais atrelada aos objetos deste programa. Em outras palavras, em OOP a inteligência (a lógica) dos programas está embutida primariamente nos objetos.

Assim, ao implementarmos nosso exemplo anterior usando o paradigma de OOP, cada forma geométrica “saberia” retornar sua área para ser impressa na tela do computador. Como resultado, o programa principal seria simplificado e se tornaria mais ou menos como mostrado abaixo.

for forma_geom in formas_geometricas:
    forma_geom.area()

Com isso, para adicionar uma nova forma geométrica ao programa, só precisamos implementar o método que calcula a área da forma geométrica. O programa principal não precisa ser alterado.

Essa vangatagem da Programação Orientada a Objetos pode parecer pequena à primeira vista, mas à medida em que nossos programa se tornam maiores e mais complexos, OOP facilita muito as tarefas de manter e estender esses programas.

Anteriormente, dissemos que em OOP a lógica é embutida nos objetos do programa e que programas são coleções de objetos que interagem entre si. Essas declarações (assim como o próprio nome “Programação Orientada a Objetos”) sugere que objetos são o centro deste paradigma de programação. Logo, precisamos entender o que são objetos, como criá-los e manipulá-los de forma correta.

Objetos e Classes

Em OOP, um objeto é uma forma de armazenarmos um estado e comportamentos associados a esse estado. Objetos possuem um tipo, uma representação interna (campos), e provêm uma interface (métodos). As informações (campos) de um objeto são chamadas de atributos do objeto, e as operações realizadas sobre o objeto são chamadas métodos do objeto/classe. Essa nomenclatura toda ficará mais clara à medida em que apresentarmos exemplos concretos, o que faremos a seguir.

Imagine, por exemplo, que tenhamos um objeto que representa um carro. O estado do objeto consistiria de coisas como marca, modelo, chassi, ano, etc. E o comportamento seria as funcionalidades que um carro normalmente possui, como ligar, desligar, acelerar, frear, etc.

No nosso exemplo de formas geométricas, o estado de uma forma são as informações associadas a ela (o valor raio, no caso de círculos; o tamanho do lado, no caso de quadrados; e assim por diante), ao passo que o comportamento seria a função que calcula a área da forma geométrica.

Uma outra maneira de entendermos objetos é como instâncias de uma classe.

Em OOP, uma classe é uma declaração dos estados que um certo tipo de objeto armazena e uma implementação dos comportamentos associados a esse tipo de objeto. Mesmo sem saber, você já deve ter utilizado classes antes, visto que elas estão por toda parte em Python. Por exemplo, quando você invoca o método reverse() para inverter os elementos de uma lista, está invocando um método de uma classe da biblioteca padrão Python, a classe list. O método invocado reverse, altera o estado do objeto em que opera, invertendo os elementos da lista.

Como criar uma classe?

No caso das formas geométricas, podemos ter uma classe que representa um círculo, na qual o estado é o valor do raio do círculo e como comportamento temos a função (método, no linguajar de OOP) area() e a função imprime(), criada por conveniência. Em Python, podemos criar esta classe assim.

class Circulo:
    def __init__(self, raio):
        self.raio = raio

    def area(self):
        pi = 3.141592653
        return pi * (self.raio ** 2)

    def imprime(self):
        print(f"Círculo de raio {self.raio}")

Existem algumas convenções seguidas no código acima que são importantes:

  • Nomes de classes não pertencentes à biblioteca padrão de Python começam com letra maiúscula, por convenção.
  • A função __init__ é invocada todas as vezes que um objeto da classe é criado. Esse tipo de função é chamada de construtor.
  • A variável raio é o estado armazenado em todo objeto (instância) do tipo Circulo que for criado em nosso programa. O estado de um objeto também é chamado de variáveis da classe, campos da classe ou atributos da classe.
  • As funções area() e imprime() contêm o comportamento de objetos do tipo Circulo, ou seja, contêm as operações que objetos desse tipo sabem realizar. O comportamento de objetos de um determinado tipo também recebe o nome de métodos da classe. Em linhas gerais, um método é uma função que funciona somente dentro da classe, isto é, somente quando invocado por objetos da classe, seguindo uma sintaxe específica: objeto.metodo. Além disso, ao invocar um método, Python sempre passa como primeiro parâmetro para o método o objeto que está invocando o método. Para indicar isso, a convenção é declarar self como primeiro parâmetro de todo método, como fizemos ao declarar os métodos area() e imprime.
  • O que a declaração da classe Circulo acima nos diz é que objetos do tipo Circulo terão um raio, definido no momento da criação do objeto, um método que calcula a área do círculo, e outro que imprime as informações do círculo.

A pergunta que costuma surgir neste momento é: Como criar objetos da classe Circulo?

Como criar objetos de uma classe?

Lembre-se que dissemos que um objeto é uma instância de uma classe. Logo, para criar um objeto da classe Circulo precisamos criar uma instância da classe, provendo um valor para o raio do círculo. O exemplo abaixo mostra como fazer isso.

obj_circulo = Circulo(3)
obj_circulo.imprime()
Círculo de raio 3

Note que:

  • O código obj_circulo = Circulo(3) Cria um objeto da classe Circulo cujo raio mede três unidades.
  • O código obj_circulo.imprime() Invoca a função imprime() do objeto recém-criado.

No código que declara a classe Circulo, usamos a palavra-chave self várias vezes. Porém, ao criar e manipular objetos da classe Circulo não tivemos que usar essa palavra-chave. Afinal, o que significa a palavra-chave self e para quê ela é utilizada?

Ao criar uma classe, estamos criando um estado e comportamento associado a objetos desta classe. Em Python, a forma de dizermos que um estado ou comportamento está associado à classe em questão é por meio da palavra-chave self. Assim, a declaração objeto.medoto(self, params) seria o equivalente de dizermos metodo(objeto, params). Em outras palavras, o self nos diz que o estado ou método está associado ao objeto sendo manipulado.

Conclusão

Agora que já entendemos os conceitos básicos de OOP em Python, podemos avançar para conceitos mais complexos e refinados. Nas próximas seções, cobriremos três conceitos que são princípios-chave em OOP:

  • Herança: como estender a funcionalidade de classes existentes.
  • Encapsulamento: como criar classes de modo a esconder certos tipos de informação e expor somente aquilo que é essencial para o uso da classe.
  • Polimorfismo: como criar classes com uma interface unificada, classes que se comportam como as classes já existentes em Python.