Como sabemos, uma variável simples é um espaço na memória, um local que fica reservado para guardar
alguma coisa, como uma caixinha de papelão, vamos supor que esta caixinha tenha a etiqueta estado
, e dentro dela colocamos um valor como SP
(São Paulo), se tentarmos colocar outro valor (como RJ
), o SP sai e dá lugar ao RJ. Exemplo:
estado = "SP"
estado = "RJ" # SP sai e entra RJ
print(estado) # Exibe RJ
Para guardarmos vários valores usamos variáveis compostas (como uma sequência de caixinhas com o nome estados
), onde podemos colocar em cada uma delas um valor, com índices numerados a partir do 0 (como RJ, SP e CE), e ao acessar algo como estados[2] acessaremos o valor na caixinha
2 (no caso, CE), e podemos substituir esses valores também, isso no Python é o princípio de lista. Também temos os dicionários, onde podemos ter índices de nomes literais (como uma sequência de caixinhas com o nome aluno
e índices como nome
, turma
, nota
e ativo
. Tanto em listas quanto em dicionários podemos ter valores de vários tipos numa mesma lista ou dicionário. Exemplos:
estados = ["RJ", "SP", "CE"]
estados[1] = "RS" # Substitui SP por RS
print(estados[2]) # Exibe CE
aluno = {"nome": "José", "turma": 301, "nota": 8.5, "ativo": True}
print(aluno["nome"]) # Exibe José
O maior problema em tudo isso é a separação entre dados e funções. Variáveis, tanto simples como compostas, só guardam dados. Para usar esses mesmos dados em funcionalidades, precisaríamos criar funções separadas pra isso.
O ideal seria permitir que a variável execute funcionalidades internas, por isso que surgiu a programação orientada a objetos, que permite que a variável guarde funções também. De certa forma, um objeto é uma variável evoluída, que além de dados, guarda funcionalidades em métodos. Em outras palavras, objetos são variáveis que, além de guardar dados, podem fazer coisas com esses dados.
PS: No Python, qualquer variável é considerada um objeto, isso geralmente acontece em muitas linguagens orientadas a objetos, mas não em todas.
Em uma classe, devemos delimitar, por organização, as partes que colocamos os atributos, que geralmente vem primeiro, e depois os métodos. Para criar uma classe usamos a palavra class e por organização, deve ter a primeira letra maiúscula, com PascalCase permitido (como class MinhaClasse, e a tabulação é importante também, algo assim:
class MinhaClasse:
# Atributos
# Métodos
obj = MinhaClasse() # Criação do objeto (instância).
PS: Não confunda a instânciação de uma classe com a invocação de uma função, apesar dela também usar parênteses. Ela é a instanciação sim de um método, que é o construtor.
Crie um arquivo Python e coloque esse código:
# Declaração de classe:
class Gafanhoto:
# Método construtor, onde ficará nossos atributos
def __init__(self):
# Atributos de instância
self.nome = ""
self.idade = 0
# Métodos de instância, que manipularão os atributos
def aniversario(self):
self.idade = self.idade + 1
def mensagem(self):
return f"{self.nome} é Gafanhoto(a) e tem {self.idade} anos de idade."
# Declaração de objetos:
O self será substituído pelo objeto a ser criado, e ele também deve estar em todo método da classe, inclusive o construtor.
No caso acima, a classe é apenas um molde
para criar os objetos, mas eles ainda não foram criados. Abaixo do comentário, crie um objeto, fora de qualquer tabulação, assim:
# Declaração de objetos:
g1 = Gafanhoto()
print(g1.mensagem())
No caso acima, a variável g1
é um objeto que foi instanciado da classe Gafanhoto (que chamará automaticamente o método __init__, que é o construtor).
Para chamar os atributos e métodos de um objeto, usamos o ponto, e os atributos não usam parênteses, apenas os métodos (sem o self, nesse caso).
Claro, que como não colocamos nada nos atributos dele, ele vai mostrar uma string vazia pro nome e 0 anos de idade. Esses foram os valores iniciados dentro do construtor. Para colocar dados no objeto, fazemos assim, de forma básica:
# Declaração de objetos:
g1 = Gafanhoto()
g1.nome = "Maria"
g1.idade = 17
print(g1.mensagem())
Podemos utilizar os métodos assim:
# Declaração de objetos:
g1 = Gafanhoto()
g1.nome = "Maria"
g1.idade = 17
g1.aniversario() # Adicionará 1 na idade
print(g1.mensagem())
Podemos criar outros objetos, onde um não terá vínculo com o outro, apesar de vierem da mesma classe:
# Declaração de objetos:
g1 = Gafanhoto()
g1.nome = "Maria"
g1.idade = 17
g1.aniversario()
print(g1.mensagem())
g2 = Gafanhoto()
g2.nome = "Mauro"
g2.idade = 53
print(g2.mensagem())
No caso acima, os atributos e métodos serão manipulados apenas nos objetos que os chamaram, onde um não interfere no outro. Por isso que na classe têm o self, que significa si mesmo
, ou seja, o próprio objeto que o chama.
PS: Podemos criar as classes em módulos separados também.
Vamos salvar o código anterior num novo arquivo Python, e vamos melhorar esse código.
Uma das melhorias vai ser usar métodos para manipular os atributos, pois o ideal é utilizar eles, ao invés de manipular os atributos diretamente.
A primeira melhoria é no método construtor da classe Gafanhoto (__init__), no qual passaremos parâmetros para definir os atributos automaticamente, ao criar o objeto, assim:
def __init__(self, n, i):
# Atributos de instância
self.nome = n
self.idade = i
Nesse caso, podemos passar diretamente os atributos a serem configurados na criação do objeto, assim:
g1 = Gafanhoto("Maria", 17)
g1.aniversario()
print(g1.mensagem())
g2 = Gafanhoto("Mauro", 53)
print(g2.mensagem())
Com isso, ele criará os objetos da mesma forma, mas com menos linhas e mais segurança.
No caso acima, os parâmetros no construtor são obrigatório, mas eles podem ser opcionais também, pra isso, altere o construtor assim:
def __init__(self, n = "vazio", i = 0):
# Atributos de instância
self.nome = n
self.idade = i
Dessa forma, caso não passe valores, ele receberá os valores predefinido no método construtor (no caso, a string vazio
pra n
e 0 para i
.
Podemos ver que, a alteração na classe já atualiza as características de todos os objetos oriundos do mesmo.
Os parâmetros do construtor pode ter os mesmo nomes dos atributos da classe, assim:
def __init__(self, nome = "vazio", idade = 0):
# Atributos de instância
self.nome = nome
self.idade = idade
No caso acima, o nome
e idade
dos parâmetros estão sozinhos
, e não devem ser confundidos com o nome e idade atributos da classe (estes contém o self
antes).
PS: Para descobrir o nome da classe, podemos fazer isso:
print(int)
E para ver a documentação da classe especificada, basta colocar __doc__ na frente dela:
print(int.__doc__)
No entanto, isso só vale pra classes nativas do Python, por exemplo, int, str, tuple, list, e outras. Nos objetos e classes criados por nós (como a Gafanhoto), ele retornará None
, dizendo que não tem manual, que deverá ser criados por nós mesmos. Para isso, basta colocar entre três aspas o código, nas primeiras linhas da classe, antes do restante do código, como se fazia pra documentar as funções, assim:
class Gafanhoto:
"""
Essa classe cria um Gafanhoto com nome e idade
Para criar uma nova pessoa, use:
variavel = Gafanhoto(nome, idade)
"""
Ao mostrar um objeto puro, ele retorna o nome da classe e o endereço de memória dele, assim:
print(g1)
E ele mostrará algo tipo assim:
<__main__.Gafanhoto object at 0x00000291C1876E40>
Podemos personalizar a mensagem sobrescrevendo o método padrão das classes __str__, assim:
def __str__(self):
return "Vou te mostrar uma coisa."
No caso, por padrão, o método __str__ mostra o nome e o endereço do objeto, mas no código acima sobrescrevemos o método e escrevemos a frase especificada acima. Aí, ao executar print(g1) novamente, ele exibirá essa frase escrita. Sabendo isso, podemos colocar os dados do objeto nesse mesmo método, pra serem exibidos por padrão:
def __str__(self):
return f"{self.nome} é Gafanhoto(a) e tem {self.idade} anos de idade."
Assim, podemos remover tudo referente ao método mensagem, tanto na classe quanto nos objetos, e deixar o código assim:
# Declaração de classe:
class Gafanhoto:
"""
Essa classe cria um Gafanhoto com nome e idade
Para criar uma nova pessoa, use:
variavel = Gafanhoto(nome, idade)
"""
# Método Construtor, onde ficará nossos atributos
def __init__(self, nome = "vazio", idade = 0):
# Atributos de instância
self.nome = nome
self.idade = idade
# Métodos de Instância
def aniversario(self):
self.idade = self.idade + 1
# Métodos Sobrescritos
def __str__(self):
return f"{self.nome} é Gafanhoto(a) e tem {self.idade} anos de idade."
# Declaração de objetos:
g1 = Gafanhoto("Maria", 17)
g1.aniversario()
print(g1)
g2 = Gafanhoto("Mauro", 53)
print(g2)
g3 = Gafanhoto()
print(g3)
Podemos mostrar os atributos do objeto em um dicionário, usando o atributo __dict__, assim:
print(g1.__dict__)
PS: Podemos usar o método __getstate__(), que funciona da mesma forma, só que este último é um método e pode ser sobrescrito na classe, assim:
def __getstate__(self):
return f"Estado:\n\nnome = {self.nome}\nidade = {self.idade}"
E assim, isso é adicionado em todos os objetos oriundos dessa classe:
# Declaração de objetos:
g1 = Gafanhoto("Maria", 17)
g1.aniversario()
print(g1.__getstate__())
g2 = Gafanhoto("Mauro", 53)
print(g2.__getstate__())
g3 = Gafanhoto()
print(g3.__getstate__())
Para saber o nome da classe da qual um objeto se origina, coloque o atributo __class__, assim:
print(g1.__class__)
Para exemplificar o que foi aprendido até agora, vamso criar uma classe simulando uma conta bancária, para isso, crie um novo arquivo para exercício e faça assim:
class ContaBancaria:
"""
Cria uma conta bancária e permite fazer saques e depósitos
"""
# Construtor
def __init__(self, id, nome, saldo = 0): # Apenas saldo é opcional, os outros são obrigatórios
self.id = id
self.titular = nome
self.saldo = saldo
print(f"Conta {self.id} criada com sucesso. Saldo atual de R${saldo:.2f}.")
c1 = ContaBancaria(112, "Gustavo", 3000)
print(c1)
No caso acima, ele exibirá a classe e o endereço de memória do objeto, para ele exibir os dados do mesmo, sobrescreva na classe o método __str__ assim:
def __str__(self):
return f"A conta {self.id} de {self.titular} tem R${self.saldo:.2f} de saldo."
Podemos exibir o __doc__ assim:
print(c1.__doc__)
Agora, defina esses métodos de instância na classe, que por enquanto ficarão com o bloco vazio (que pro Python, deverão ter dentro apenas a palavra pass), assim:
# Métodos de Instância:
def depositar(self, valor):
pass
def sacar(self, valor):
pass
Aí, pra chamar os métodos, fazemos assim:
c1 = ContaBancaria(112, "Gustavo", 3000)
c1.depositar(500)
c1.sacar(2000)
print(c1)
Só que, claro, ele não fará nada, para ele depositar o valor, fazemos assim no método depositar e sacar:
def depositar(self, valor):
self.saldo += valor
print(f"Depósito de R${valor:.2f} autorizado na conta {self.id}.")
def sacar(self, valor):
self.saldo -= valor
print(f"Saque de R${valor:.2f} autorizado na conta {self.id}.")
Claro que isso é um código básico, ainda necessita várias melhorias, um exemplo é de sacar mais dinheiro do que tem na conta, por exemplo:
c1 = ContaBancaria(112, "Gustavo", 3000)
c1.depositar(500)
c1.sacar(2_000_000) # É o mesmo que 2000000
print(c1)
No caso acima, ele sacará e ficará com um saldo negativo, mas não é assim que as contas funcionam na prática. Para isso, deveremos fazer métodos mais robustos. Veja como ficaria o método sacar, com a correção:
def sacar(self, valor):
if valor > self.saldo:
print(f"Saque de R${valor:.2f} negado na conta {self.id}. Saldo insuficiente.")
else:
self.saldo -= valor
print(f"Saque de R${valor:.2f} autorizado na conta {self.id}.")
PS: Podemos fazer outras melhorias, como colocar cores, por exemplo.
Na classe ContaBancaria, coloque as importações da biblioteca Rich assim:
from rich import print
from rich import inspect
E depois crie um objeto assim, e exiba ele com o inspect:
c = ContaBancaria(111, "José", 500)
inspect(c) # É melhor visualmente do que print(c.__getstate__())
Ele mostrará todos os dados que colocamos anteriormente na classe, incluindo a documentação. Não esqueça de colocar a opção Emulate Terminal no Run With Parameters.