Encapsulamento esconde detalhes internos de uma classe.
Ele protege dados contra alterações indevidas e acidentais.
Primeiramente, Python usa convenções de nomenclatura para indicar acesso.
Por exemplo, nome indica público, _nome indica protegido.
Além disso, __nome indica privado com name mangling.
Assim, você comunica claramente sua intenção aos outros programadores.
Consequentemente, o código se torna mais seguro e previsível.
Quando utilizar encapsulamento? Em atributos que exigem validação.
Também quando você quer manter uma interface estável.
Por outro lado, para dados simples, atributos públicos bastam.
Python não impõe restrições rígidas como Java ou C++.
Portanto, respeitar as convenções depende da sua disciplina.
Então, vamos explorar cada nível com exemplos práticos.
Três subtítulos guiarão você pelos níveis de encapsulamento.
Finalmente, você projetará classes mais seguras e robustas.
Atributos públicos: acesso livre e direto
Atributos públicos não têm underscore no início. Você pode acessá-los e modificá-los livremente de fora. Quando usar atributos públicos? Em dados simples e sem restrições. Por exemplo, coordenadas de um ponto ou cores de uma tela. Nenhuma validação ocorre automaticamente nesses casos. Exemplo de atributos públicos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
class Ponto: """Classe com atributos públicos.""" def __init__(self, x, y): self.x = x # Público self.y = y # Público def distancia_origem(self): return (self.x ** 2 + self.y ** 2) ** 0.5 def __str__(self): return f"Ponto({self.x}, {self.y})" # Uso: acesso direto aos atributos p = Ponto(3, 4) print(f"Ponto: {p}") print(f"x = {p.x}, y = {p.y}") # Modificação direta (permitida) p.x = 10 p.y = 20 print(f"Após modificação: {p}") print(f"Distância: {p.distancia_origem():.2f}") # Qualquer valor pode ser atribuído (sem validação) p.x = "texto" # Isso funciona, mas quebra a lógica! print(f"Com texto: {p}") # Você também pode deletar o atributo del p.x # print(p.x) # AttributeError! class Configuracao: """Configurações simples com atributos públicos.""" def __init__(self): self.titulo = "Aplicação" self.versao = "1.0.0" self.debug = True self.tema = "escuro" config = Configuracao() print(f"\nConfig: {config.titulo}, v{config.versao}") # Modificações diretas são convenientes config.tema = "claro" config.debug = False print(f"Tema: {config.tema}, Debug: {config.debug}") # Risco: alguém pode criar atributo externamente config.novo_atributo = "inesperado" print(f"Atributo extra: {config.novo_atributo}") |
Atributos públicos oferecem simplicidade e direção. Eles funcionam bem para dados que não precisam de proteção.
Atributos protegidos: convenção de não acessar
Atributos protegidos usam um underscore prefixo (_atributo).
Essa convenção significa “não toque a menos que saiba o que faz”.
O interpretador Python não impõe nenhuma restrição real.
Quando usar atributos protegidos? Em herança e desenvolvimento interno.
Use-os para indicar detalhes de implementação.
A comunidade respeita essa convenção amplamente.
Exemplo de atributos protegidos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
class ContaBancaria: """Classe com atributos protegidos.""" def __init__(self, titular, saldo_inicial): self.titular = titular # Público self._saldo = saldo_inicial # Protegido self._extrato = [] # Protegido self._registrar_transacao("Conta criada", saldo_inicial) def _registrar_transacao(self, descricao, valor): """Método protegido (convenção).""" self._extrato.append(f"{descricao}: R${valor:.2f}") def depositar(self, valor): """Método público com validação.""" if valor <= 0: raise ValueError("Valor deve ser positivo") self._saldo += valor self._registrar_transacao("Depósito", valor) def sacar(self, valor): """Método público com validação.""" if valor <= 0: raise ValueError("Valor deve ser positivo") if valor > self._saldo: raise ValueError("Saldo insuficiente") self._saldo -= valor self._registrar_transacao("Saque", -valor) return valor def obter_saldo(self): """Acesso controlado ao saldo.""" return self._saldo def obter_extrato(self): """Retorna cópia do extrato (não a lista original).""" return self._extrato.copy() # Demonstração print("=== Atributos Protegidos ===\n") conta = ContaBancaria("Ana", 1000) # Acesso público está ok print(f"Titular: {conta.titular}") # Acesso protegido funciona, mas a convenção desencoraja print(f"Saldo (acesso direto): R${conta._saldo:.2f}") # Método público oferece o acesso recomendado print(f"Saldo (via método): R${conta.obter_saldo():.2f}") # Método protegido também funciona (mas evite) conta._registrar_transacao("Acesso externo", 0) # Herança respeita a convenção class ContaEspecial(ContaBancaria): def __init__(self, titular, saldo, limite): super().__init__(titular, saldo) self._limite = limite # Protegido na filha também def sacar_com_limite(self, valor): """Usa atributo protegido da mãe.""" if valor <= self._saldo + self._limite: self._saldo -= valor self._registrar_transacao("Saque c/ limite", -valor) return True return False conta_especial = ContaEspecial("Carlos", 500, 200) print(f"\nConta Especial - Saldo: R${conta_especial.obter_saldo():.2f}") conta_especial.sacar_com_limite(600) print(f"Após saque com limite: R${conta_especial.obter_saldo():.2f}") print(f"Extrato: {conta_especial.obter_extrato()}") |
Atributos protegidos funcionam como uma convenção social. Eles ajudam muito em bibliotecas e frameworks.
Atributos privados: name mangling e encapsulamento real
Atributos privados usam dois underscores prefixo (__atributo).
Python aplica name mangling: renomeia para _Classe__atributo.
Isso dificulta o acesso acidental de fora da classe.
Quando usar atributos privados? Em dados sensíveis ou internos.
Use-os também para evitar conflitos de nome em herança.
O interpretador altera o nome automaticamente.
Exemplo de atributos privados:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
class Banco: """Classe com atributos privados.""" def __init__(self, nome): self.nome = nome # Público self.__taxa_juros = 0.02 # Privado self.__segredo = "ChaveSuprema123" # Privado def __metodo_privado(self): """Método privado (não acessível externamente).""" return f"Processamento interno de {self.nome}" def aplicar_juros(self, saldo): """Método público que usa método privado.""" juros = saldo * self.__taxa_juros print(self.__metodo_privado()) return juros def alterar_taxa(self, nova_taxa): """Controle de acesso via método público.""" if 0 <= nova_taxa <= 0.1: self.__taxa_juros = nova_taxa else: raise ValueError("Taxa deve estar entre 0 e 0.1") class BancoFilho(Banco): def tentar_acessar_privado(self): """Tentativa de acessar atributo privado da mãe.""" # Isso não funciona! __taxa_juros virou _Banco__taxa_juros try: print(self.__taxa_juros) except AttributeError: print("Não consegue acessar __taxa_juros diretamente") # Mas consegue via name mangling (não recomendado) print(f"Acesso via name mangling: {self._Banco__taxa_juros}") # Demonstração print("=== Atributos Privados ===\n") banco = Banco("MeuBanco") # Acesso público funciona print(f"Nome: {banco.nome}") # Acesso privado direto gera erro try: print(banco.__taxa_juros) except AttributeError as e: print(f"Erro ao acessar __taxa_juros: {e}") # Acesso via name mangling (possível, mas NÃO FAÇA ISSO) print(f"Via name mangling: {banco._Banco__taxa_juros}") # Método privado também não acessível try: banco.__metodo_privado() except AttributeError as e: print(f"Erro: {e}") # Método público funciona normalmente juros = banco.aplicar_juros(1000) print(f"Juros sobre R$1000: R${juros:.2f}") # Herança e name mangling print("\n=== Herança e Privados ===") filho = BancoFilho("Filial") filho.tentar_acessar_privado() # Name mangling evita conflitos de nome class A: def __init__(self): self.__valor = "Valor de A" class B: def __init__(self): self.__valor = "Valor de B" class C(A, B): def mostrar_valores(self): # __valor de A virou _A__valor # __valor de B virou _B__valor print(f"_A__valor: {self._A__valor}") print(f"_B__valor: {self._B__valor}") print("\n=== Conflito Evitado ===") c = C() c.mostrar_valores() # Exemplo completo com getters e setters class Pessoa: """Classe com encapsulamento completo.""" def __init__(self, nome, idade): self.nome = nome # Público self.__idade = idade # Privado @property def idade(self): """Getter para idade (controle de acesso).""" return self.__idade @idade.setter def idade(self, nova_idade): """Setter com validação.""" if not isinstance(nova_idade, int): raise TypeError("Idade deve ser inteiro") if nova_idade < 0 or nova_idade > 150: raise ValueError("Idade inválida") self.__idade = nova_idade @idade.deleter def idade(self): """Impede deleção do atributo.""" raise AttributeError("Não é possível deletar a idade") def fazer_aniversario(self): """Método que modifica atributo privado.""" self.__idade += 1 return f"{self.nome} agora tem {self.__idade} anos" print("\n=== Getters e Setters ===") p = Pessoa("João", 30) print(f"{p.nome} tem {p.idade} anos") p.idade = 31 # Usa o setter print(f"Após setter: {p.idade}") try: p.idade = -5 # Validação bloqueia except ValueError as e: print(f"Erro: {e}") print(p.fazer_aniversario()) try: del p.idade # Deleter impede except AttributeError as e: print(f"Erro: {e}") |
Atributos privados oferecem o encapsulamento mais forte em Python. A fórmula da proteção segue esta hierarquia: \(P = \text{público} < \text{protegido} < \text{privado}[/latex] Use público para dados simples e sem restrições. Use protegido para comunicação interna entre classes mãe e filha. Use privado para dados sensíveis ou implementação interna. Portanto, respeite as convenções e seu código ficará mais seguro.
Métodos Mágicos em Python: O Poder dos Dunder Methods
Métodos mágicos são funções especiais com underscores duplos.
Eles definem como objetos se comportam com operadores nativos.
Primeiramente, esses métodos começam e terminam com __.
Por exemplo, __init__ constrói objetos, __str__ os exibe.
Além disso, __add__ define o operador + para sua classe.
A voz passiva é usada aqui: “esses métodos são chamados automaticamente pelo Python”.
Quando utilizar métodos mágicos? Para tornar objetos mais naturais.
Também para integrar suas classes com a linguagem.
Python possui dezenas desses métodos para diferentes propósitos.
Vamos explorar os mais importantes com exemplos práticos.
Três subtítulos guiarão você pelos principais métodos mágicos.
Ao final, você criará classes que parecem tipos nativos.
Construtores, representações e chamadas
__init__ inicializa uma nova instância da classe.
__new__ controla a criação do objeto (mais raro).
__str__ retorna string amigável para usuários.
__repr__ retorna string para depuração (deve recriar objeto).
__call__ permite chamar o objeto como uma função.
Quando usar cada um? __init__ em praticamente toda classe.
__str__ para exibição, __repr__ para logs.
__call__ para objetos que se comportam como funções.
A voz passiva é aplicada: “a string é gerada automaticamente ao imprimir”.
Exemplo desses métodos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class Pessoa: """Demonstra __init__, __str__, __repr__ e __call__.""" def __init__(self, nome, idade): """Construtor: inicializa o objeto.""" self.nome = nome self.idade = idade print(f"__init__: {nome} criado") def __new__(cls, *args, **kwargs): """Controla criação (raro, mas útil para singletons).""" print(f"__new__: criando instância de {cls.__name__}") return super().__new__(cls) def __str__(self): """String amigável para usuários.""" return f"Pessoa: {self.nome} ({self.idade} anos)" def __repr__(self): """String para depuração (idealmente recria o objeto).""" return f"Pessoa('{self.nome}', {self.idade})" def __call__(self, mensagem): """Permite chamar o objeto como função.""" return f"{self.nome} diz: {mensagem}" # Demonstração print("=== __init__, __str__, __repr__, __call__ ===\n") p = Pessoa("Ana", 25) # __str__ é chamado pelo print print(f"print(p): {p}") # __repr__ é chamado no REPL ou com repr() print(f"repr(p): {repr(p)}") # __call__ permite usar o objeto como função print(f"p('Olá!'): {p('Olá!')}") # Exemplo com __repr__ avalável p2 = eval(repr(p)) # Recria o objeto a partir da string print(f"Recriado: {p2}") # Classe com __call__ útil class Contador: def __init__(self): self.contagem = 0 def __call__(self): """Incrementa e retorna o valor.""" self.contagem += 1 return self.contagem print("\n=== Contador com __call__ ===") c = Contador() print(f"c(): {c()}") print(f"c(): {c()}") print(f"c(): {c()}") print(f"Contagem atual: {c.contagem}") |
Métodos mágicos de construção tornam suas classes profissionais.
__repr__ deve ser o mais explícito possível para depuração.
Operadores aritméticos e comparações
__add__ define +, __sub__ define -.
__mul__ define *, __truediv__ define /.
__eq__ define ==, __lt__ define <.
Quando usar esses métodos? Para criar tipos numéricos personalizados.
Por exemplo, vetores, matrizes, dinheiro ou frações.
A voz passiva é aplicada: “as operações são sobrecarregadas pelos métodos”.
Exemplo completo com operadores aritméticos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
class Vetor: """Vetor 2D com operadores aritméticos.""" def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" def __repr__(self): return f"Vetor({self.x}, {self.y})" # Operadores aritméticos def __add__(self, outro): """Vetor + Vetor ou Vetor + número.""" if isinstance(outro, Vetor): return Vetor(self.x + outro.x, self.y + outro.y) elif isinstance(outro, (int, float)): return Vetor(self.x + outro, self.y + outro) return NotImplemented def __radd__(self, outro): """Número + Vetor (soma à direita).""" return self.__add__(outro) def __sub__(self, outro): """Vetor - Vetor.""" if isinstance(outro, Vetor): return Vetor(self.x - outro.x, self.y - outro.y) return Vetor(self.x - outro, self.y - outro) def __mul__(self, escalar): """Vetor * escalar.""" if isinstance(escalar, (int, float)): return Vetor(self.x * escalar, self.y * escalar) return NotImplemented def __rmul__(self, escalar): """Escalar * Vetor.""" return self.__mul__(escalar) def __truediv__(self, escalar): """Vetor / escalar.""" if isinstance(escalar, (int, float)): return Vetor(self.x / escalar, self.y / escalar) return NotImplemented # Operadores de comparação def __eq__(self, outro): """Vetor == Vetor.""" if not isinstance(outro, Vetor): return False return self.x == outro.x and self.y == outro.y def __lt__(self, outro): """Vetor < Vetor (compara módulo).""" return self.modulo() < outro.modulo() def __le__(self, outro): """Vetor <= Vetor.""" return self.modulo() <= outro.modulo() # Métodos auxiliares def modulo(self): return (self.x ** 2 + self.y ** 2) ** 0.5 # Demonstração print("=== Operadores Aritméticos ===\n") v1 = Vetor(3, 4) v2 = Vetor(1, 2) print(f"v1 = {v1}") print(f"v2 = {v2}") print(f"v1 + v2 = {v1 + v2}") print(f"v1 + 5 = {v1 + 5}") print(f"3 * v1 = {3 * v1}") print(f"v1 - v2 = {v1 - v2}") print(f"v1 / 2 = {v1 / 2}") print(f"v1 == Vetor(3,4)? {v1 == Vetor(3, 4)}") print(f"v1 < v2? {v1 < v2}") # Exemplo com frações class Fracao: """Número racional com operadores.""" def __init__(self, numerador, denominador): self.num = numerador self.den = denominador self._simplificar() def _simplificar(self): """Simplifica a fração usando MDC.""" from math import gcd mdc = gcd(self.num, self.den) self.num //= mdc self.den //= mdc def __add__(self, outro): """Fração + Fração.""" novo_num = self.num * outro.den + outro.num * self.den novo_den = self.den * outro.den return Fracao(novo_num, novo_den) def __mul__(self, outro): """Fração * Fração.""" return Fracao(self.num * outro.num, self.den * outro.den) def __str__(self): return f"{self.num}/{self.den}" def __float__(self): """Converte para float.""" return self.num / self.den print("\n=== Frações ===") f1 = Fracao(1, 2) f2 = Fracao(1, 3) print(f"1/2 + 1/3 = {f1 + f2}") print(f"1/2 * 1/3 = {f1 * f2}") print(f"float(1/2) = {float(f1)}") |
Operadores aritméticos tornam suas classes intuitivas.
Use NotImplemented para operações não suportadas.
Métodos de contêiner e gerenciamento de contexto
__len__ define o tamanho (chamado por len()).
__getitem__ permite acesso por índice (obj[i]).
__setitem__ permite atribuição por índice.
__contains__ define o operador in.
__enter__ e __exit__ criam gerenciadores de contexto.
Quando usar esses métodos? Para criar coleções personalizadas.
Também para recursos que precisam de inicialização e limpeza.
A voz passiva é aplicada: “o recurso é adquirido e liberado automaticamente”.
Exemplo de contêiner e context manager:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
class ListaPersonalizada: """Lista com métodos mágicos de contêiner.""" def __init__(self, itens=None): self._itens = list(itens) if itens else [] def __len__(self): """Retorna o tamanho (len()).""" return len(self._itens) def __getitem__(self, indice): """Acesso por índice (lista[indice]).""" if isinstance(indice, slice): return ListaPersonalizada(self._itens[indice]) return self._itens[indice] def __setitem__(self, indice, valor): """Atribuição por índice (lista[indice] = valor).""" self._itens[indice] = valor def __delitem__(self, indice): """Remove por índice (del lista[indice]).""" del self._itens[indice] def __contains__(self, item): """Operador 'in' (item in lista).""" return item in self._itens def __iter__(self): """Torna a classe iterável.""" return iter(self._itens) def __add__(self, outro): """Concatenação com +.""" return ListaPersonalizada(self._itens + outro._itens) def __str__(self): return str(self._itens) # Demonstração de contêiner print("=== Métodos de Contêiner ===\n") lista = ListaPersonalizada([10, 20, 30, 40, 50]) print(f"Lista: {lista}") print(f"Tamanho: {len(lista)}") print(f"lista[2]: {lista[2]}") print(f"lista[1:4]: {lista[1:4]}") print(f"30 in lista? {30 in lista}") print(f"60 in lista? {60 in lista}") lista[2] = 99 print(f"Após lista[2]=99: {lista}") del lista[0] print(f"Após del lista[0]: {lista}") print("Iterando:") for item in lista: print(f" {item}") # Gerenciador de contexto print("\n=== Gerenciador de Contexto ===") class ArquivoGerenciado: """Context manager para arquivos (similar a 'with open').""" def __init__(self, nome, modo): self.nome = nome self.modo = modo self.arquivo = None def __enter__(self): """Entra no contexto (abre o arquivo).""" print(f"Abrindo arquivo: {self.nome}") self.arquivo = open(self.nome, self.modo) return self.arquivo def __exit__(self, exc_type, exc_val, exc_tb): """Sai do contexto (fecha o arquivo).""" print(f"Fechando arquivo: {self.nome}") if self.arquivo: self.arquivo.close() # Retorna False para propagar exceções return False # Usando o context manager with ArquivoGerenciado("teste_magico.txt", "w") as f: f.write("Conteúdo escrito via context manager\n") f.write("Linha 2\n") print("Arquivo escrito com sucesso!") # Lendo o arquivo with ArquivoGerenciado("teste_magico.txt", "r") as f: conteudo = f.read() print(f"Conteúdo lido:\n{conteudo}") # Exemplo com __enter__/__exit__ para timing import time class Temporizador: """Mede tempo de execução de um bloco.""" def __enter__(self): self.inicio = time.time() return self def __exit__(self, exc_type, exc_val, exc_tb): self.fim = time.time() self.duracao = self.fim - self.inicio print(f"Tempo decorrido: {self.duracao:.4f}s") print("\n=== Temporizador ===") with Temporizador() as t: time.sleep(0.5) print("Bloco executado") |
Métodos de contêiner tornam suas coleções nativas.
A fórmula de utilidade dos métodos mágicos:
[latex]U = \frac{N_{\text{métodos implementados}}}{N_{\text{comportamentos nativos}}} \times 100\%\)
Quanto mais métodos mágicos, mais natural sua classe.
Comece com __init__, __str__ e __repr__.
Depois adicione operadores e métodos de contêiner conforme necessário.
Suas classes parecerão tipos nativos da linguagem.