import rich
import dataclasses
import pandas as pd11 Introdução à programação orientada a objetos (OOP)
11.1 Introdução
Até este ponto do curso, trabalhamos principalmente com funções e estruturas de dados como listas, dicionários, arrays e dataframes. Essa abordagem já é suficiente para resolver uma grande variedade de problemas encontrados em aplicações empíricas na Economia.
No entanto, muitas das bibliotecas mais importantes do ecossistema Python utilizam um paradigma conhecido como programação orientada a objetos (Object-Oriented Programming, ou OOP). Na prática, objetos estão presentes em praticamente todos os programas escritos em Python. Um DataFrame do Pandas, um array do NumPy ou uma figura criada com Matplotlib são exemplos de objetos que armazenam informações e disponibilizam operações específicas para manipulá-las.
Por esse motivo, mesmo que você não pretenda desenvolver sistemas complexos ou trabalhar profissionalmente com engenharia de software, compreender os conceitos básicos de orientação a objetos é importante para utilizar Python de forma mais eficiente e entender melhor a estrutura das bibliotecas que empregamos em análise de dados. Neste capítulo faremos uma breve introdução ao tema. O objetivo não é explorar todos os recursos da programação orientada a objetos, mas compreender como criar classes simples, armazenar atributos e definir comportamentos associados a um objeto.
11.1.1 Pré-requisitos
Neste capítulo utilizaremos três bibliotecas adicionais. O pacote pandas será empregado para criar e manipular objetos do tipo DataFrame, permitindo ilustrar conceitos como atributos e métodos em uma estrutura de dados já familiar. O módulo dataclasses, disponível na biblioteca padrão do Python, será utilizado para simplificar a criação de classes cujos objetos servem principalmente para armazenar dados. Por fim, o pacote rich será utilizado apenas para melhorar a formatação e a visualização de alguns resultados no terminal, tornando os exemplos mais legíveis.
richpandasdataclasses
11.2 Por que estudar OOP?
Ao aprender Python, é comum ter a impressão de que programação orientada a objetos é um tópico avançado e distante das aplicações empíricas. Em parte, essa percepção é correta: muitas análises econômicas podem ser realizadas utilizando apenas funções e estruturas de dados tradicionais. Ainda assim, a orientação a objetos desempenha um papel importante no ecossistema Python. Bibliotecas amplamente utilizadas em ciência de dados foram construídas com base nesse paradigma. Quando executamos comandos como df.info() ou df.describe() estamos utilizando métodos associados a objetos específicos, no caso dataframes de Pandas.
A principal ideia da orientação a objetos é reunir, em uma única estrutura, tanto os dados quanto as operações relacionadas a esses dados. Em vez de armazenar informações em várias variáveis independentes e utilizar funções separadas para manipulá-las, podemos criar um objeto que encapsula essas informações e os comportamentos associados. Essa forma de organização se torna especialmente útil quando trabalhamos com entidades que possuem várias características e operações relacionadas. Por exemplo, podemos representar um município por meio de um objeto contendo informações como população, renda per capita e número de crimes, além de métodos capazes de calcular indicadores derivados dessas variáveis.
Embora não seja um tema central deste curso, uma compreensão básica desses conceitos ajudará você a interpretar documentações, utilizar bibliotecas modernas e compreender melhor como programas em Python são estruturados.
11.3 Objetos e suas características
Ao longo do curso utilizamos diversos tipos de objetos em Python, como números, textos, listas, arrays do NumPy e dataframes do Pandas. Mas, afinal, o que é um objeto? Embora esses comandos já façam parte da nossa rotina de programação, ainda não discutimos com detalhes o que exatamente é um objeto. De maneira simplificada, um objeto é uma entidade armazenada na memória do computador que reúne informações e comportamentos. Sempre que criamos uma variável em Python, estamos na verdade criando uma referência para algum objeto armazenado na memória.
Por trás dos bastidores, todo objeto possui algumas características fundamentais. Em particular, podemos pensar em um objeto como sendo composto por quatro elementos principais:
- um tipo, que determina sua categoria;
- uma identidade, que permite distingui-lo dos demais objetos existentes na memória;
- um conjunto de atributos, que armazenam informações sobre o objeto;
- um conjunto de métodos, que definem operações que podem ser realizadas sobre ele.
Esses elementos aparecem constantemente durante a programação, mesmo que implicitamente. Antes de aprendermos a criar novos tipos de objetos, porém, é preciso entender melhor o que cada uma dessas características representa.
11.3.1 Tipo e identidade
Você deve se lembrar do capítulo Capítulo 4 que no Python os tipos determinam quais informações podem ser armazenadas no objeto e quais operações podem ser realizadas sobre ele. Por exemplo, os números inteiros pertencem ao tipo int, enquanto textos pertencem ao tipo str. O tipo funciona como uma espécie de “categoria” do objeto. É ele que informa ao Python como aquele objeto deve ser tratado internamente.
x = 10
y = 'Danilo'
print(type(x))
print(type(y))<class 'int'>
<class 'str'>
O tipo também é importante porque determina como determinadas operações devem ser interpretadas pelo Python. Por exemplo, o operador + possui significados diferentes dependendo do tipo dos objetos envolvidos: quando aplicado a strings, ele realiza uma concatenação de textos, enquanto, quando aplicado a números, ele executa uma operação de adição. Como Python é uma linguagem de tipagem forte e dinâmica, cada objeto possui um tipo bem definido, mas esse tipo normalmente não precisa ser declarado explicitamente pelo usuário. Em vez disso, o próprio Python identifica e atribui automaticamente o tipo apropriado no momento em que o objeto é criado.
Além do tipo, todo objeto possui uma identidade única. Essa identidade corresponde ao endereço de memória utilizado pelo Python para localizar o objeto durante a execução do programa. Podemos visualizar essa identidade utilizando a função id().
print(id(x))
print(id(y))140729810068680
1741395842496
É importante distinguir, porém, entre identidade e conteúdo. Vejamos o caso das listas abaixo:
lista1 = [1, 2, 3]
lista2 = [1, 2, 3]
print(id(lista1))
print(id(lista2))1741317250112
1741317380416
Embora lista1 e lista2 possuam exatamente o mesmo conteúdo, elas são objetos diferentes armazenados em locais distintos da memória. Essa distinção entre conteúdo e identidade é importante porque dois objetos podem parecer iguais para o usuário, mas serem tratados como entidades independentes pelo Python.
Saber que um objeto possui um tipo e uma identidade nos ajuda a entender como o Python o representa internamente. No entanto, essas características não são as que normalmente utilizamos ao programar. Na prática, interagimos com objetos principalmente por meio de seus atributos e métodos, que definem as informações armazenadas e as operações disponíveis.
11.3.2 Atributos
Um atributo é simplesmente uma característica associada ao objeto. Dependendo do tipo do objeto, esses atributos podem guardar informações úteis sobre seu estado ou sua estrutura. Em Python, qualquer nome acessado após o operador ponto (.) é denominado um atributo do objeto que aparece à esquerda desse operador.
dados = pd.DataFrame({"nome": ["Ana", "Bruno", "Carlos"],"idade": [20, 25, 30]})
print(dados.shape)
print(dados.columns)
print(dados.index)(3, 2)
Index(['nome', 'idade'], dtype='object')
RangeIndex(start=0, stop=3, step=1)
Observe que não utilizamos parênteses após shape, columns ou index. Isso ocorre porque esses elementos armazenam informações sobre o objeto e não executam nenhuma ação. De forma geral, atributos representam características ou propriedades associadas a um objeto. No caso de um dataframe, os atributos podem informar o número de linhas e colunas, os nomes das variáveis ou o índice utilizado para identificar as observações.
11.3.3 Métodos
Esse aqui a gente já utilizou bastante ao longo do curso: métodos são funções associadas e aplicáveis a um objeto específico. Formalmente, eles correspondem a atributos que podem ser chamados como funções. É por isso que os métodos são acessados com a notação de ponto e seguidos por parênteses.
Tipicamente, métodos agem sobre os dados contidos no objeto ao qual pertencem ou combinam esses dados com outros dados.
s = 'Esse é um string'
# operação sobre os dados contidos no string 's'
print(s.upper())
# operação através da combinação do string 's' com o outro string
print(s.replace('Esse','Este'))ESSE É UM STRING
Este é um string
Já sabemos que o Python é uma linguagem multiparadigma, que funciona de forma procedural, imperativa ou mesmo sob a lógica da programação orientada a objetos. Porém, mesmo quando estamos utilizando uma sintaxe que não aquela associada a OOP, i.e., sem o uso explicito de atributos e métodos ligados a um objeto específico, por trás das cortinas o Python muitas vezes está fazendo dessa forma. Vejamos o exemplo abaixo, de substituição de um valor armazenado em uma lista simples.
lista1 = [1,2,3]
lista1[0] = 'primeiro elemento'
print(lista1)['primeiro elemento', 2, 3]
Utilizamos a lógica de indexação e o fato de listas serem objetos mutáveis para fazer a substituição do primeiro elemento. Para realizar essa operação não utilizamos nenhum tipo de método associado ao objeto lista, certo? Errado. Por de trás das cortinas, a atribuição de um novo valor a um elemento existente da lista através da lógica de indexação e da sintaxe de colchetes é apenas uma interface amigável para o uso do método __setitem__ em uma lista.
lista2 = [1,2,3]
lista2.__setitem__(0,'primeiro elemento')
print(lista2)['primeiro elemento', 2, 3]
O mesmo vale para quando utilizamos a função nativa len() para descobrir o número de elementos presentes em uma sequência, como uma lista. O uso dessa função é apenas uma interface amigável para a aplicação do método __len__().
# uso da função nativa
print(len(lista2))
# uso do método
print(lista2.__len__())3
3
O pacote rich traz funções bastante interessantes para que possamos entender um pouco mais de um objeto no próprio console do Python. É possível, por exemplo, extrair o tipo do dado e os métodos disponíveis para aquele objeto. Para o caso de uma lista como lista1 = [1,2,3], a função inspect nos permite inspecionar os métodos disponíveis para aplicação nesse tipo de objeto.
lista1 = [1,2,3]
rich.inspect(lista1, methods=True)╭───────────────────────────────────────── <class 'list'> ──────────────────────────────────────────╮ │ Built-in mutable sequence. │ │ │ │ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ │ [1, 2, 3] │ │ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ append = def append(object, /): Append object to the end of the list. │ │ clear = def clear(): Remove all items from list. │ │ copy = def copy(): Return a shallow copy of the list. │ │ count = def count(value, /): Return number of occurrences of value. │ │ extend = def extend(iterable, /): Extend list by appending elements from the iterable. │ │ index = def index(value, start=0, stop=9223372036854775807, /): Return first index of value. │ │ insert = def insert(index, object, /): Insert object before index. │ │ pop = def pop(index=-1, /): Remove and return item at index (default last). │ │ remove = def remove(value, /): Remove first occurrence of value. │ │ reverse = def reverse(): Reverse *IN PLACE*. │ │ sort = def sort(*, key=None, reverse=False): Sort the list in ascending order and return None. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
11.4 Classes
Até agora vimos que objetos possuem atributos e métodos. No entanto, todos os exemplos utilizados até aqui envolveram objetos já existentes na linguagem ou fornecidos por bibliotecas externas. Isso levanta uma questão natural: e se quisermos criar nossos próprios tipos de objetos?
Em muitos problemas econômicos trabalhamos com entidades que possuem diversas características associadas. Municípios possuem população, área territorial e renda per capita. Empresas possuem faturamento, número de empregados e setor de atividade. Famílias possuem renda, consumo e tamanho. Poderíamos armazenar essas informações utilizando dicionários ou múltiplas variáveis separadas. Entretanto, quando várias informações e operações estão relacionadas à mesma entidade, pode ser conveniente agrupá-las em uma única estrutura. É justamente esse o papel das classes.
Uma classe reúne, em uma única estrutura, um conjunto de dados e métodos responsáveis por manipular esses dados. Cada objeto criado a partir da classe possui seus próprios dados, mas compartilha os mesmos comportamentos definidos pela classe. Por exemplo, números inteiros pertencem à classe int, listas pertencem à classe list e dataframes pertencem à classe DataFrame.
11.4.1 Classes personalizadas
Suponha que desejamos representar municípios brasileiros em um programa utilizando informações do Censo Demográfico de 2022. Cada município possui características como nome, unidade da federação, área territorial e população. Uma possibilidade seria armazenar essas informações em dicionários ou variáveis separadas. No entanto, quando vários dados estão associados à mesma entidade, pode ser conveniente agrupá-los em uma única estrutura. Para isso, podemos criar uma classe que reúna essas informações como atributos dos objetos que serão criados.
Em Python, novas classes são definidas por meio da palavra-chave class. Além do nome da classe, dois elementos aparecem com frequência em praticamente todas as definições: o método __init__ e o parâmetro self.
O método
__init__é é executado automaticamente sempre que um novo objeto é criado. Sua principal função é inicializar os atributos do objeto, armazenando as informações que serão utilizadas posteriormente por outros métodos da classe.O parâmetro
selfrepresenta o próprio objeto que está sendo criado ou utilizado. Por convenção, ele deve aparecer como primeiro parâmetro de todos os métodos definidos na classe. É por meio deselfque podemos acessar e modificar os atributos associados a cada objeto individualmente.
class Municipio:
def __init__(self,nome,estado,area,populacao):
self.nome = nome
self.uf = estado
self.km2 = area
self.pop = populacaoAgora podemos criar municípios diferentes como objetos da classe Municipio e acessar seus atributos a partir da notação e sintaxe usual de OOP no Python.
dracena = Municipio('Dracena','SP',487.688,45474)
sp = Municipio('São Paulo','SP',1521.202,11451999)
print(type(dracena))<class '__main__.Municipio'>
print(dracena.nome)
print(dracena.km2)
print(sp.pop)Dracena
487.688
11451999
Interessante, não? Fica ainda mais interessante quando criamos nossos próprios métodos para além do método obrigatório __init__. Suponha que você queira criar um método que calcule a densidade populacional, dividindo a população pela área do município.
class Municipio:
def __init__(self,nome,estado,area,populacao):
self.nome = nome
self.uf = estado
self.km2 = area
self.pop = populacao
def densidade_pop(self):
return self.pop / self.km2 Como redefinimos a classe, é importante redefinir os objetos. Após isso, é possível aplicar o novo método densidade_pop para calcular a densidade populacional de ambos os municípios.
dracena = Municipio('Dracena','SP',487.688,45474)
sp = Municipio('São Paulo','SP',1521.202,11451999)
print(dracena.densidade_pop())
print(sp.densidade_pop())93.24404127228884
7528.256602344724
11.4.2 Dataclasses
Embora a definição anterior funcione perfeitamente, ela exige uma quantidade considerável de código repetitivo. Em aplicações reais, muitas classes servem apenas para armazenar dados e não realizam operações complexas. Como essa situação é muito comum, Python oferece o módulo dataclasses, que automatiza a criação de parte desse código. Com ele podemos definir os atributos de uma classe de forma mais compacta, sem precisar escrever explicitamente o método __init__. No caso da classe Municipio definida anteriormente, podemos utilizar a função dataclass e o decorador @dataclass na linha exatamente anterior à definição da classe para reescrever o código:
@dataclasses.dataclass
class Municipio:
nome: str
uf: str
km2: float
pop: int
def densidade_pop(self):
return self.pop / self.km2Observe que os atributos são definidos diretamente abaixo do nome da classe. A partir dessas informações, Python cria automaticamente o método construtor e outras funcionalidades úteis. Para classes simples que servem principalmente para armazenar dados, essa abordagem costuma produzir códigos mais curtos e legíveis. Nada muda, porém, quando da necessidade de definir novos métodos dentro da classe.
dracena = Municipio('Dracena','SP',487.688,45474)
print(dracena.nome)
print(dracena.densidade_pop())Dracena
93.24404127228884
11.5 Conclusão
Neste capítulo apresentamos os conceitos fundamentais da programação orientada a objetos em Python. Vimos que todo objeto possui um tipo, uma identidade, atributos e métodos, e que esses elementos estão presentes em diversos objetos que utilizamos ao longo do curso, como listas, arrays e dataframes.
Também introduzimos o conceito de classe, que permite criar novos tipos de objetos reunindo dados e comportamentos em uma única estrutura. Embora a orientação a objetos não seja o foco principal desta disciplina, compreender seus fundamentos ajuda a entender melhor o funcionamento do Python e das bibliotecas utilizadas em análise de dados.
11.6 Exercícios
Considere a base de dados original da PWT, disponível aqui. Baixe a versão em Excel e utilize a função
read_excelpara ler os dados disponíveis na abaDatano formato de um DataFrame do Pandas.- Verifique o tipo do objeto criado.
- Utilize pelo menos dois atributos para obter informações sobre o dataframe.
- Utilize pelo menos dois métodos para explorar os dados.
- Explique brevemente a diferença entre atributos e métodos com base nos exemplos utilizados.
Considere o código abaixo:
universidade = 'Universidade de São Paulo'- Utilize as funções
type()eid()para verificar o tipo e a identidade do objeto armazenado emuniversidade. - Liste três métodos disponíveis para objetos do tipo
str. - Utilize um desses métodos para transformar o texto em letras maiúsculas.
- Utilize as funções
X