Nas poucas e raríssimas lives que eu fiz na Twitch, surgiu a ideia de escrever sobre programação orientada a objetos em Python, principalmente por algumas diferenças de como ela foi implementada nessa linguagem. Aproveitando o tema, vou fazer uma série de postagens dando uma visão diferente sobre orientação a objetos. E nessa primeira postagem falarei sobre classes e objetos.
Usando um dicionário
Entretanto, antes de começar com orientação a objetos, gostaria de apresentar e discutir alguns exemplos sem utilizar esse paradigma de programação.
Pensando em um sistema que precise manipular dados de pessoas, é possível utilizar os dicionários do Python para agrupar os dados de uma pessoa em uma única variável, como no exemplo a baixo:
pessoa = {
'nome': 'João',
'sobrenome': 'da Silva',
'idade': 20,
}
Onde os dados poderiam ser acessados através da variável e do nome do dado desejado, como:
print(pessoa['nome']) # Imprimindo João
Assim, todos os dados de uma pessoa ficam agrupados em uma variável, o que facilita bastante a programação, visto que não é necessário criar uma variável para cada dado, e quando se manipula os dados de diferentes pessoas fica muito mais fácil identificar de qual pessoa aquele dado se refere, bastando utilizar variáveis diferentes.
Função para criar o dicionário
Apesar de prático, é necessário replicar essa estrutura de dicionário toda vez que se desejar utilizar os dados de uma nova pessoa. Para evitar a repetição de código, a criação desse dicionário pode ser feita dentro de uma função que pode ser colocada em um módulo pessoa
(arquivo, nesse caso com o nome de pessoa.py
):
# Arquivo: pessoa.py
def nova(nome, sobrenome, idade):
return {
'nome': nome,
'sobrenome': sobrenome,
'idade': idade,
}
E para criar o dicionário que representa uma pessoa, basta importar esse módulo (arquivo) e chamar a função nova
:
import pessoa
p1 = pessoa.nova('João', 'da Silva', 20)
p2 = pessoa.nova('Maria', 'dos Santos', 18)
Desta forma, garante-se que todos os dicionários representando pessoas terão os campos desejados e devidamente preenchidos.
Função com o dicionário
Também é possível criar algumas funções para executar operações com os dados desses dicionários, como pegar o nome completo da pessoa, trocar o seu sobrenome, ou fazer aniversário (o que aumentaria a idade da pessoa em um ano):
# Arquivo: pessoa.py
def nova(nome, sobrenome, idade):
... # Código abreviado
def nome_completo(pessoa):
return f"{pessoa['nome']} {pessoa['sobrenome']}"
def trocar_sobrenome(pessoa, sobrenome):
pessoa['sobrenome'] = sobrenome
def fazer_aniversario(pessoa):
pessoa['idade'] += 1
E sendo usado como:
import pessoa
p1 = pessoa.nova('João', 'da Silva', 20)
pessoa.trocar_sobrenome(p1, 'dos Santos')
print(pessoa.nome_completo(p1))
pessoa.fazer_aniversario(p1)
print(p1['idade'])
Nesse caso, pode-se observar que todas as funções aqui implementadas seguem o padrão de receber o dicionário que representa a pessoa como primeiro argumento, podendo ter outros argumentos ou não conforme a necessidade, acessando e alterando os valores desse dicionário.
Versão com orientação a objetos
Antes de entrar na versão orientada a objetos propriamente dita dos exemplos anteriores, vou fazer uma pequena alteração para facilitar o entendimento posterior. A função nova
será separada em duas partes, a primeira que criará um dicionário, e chamará uma segunda função (init
), que receberá esse dicionário como primeiro argumento (seguindo o padrão das demais funções) e criará sua estrutura com os devidos valores.
# Arquivo: pessoa.py
def init(pessoa, nome, sobrenome, idade):
pessoa['nome'] = nome
pessoa['sobrenome'] = sobrenome
pessoa['idade'] = idade
def nova(nome, sobrenome, idade):
pessoa = {}
init(pessoa, nome, sobrenome, idade)
return pessoa
... # Demais funções do arquivo
Porém isso não muda a forma de uso:
import pessoa
p1 = pessoa.nova('João', 'da Silva', 20)
Função para criar uma pessoa
A maioria das linguagens de programação que possuem o paradigma de programação orientado a objetos faz o uso de classes para definir a estrutura dos objetos. O Python também utiliza classes, que podem ser definidas com a palavra-chave class
seguidas de um nome para ela. E dentro dessa estrutura, podem ser definidas funções para manipular os objetos daquela classe, que em algumas linguagens também são chamadas de métodos (funções declaradas dentro do escopo uma classe).
Para converter o dicionário para uma classe, o primeiro passo é implementar uma função para criar a estrutura desejada. Essa função deve possui o nome __init__
, e é bastante similar a função init
do código anterior:
class Pessoa:
def __init__(self, nome, sobrenome, idade):
self.nome = nome
self.sobrenome = sobrenome
self.idade = idade
As diferenças são que agora o primeiro parâmetro se chama self
, que é um padrão utilizado no Python, e em vez de usar colchetes e aspas para acessar os dados, aqui basta utilizar o ponto e o nome do dado desejado (que aqui também pode ser chamado de atributo, visto que é uma variável do objeto). A função nova
implementada anteriormente não é necessária, a própria linguagem cria um objeto e passa ele como primeiro argumento para o __init__
. E assim para se criar um objeto da classe Pessoa
basta chamar a classe como se fosse uma função, ignorando o argumento self
e informando os demais, como se estivesse chamando a função __init__
diretamente:
p1 = Pessoa('João', 'da Silva', 20)
Nesse caso, como a própria classe cria um contexto diferente para as funções (escopo ou namespace), não está mais sendo utilizado arquivos diferentes, porém ainda é possível fazê-lo, sendo necessário apenas fazer o import
adequado. Mas para simplificação, tanto a declaração da classe, como a criação do objeto da classe Pessoa
podem ser feitas no mesmo arquivo, assim como os demais exemplos dessa postagem.
Outras funções
As demais funções feitas anteriormente para o dicionário também podem ser feitas na classe Pessoa
, seguindo as mesmas diferenças já apontadas anteriormente:
class Pessoa:
def __init__(self, nome, sobrenome, idade):
self.nome = nome
self.sobrenome = sobrenome
self.idade = idade
def nome_completo(self):
return f'{self.nome} {self.sobrenome}'
def trocar_sobrenome(self, sobrenome):
self.sobrenome = sobrenome
def fazer_aniversario(self):
self.idade += 1
Para se chamar essas funções, basta acessá-las através do contexto da classe, passando o objeto criado anteriormente como primeiro argumento:
p1 = Pessoa('João', 'dos Santos', 20)
Pessoa.trocar_sobrenome(p1, 'dos Santos')
print(Pessoa.nome_completo(p1))
Pessoa.fazer_aniversario(p1)
print(p1.idade)
Essa sintaxe é bastante semelhante a versão sem orientação a objetos implementada anteriormente. Porém quando se está utilizando objetos, é possível chamar essas funções com uma outra sintaxe, informando primeiro o objeto, seguido de ponto e o nome da função desejada, com a diferença de que não é mais necessário informar o objeto como primeiro argumento. Como a função foi chamada através de um objeto, o próprio Python se encarrega de passá-lo para o argumento self
, sendo necessário informar apenas os demais argumentos:
p1.trocar_sobrenome('dos Santos')
print(p1.nome_completo())
p1.fazer_aniversario()
print(p1.idade)
Existem algumas diferenças entre as duas sintaxes, porém isso será tratado posteriormente. Por enquanto a segunda sintaxe pode ser vista como um açúcar sintático da primeira, ou seja, uma forma mais rápida e fácil de fazer a mesma coisa que a primeira, e por isso sendo a recomendada.
Considerações
Como visto nos exemplos, programação orientada a objetos é uma técnica para juntar variáveis em uma mesma estrutura e facilitar a escrita de funções que seguem um determinado padrão, recebendo a estrutura como argumento, porém a sintaxe mais utilizada no Python para chamar as funções de um objeto (métodos) posiciona a variável que guarda a estrutura antes do nome da função, em vez do primeiro argumento.
No Python, o argumento da estrutura ou objeto (self
) aparece explicitamente como primeiro argumento da função, enquanto em outras linguagens essa variável pode receber outro nome (como this
) e não aparece explicitamente nos argumentos da função, embora essa variável tenha que ser criada dentro do contexto da função para permitir manipular o objeto.
Top comments (6)
"programação orientada a objetos é uma técnica para juntar variáveis em uma mesma estrutura e facilitar a escrita de funções que seguem um determinado padrão"
O que diferencia esta descrição da programação estruturada com C ou qualquer outra linguagem que tenha o conceito de registros/structs?
Vai além: a programação orientada a objetos também incluí os conceitos de herança, composição, polimorfismo... vai além.
O que você tem aí na prática é apenas o paradigma estruturado com alguma disciplina, não?
De fato, não existe nenhuma diferença do que foi apresentado com structs do C, por exemplo. E também é possível utilizar herança e polimorfismo em C puro com structs, o código do htop é uma prova disso (e palestra do autor falando sobre isso a baixo). O que muda é que da um pouco mais de trabalho do que se estivesse utilizando alguma linguagem que disponibiliza alguns desses recursos por padrão ou através de alguma palavra-chave. Eu ainda pretendo desenvolver alguns desses conceitos nas próximas postagens. Mas olhando pela sua perspectiva, sim, orientação a objetos é um código estruturado seguindo alguns padrões, e é possível implementar as funcionalidades de orientação a objetos em um código estruturado.
mas então... qual a vantagem? Seguindo sua linha de raciocínio, não seria mais interessante melhorarmos a forma como escrevemos nosso código de forma procedural ao invés de forçar uma OOP nele?
Muitos anos atrás lembro que compilaram uma lista com implementações de OOP nas mais variadas linguagens, até mesmo arquivos em lote (.bat) do Windows. Como uma curiosidade técnica ou mesmo exercício, é muito interessante, mas pro dia a dia, por adicionar complexidade aonde não deveria existir, é problema.
Nesse caso existe um trade-off entre desempenho e facilidade para escrever o código, já que alguns dos padrões usados para implementar algumas funcionalidades possuem algum custo computacional na sua execução. Porém as vezes é necessário pagar esse custo devido a característica do problema a que está sendo resolvido. Saber escolher quando esse custo deve ser pago, ou pode ser pago sem prejuízos é de grande valor para alguns sistemas. A ideia dessa série é mostrar um ponto de vista sobre orientação a objetos como algo que surge de determinados padrões de código, assim como existem outros padrões de código que resolvem certos problemas (como usar uma variável auxiliar para trocar o valor de variáveis, as funções filter, map e reduce para tratar iteráveis...), dando uma visão de como orientação a objetos é implementada e o que determinadas sintaxes descrevem, e se possível uma ideia de custo para implementar esses padrões ou como transitar entre um código estruturado e orientado a objetos.
mas o custo em desempenho neste caso chega a ser insignificante, não? Você tem algum material aí pra gente ver qual seria este custo?
Eu não sei exatamente o custo de todos os recursos, e também não conheço nenhum material que teria isso. Porém, considerando o caso de funções virtuais da apresentação do autor do htop, sim, esse custo é quase insignificante para um computador atual, porém pode fazer diferença em um hardware mais limitado, como um Arduino, ou em um servidor que execute a mesma função milhões ou bilhões de vezes em um curto período de tempo, onde uma pequena diferença é escalada várias vezes. Hoje se usa Python para fazer diversos scripts, que conhecidamente não tem o melhor desempenho (salve quando utiliza alguma lib implementada em C ou Fortran), então muitas vezes esse custo é pago sem que o usuário final perceba tanta diferença, e acredito que o custo do interpretador do Python seja muito maior do que a implementação de algumas funcionalidades de orientação a objetos. Rust é vendido como uma linguagem que só se paga o custo do que se usa, porém é necessário de um bom conhecimento de baixo nível para entender esses custos.