Python Atributos e Métodos

Um objeto junta duas coisas: o que ele tem e o que ele faz. Uma conta bancária tem titular e saldo, e faz depósitos e saques. O que o objeto tem são os atributos (os dados); o que ele faz são os métodos (as funções que moram na classe). Na aula __init__ e self você escreveu o primeiro método, mostrar. Nesta aula você vai escrever métodos que mudam o objeto, métodos que respondem uma pergunta, e vai conhecer um segundo tipo de atributo: o que pertence à classe inteira, e não a cada objeto.

Você vai aprender
  • A escrever métodos que mudam os dados do objeto (depositar, sacar).
  • A escrever métodos que devolvem um valor com return, e um método que chama outro com self..
  • A diferença entre atributo de instância (de cada objeto) e atributo de classe (de todos).
  • A contar quantos objetos uma classe já criou.
  • A armadilha da lista guardada na classe, que todos os objetos dividem sem querer.
  • A perguntar se um objeto tem um atributo, com hasattr e getattr.
  • Os erros reais: o método sem parênteses, o atributo com o nome do método, o método chamado sem objeto e o self. esquecido.

A ideia: substantivos e verbos

Pense na ficha de um aluno da escola do bairro. Os campos da ficha (nome, turma, notas) são substantivos: coisas que o aluno tem. Mas a secretaria também faz coisas com a ficha: lança uma nota, calcula a média, diz se o aluno passou. Isso são verbos.

AtributoMétodo
O que éUm dado guardado no objetoUma função que mora na classe
Parece comUm substantivo: saldo, nomeUm verbo: depositar, mostrar
Como se usaSem parênteses: conta.saldoCom parênteses: conta.depositar(50)
Você já usou—"ana".upper(), lista.append(5)

Os métodos de texto e de lista que você já conhece são exatamente isso: métodos escritos dentro das classes str e list. O compras.append("leite") é um método que muda a lista; o nome.upper() é um método que devolve um texto novo. Vamos escrever os dois tipos.


1. Métodos que mudam o objeto

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def depositar(self, valor):
        self.saldo += valor
        print(f"Depósito de R$ {valor:.2f}. Saldo: R$ {self.saldo:.2f}")

    def sacar(self, valor):
        if valor > self.saldo:
            print(f"Saque de R$ {valor:.2f} negado. Saldo: R$ {self.saldo:.2f}")
        else:
            self.saldo -= valor
            print(f"Saque de R$ {valor:.2f}. Saldo: R$ {self.saldo:.2f}")


conta = ContaBancaria("Ana", 100)
conta.depositar(50)
conta.sacar(30)
conta.sacar(500)
print(conta.saldo)
def depositar(self, valor):
Um método com um parâmetro além do self. Quem chama passa só o valor: conta.depositar(50). O self é a conta, que vem antes do ponto.
self.saldo += valor
Muda o atributo saldo do próprio objeto. A mudança fica guardada: o próximo método que olhar o saldo vai ver o valor novo.
valor
Um parâmetro comum, que só existe enquanto o método roda. Não precisa de self.: não é um dado do objeto, é só o valor desta operação.
if valor > self.saldo:
O método pode decidir. É a vantagem de ter o saque dentro da classe: a regra "não dá para sacar mais do que tem" fica num lugar só, e vale para toda conta.
$ python3 conta.py
Depósito de R$ 50.00. Saldo: R$ 150.00
Saque de R$ 30.00. Saldo: R$ 120.00
Saque de R$ 500.00 negado. Saldo: R$ 120.00
120

Cada chamada partiu do saldo que a anterior deixou: 100 + 50 = 150, menos 30 = 120, e o saque de 500 foi negado. O objeto lembra o que aconteceu com ele. (O último print mostra 120, sem casas decimais, porque a conta começou com o inteiro 100 e só recebeu inteiros.)

Missão: a conta da Bia sabe depositar, mas ainda não sabe sacar, e o programa quebra na linha do saque. Escreva o método sacar(self, valor), que tira o valor do saldo. A Bia tem 100 reais e saca 30: o programa deve mostrar Saldo: R$ 70.00.

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def depositar(self, valor):
        self.saldo += valor

    # escreva aqui o método sacar

    def mostrar(self):
        print(f"Saldo: R$ {self.saldo:.2f}")


conta = ContaBancaria("Bia", 100)
conta.sacar(30)
conta.mostrar()
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def depositar(self, valor):
        self.saldo += valor

    def sacar(self, valor):
        self.saldo -= valor

    def mostrar(self):
        print(f"Saldo: R$ {self.saldo:.2f}")


conta = ContaBancaria("Bia", 100)
conta.sacar(30)
conta.mostrar()

2. Métodos que respondem: return

Nem todo método precisa imprimir. Muitos só calculam e devolvem uma resposta, com return (aula return), e quem chamou decide o que fazer com ela: somar, comparar, mostrar.

class Produto:
    def __init__(self, nome, preco, estoque=0):
        self.nome = nome
        self.preco = preco
        self.estoque = estoque

    def valor_em_estoque(self):
        return self.preco * self.estoque

    def tem_estoque(self):
        return self.estoque > 0

    def resumo(self):
        situacao = "tem" if self.tem_estoque() else "esgotado"
        return f"{self.nome} ({situacao}): R$ {self.valor_em_estoque():.2f} parados"


cafe = Produto("Café", 18.90, 12)
leite = Produto("Leite", 6.50)

total = cafe.valor_em_estoque() + leite.valor_em_estoque()
print(f"Total parado no estoque: R$ {total:.2f}")
print(cafe.resumo())
print(leite.resumo())
return self.preco * self.estoque
Calcula quanto dinheiro está "parado" na prateleira e devolve. O método não mostra nada: quem chamou recebe o número.
return self.estoque > 0
Um método que responde sim ou não (True ou False). É costume dar a esses métodos nomes de pergunta: tem_estoque, esta_vazio, pode_sacar.
self.tem_estoque()
Um método chamando outro método do mesmo objeto. Repare no self. antes: é o "eu mesmo, verifique se eu tenho estoque".
cafe.valor_em_estoque() + leite.valor_em_estoque()
Como o método devolve um número, dá para fazer conta com o resultado.
$ python3 devolve.py
Total parado no estoque: R$ 226.80
Café (tem): R$ 226.80 parados
Leite (esgotado): R$ 0.00 parados
Devolver ou imprimir?

Prefira métodos que devolvem. Um método que só imprime serve para uma coisa: imprimir. Um que devolve serve para imprimir, somar, comparar, guardar num arquivo. O "ana".upper() não imprime nada: devolve "ANA", e você faz o que quiser com ele.


3. Atributos de instância × atributos de classe

Todos os atributos que você criou até aqui foram escritos com self., dentro do __init__. Eles se chamam atributos de instância: cada objeto tem o seu (o nome do café não é o nome do pão).

Às vezes, um dado é o mesmo para todos os objetos. Todos os produtos são da mesma loja, por exemplo. Em vez de guardar uma cópia em cada objeto, você escreve o dado direto na classe, fora dos métodos. É um atributo de classe. Na comparação da forma de bolo: o sabor é de cada bolo (instância); a marca gravada no fundo da forma sai igual em todos (classe).

class Produto:
    loja = "Mercadinho da Ana"

    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco


cafe = Produto("Café", 18.90)
pao = Produto("Pão de forma", 9.50)

print(cafe.loja)
print(pao.loja)
print(Produto.loja)

Produto.loja = "Mercadinho Aurora"
print(cafe.loja)
print(pao.loja)

print(vars(cafe))
loja = "Mercadinho da Ana"
Escrito dentro da classe, mas fora de qualquer método e sem self.. É um atributo da classe Produto.
cafe.loja
O café não tem um atributo loja próprio. Quando não acha o nome no objeto, o Python procura na classe, e acha lá.
Produto.loja
O atributo de classe também pode ser lido pela própria classe, sem objeto nenhum.
Produto.loja = "Mercadinho Aurora"
Trocar o valor na classe muda o que todos os objetos enxergam, de uma vez.
$ python3 de_classe.py
Mercadinho da Ana
Mercadinho da Ana
Mercadinho da Ana
Mercadinho Aurora                  « mudou na classe…
Mercadinho Aurora                  « …e todos os objetos enxergam
{'nome': 'Café', 'preco': 18.9}

A última linha é a prova: dentro do objeto cafe só há nome e preco. A loja não está nele; está na classe, e o objeto só "pega emprestado".

A ordem da busca: primeiro o objeto, depois a classe

E se a Ana escrever cafe.loja = "Barraca da feira"? A regra é: escrever com objeto. cria (ou troca) o atributo no objeto, nunca na classe. O café ganha uma loja só dele, que passa a tapar a da classe:

class Produto:
    loja = "Mercadinho da Ana"

    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco


cafe = Produto("Café", 18.90)
pao = Produto("Pão de forma", 9.50)

cafe.loja = "Barraca da feira"
print(cafe.loja)
print(pao.loja)
print(Produto.loja)
print(vars(cafe))

del cafe.loja
print(cafe.loja)
$ python3 sombra.py
Barraca da feira                   « o café tem uma loja própria
Mercadinho da Ana                  « o pão continua lendo a da classe
Mercadinho da Ana                  « a classe não mudou
{'nome': 'Café', 'preco': 18.9, 'loja': 'Barraca da feira'}
Mercadinho da Ana                  « depois do del, o café volta a ler a da classe

Quando o Python procura cafe.loja, ele olha primeiro no objeto; só se não achar, olha na classe. O del cafe.loja apaga o atributo próprio do café, e aí a busca volta a cair na classe.

Atributo de instânciaAtributo de classe
Onde se escreveNum método, com self.: self.nome = nomeNo corpo da classe, fora dos métodos: loja = "…"
De quem éDe cada objetoDa classe, dividido por todos os objetos
Como se lêcafe.nomeProduto.loja (ou cafe.loja, se o café não tiver uma própria)
Como se trocacafe.nome = "…"Produto.loja = "…" (com o nome da classe)
Bom paraOs dados de cada um: nome, preço, saldoConstantes e configurações iguais para todos; contadores

Um contador de objetos

Um uso clássico do atributo de classe: contar quantos objetos já foram criados. Cada __init__ soma 1 no contador da classe:

class Produto:
    total = 0

    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco
        Produto.total += 1


print(Produto.total)
cafe = Produto("Café", 18.90)
pao = Produto("Pão de forma", 9.50)
leite = Produto("Leite", 6.50)
print(f"Produtos cadastrados: {Produto.total}")
$ python3 contador.py
0
Produtos cadastrados: 3

Repare no Produto.total += 1, com o nome da classe. Com self.total += 1 não funcionaria, e sem erro nenhum: veja em Quando dá errado.


4. Perguntando se o objeto tem um atributo

Duas funções embutidas ajudam quando você não sabe se um atributo existe:

class Produto:
    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco


cafe = Produto("Café", 18.90)
print(hasattr(cafe, "preco"))
print(hasattr(cafe, "estoque"))
print(getattr(cafe, "nome"))
print(getattr(cafe, "estoque", 0))
$ python3 getattr_.py
True
False
Café
0                                  « não existe: devolveu o valor reserva
hasattr(cafe, "preco")
has attribute, "tem o atributo?". Responde True ou False, sem erro. O nome do atributo vai entre aspas, como texto.
getattr(cafe, "nome")
get attribute, "pegue o atributo". O mesmo que cafe.nome, mas com o nome num texto, útil quando o nome está guardado numa variável.
getattr(cafe, "estoque", 0)
Com um terceiro valor: se o atributo não existir, devolve esse valor em vez de dar AttributeError. Parecido com o get dos dicionários.

No dia a dia, essas funções aparecem pouco: se o __init__ cria todos os atributos, você sabe que eles existem. Elas são úteis com objetos que vêm de fora do seu programa.


Formas diferentes de mudar um atributo

Para aumentar o saldo de uma conta, você pode mexer no atributo de fora ou pedir ao objeto, por um método:

FormaExemploQuando usar
Pelo métodoconta.depositar(50)Quase sempre. A regra (validar o valor, avisar, registrar) fica num lugar só
Direto no atributoconta.saldo += 50Em dados simples, sem regra nenhuma. Numa conta bancária, evite: ninguém confere se o valor faz sentido
Com setattrsetattr(conta, "saldo", 150)Raramente: só quando o nome do atributo está guardado numa variável

O Python não proíbe ninguém de escrever conta.saldo = -1000 direto. Como avisar "não mexa aqui" é o assunto da aula Encapsulamento, e como validar cada mudança é o da aula @property.


Quando dá errado

A lista na classe: todos os alunos com as mesmas notas

Este é um dos erros mais famosos do Python. A Ana guardou a lista de notas como atributo de classe:

class Aluno:
    notas = []

    def __init__(self, nome):
        self.nome = nome

    def adicionar_nota(self, nota):
        self.notas.append(nota)


bia = Aluno("Bia")
caio = Aluno("Caio")
bia.adicionar_nota(9.0)
caio.adicionar_nota(4.5)
print(bia.nome, bia.notas)
print(caio.nome, caio.notas)
$ python3 lista_compartilhada.py
Bia [9.0, 4.5]                     « a Bia ganhou o 4,5 do Caio
Caio [9.0, 4.5]                    « e o Caio, o 9 da Bia

Nenhuma mensagem de erro, e as notas se misturaram. Siga a busca: self.notas não existe na Bia, então o Python acha a lista da classe; o append põe o 9.0 nela. Depois, self.notas do Caio também cai na classe: a mesma lista. Existe uma lista só, dividida por todos. (Diferente do cafe.loja = …, o append não cria nada no objeto: ele muda a lista que já existe.)

Como consertar: crie a lista no __init__, com self.. Assim cada aluno nasce com a sua lista vazia:

class Aluno:
    def __init__(self, nome):
        self.nome = nome
        self.notas = []

    def adicionar_nota(self, nota):
        self.notas.append(nota)


bia = Aluno("Bia")
caio = Aluno("Caio")
bia.adicionar_nota(9.0)
caio.adicionar_nota(4.5)
print(bia.nome, bia.notas)
print(caio.nome, caio.notas)
$ python3 lista_certa.py
Bia [9.0]
Caio [4.5]

Regra prática: listas, dicionários e conjuntos (as coisas que mudam, aula Mutável × Imutável) vão no __init__, com self.. Na classe, só valores que ninguém vai mudar por dentro.

O contador que não conta: self.total += 1
class Produto:
    total = 0

    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco
        self.total += 1


cafe = Produto("Café", 18.90)
pao = Produto("Pão de forma", 9.50)
leite = Produto("Leite", 6.50)
print(f"Produtos cadastrados: {Produto.total}")
print(cafe.total, pao.total, leite.total)
$ python3 contador_errado.py
Produtos cadastrados: 0
1 1 1

Três produtos, e o contador da classe continua em 0. O self.total += 1 é o mesmo que self.total = self.total + 1: a parte da direita lê o 0 da classe, soma 1, e a parte da esquerda escreve no objeto (escrever com self. sempre cria no objeto). Cada produto ganhou um total próprio valendo 1, e a classe nem ficou sabendo. Como consertar: para mudar um atributo de classe, use o nome da classe: Produto.total += 1.

Chamar o método sem os parênteses
class Produto:
    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco

    def mostrar(self):
        print(f"{self.nome}: R$ {self.preco:.2f}")


cafe = Produto("Café", 18.90)
cafe.mostrar
print("Fim do programa")
print(cafe.mostrar)
$ python3 sem_parenteses.py
Fim do programa
<bound method Produto.mostrar of <__main__.Produto object at 0x7fdd129906e0>>

A linha 11, cafe.mostrar, não fez nada, e sem erro: sem os parênteses, o método não é chamado, só citado. O último print mostra o que é o cafe.mostrar sem parênteses: um bound method, "método amarrado" ao objeto cafe, pronto para ser chamado, mas não chamado. Como consertar: cafe.mostrar(). Métodos são verbos: precisam dos parênteses para acontecer.

Atributo e método com o mesmo nome: 'int' object is not callable
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def saldo(self):
        print(f"Saldo: R$ {self.saldo:.2f}")


conta = ContaBancaria("Ana", 100)
conta.saldo()
$ python3 mesmo_nome.py
Traceback (most recent call last):
  File "/home/ana/mesmo_nome.py", line 11, in <module>
    conta.saldo()
    ~~~~~~~~~~~^^
TypeError: 'int' object is not callable

"Um objeto int não pode ser chamado". O método saldo mora na classe; o atributo saldo (o número 100) mora no objeto. Na busca, o objeto vem primeiro: conta.saldo achou o número, e os parênteses tentaram "chamar" o 100, como se fosse uma função. Como consertar: dê nomes diferentes. Atributos com substantivos (saldo), métodos com verbos (mostrar_saldo).

Chamar o método pela classe, sem objeto: missing 1 required positional argument: 'self'
class Produto:
    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco

    def mostrar(self):
        print(f"{self.nome}: R$ {self.preco:.2f}")


Produto.mostrar()
$ python3 sem_objeto.py
Traceback (most recent call last):
  File "/home/ana/sem_objeto.py", line 10, in <module>
    Produto.mostrar()
    ~~~~~~~~~~~~~~~^^
TypeError: Produto.mostrar() missing 1 required positional argument: 'self'

"Falta o argumento self". Mostrar qual produto? Pela classe não há objeto antes do ponto, então ninguém preencheu o self. É como pedir para a forma de bolo dizer o seu sabor. Como consertar: crie um objeto e chame nele: cafe = Produto("Café", 18.90) e cafe.mostrar().

Um método chamando outro sem self.
class Produto:
    def __init__(self, nome, preco, estoque=0):
        self.nome = nome
        self.preco = preco
        self.estoque = estoque

    def tem_estoque(self):
        return self.estoque > 0

    def mostrar(self):
        if tem_estoque():
            print(f"{self.nome}: disponível")
        else:
            print(f"{self.nome}: esgotado")


leite = Produto("Leite", 6.50)
leite.mostrar()
$ python3 sem_self_chamada.py
Traceback (most recent call last):
  File "/home/ana/sem_self_chamada.py", line 18, in <module>
    leite.mostrar()
    ~~~~~~~~~~~~~^^
  File "/home/ana/sem_self_chamada.py", line 11, in mostrar
    if tem_estoque():
       ^^^^^^^^^^^
NameError: name 'tem_estoque' is not defined. Did you mean: 'self.tem_estoque'?

O rastro tem dois trechos: a chamada na linha 18 e, dentro do mostrar, a linha 11. "O nome tem_estoque não está definido. Você quis dizer self.tem_estoque?" O próprio Python sugere o conserto. O método mora na classe, não solto no programa; para chegar nele de dentro de outro método, passe pelo objeto. Como consertar: if self.tem_estoque():.

Missão: as notas da Bia e do Caio estão se misturando. Conserte a classe para cada aluno ter a sua própria lista de notas. O programa deve mostrar Bia [9.0] e Caio [4.5].

class Aluno:
    notas = []

    def __init__(self, nome):
        self.nome = nome

    def adicionar_nota(self, nota):
        self.notas.append(nota)


bia = Aluno("Bia")
caio = Aluno("Caio")
bia.adicionar_nota(9.0)
caio.adicionar_nota(4.5)
print(bia.nome, bia.notas)
print(caio.nome, caio.notas)
class Aluno:
    def __init__(self, nome):
        self.nome = nome
        self.notas = []

    def adicionar_nota(self, nota):
        self.notas.append(nota)


bia = Aluno("Bia")
caio = Aluno("Caio")
bia.adicionar_nota(9.0)
caio.adicionar_nota(4.5)
print(bia.nome, bia.notas)
print(caio.nome, caio.notas)

Referência rápida

EscritaO que faz
def depositar(self, valor):Método com parâmetro; chamado com conta.depositar(50)
self.saldo += valorMuda um atributo do próprio objeto
return self.preco * self.estoqueO método devolve um valor para quem chamou
self.tem_estoque()Um método chamando outro do mesmo objeto
loja = "…" no corpo da classeAtributo de classe, dividido por todos os objetos
Produto.total += 1Muda um atributo de classe (pelo nome da classe)
hasattr(obj, "nome")True se o objeto tiver o atributo
getattr(obj, "nome", padrao)Lê o atributo; se não existir, devolve padrao
del obj.nomeApaga o atributo do objeto
SintomaCausa provável
Objetos diferentes com a mesma listaLista criada na classe, e não no __init__ com self.
Contador da classe parado em 0self.total += 1 no lugar de Produto.total += 1
O método "não fez nada"; aparece <bound method …>Faltaram os parênteses na chamada
TypeError: 'int' object is not callableAtributo e método com o mesmo nome
missing 1 required positional argument: 'self'Método chamado pela classe, sem objeto
NameError … Did you mean: 'self.…'?Faltou o self. para chamar outro método (ou ler um atributo)
AttributeError: … object has no attribute 'sacar'O método não existe na classe (ou o nome está errado)

Exercícios

1. Dentro do método

Complete para o método aumentar o estoque do próprio produto:

def repor(self, quantidade):
     += quantidade

2. O contador da turma

Na classe Aluno, há o atributo de classe matriculados = 0. Complete a linha do __init__ que soma 1 no contador da classe:

+= 1

3. Na classe Produto, loja = "Mercadinho da Ana" está fora dos métodos. A Ana escreve cafe.loja = "Feira". Quanto vale pao.loja?

4. Por que notas = [] no corpo da classe faz todos os alunos dividirem as notas?

5. print(conta.depositar) mostrou <bound method ContaBancaria.depositar of …>. O que faltou?

6. Qual destes é um bom nome para um método que responde se a conta pode pagar um boleto?

Missão final: a média da turma

Missão: a classe Aluno já guarda as notas. Escreva o método media(self), que devolve (com return) a média das notas do aluno: a soma dividida pela quantidade. O programa deve mostrar Bia: média 8.50 e Caio: média 5.25.

class Aluno:
    def __init__(self, nome):
        self.nome = nome
        self.notas = []

    def adicionar_nota(self, nota):
        self.notas.append(nota)

    # escreva aqui o método media


bia = Aluno("Bia")
bia.adicionar_nota(8.0)
bia.adicionar_nota(9.0)

caio = Aluno("Caio")
caio.adicionar_nota(4.5)
caio.adicionar_nota(6.0)

for aluno in [bia, caio]:
    print(f"{aluno.nome}: média {aluno.media():.2f}")
class Aluno:
    def __init__(self, nome):
        self.nome = nome
        self.notas = []

    def adicionar_nota(self, nota):
        self.notas.append(nota)

    def media(self):
        return sum(self.notas) / len(self.notas)


bia = Aluno("Bia")
bia.adicionar_nota(8.0)
bia.adicionar_nota(9.0)

caio = Aluno("Caio")
caio.adicionar_nota(4.5)
caio.adicionar_nota(6.0)

for aluno in [bia, caio]:
    print(f"{aluno.nome}: média {aluno.media():.2f}")

Repare no for aluno in [bia, caio]: uma lista de objetos, e o mesmo método chamado em cada um. O sum e o len estão na aula Funções Embutidas. Para ir além: e se o aluno ainda não tiver nenhuma nota? Experimente tirar as notas do Caio e veja o ZeroDivisionError; um if not self.notas: return 0 no começo do método resolve.


Resumo

  • Atributos são o que o objeto tem (substantivos, sem parênteses); métodos são o que ele faz (verbos, com parênteses).
  • Um método muda o objeto mexendo em self.atributo; a mudança fica guardada para os próximos métodos.
  • Prefira métodos que devolvem com return; um método chama outro com self.metodo().
  • Atributo de instância: self.x no __init__, um por objeto. Atributo de classe: escrito no corpo da classe, dividido por todos.
  • Na leitura, o Python procura primeiro no objeto, depois na classe. Escrever com objeto.x = … sempre cria no objeto; para mudar o da classe, use Classe.x.
  • Listas e dicionários vão no __init__, com self.; na classe, eles viram uma lista só para todo mundo.
  • hasattr e getattr perguntam e leem atributos pelo nome, sem AttributeError.