DEV Community

Eduardo Klosowski for A Caverna do Patocórnio

Posted on • Originally published at eduardoklosowski.github.io

Orientação a objetos de outra forma: Classes e objetos

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,
}
Enter fullscreen mode Exit fullscreen mode

Onde os dados poderiam ser acessados através da variável e do nome do dado desejado, como:

print(pessoa['nome'])  # Imprimindo João
Enter fullscreen mode Exit fullscreen mode

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,
    }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Porém isso não muda a forma de uso:

import pessoa

p1 = pessoa.nova('João', 'da Silva', 20)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (6)

Collapse
 
loboweissmann profile image
Henrique Lobo Weissmann (Kico)

"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?

Collapse
 
eduardoklosowski profile image
Eduardo Klosowski

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.

Collapse
 
loboweissmann profile image
Henrique Lobo Weissmann (Kico)

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.

Thread Thread
 
eduardoklosowski profile image
Eduardo Klosowski

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.

Thread Thread
 
loboweissmann profile image
Henrique Lobo Weissmann (Kico)

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?

Thread Thread
 
eduardoklosowski profile image
Eduardo Klosowski • Edited

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.