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 tipoCirculo
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()
eimprime()
contêm o comportamento de objetos do tipoCirculo
, 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 é declararself
como primeiro parâmetro de todo método, como fizemos ao declarar os métodosarea()
eimprime
. - O que a declaração da classe
Circulo
acima nos diz é que objetos do tipoCirculo
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 classeCirculo
cujo raio mede três unidades. - O código
obj_circulo.imprime()
Invoca a funçãoimprime()
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.