Modificadores de Acesso: Público vs. Privado
Em Python, o encapsulamento é gerenciado através de convenções de nomenclatura. Membros públicos podem ser acessados de qualquer lugar, enquanto membros privados são restritos ao escopo interno da classe. Isso é útil para proteger dados sensíveis, como credenciais de banco de dados.
1. Membros Públicos
Atributos e métodos sem prefixos de sublinhado duplo são públicos por padrão.
class Servidor:
def __init__(self, ip, porta):
self.ip = ip
self.porta = porta
instancia = Servidor('192.168.1.1', 8080)
print(instancia.ip) # Acesso direto permitido
print(instancia.porta)
2. Membros Privados
Ao prefixar um atributo ou método com __ (dunder), o Python utiliza name mnagling para dificultar o acesso externo.
class GerenciadorBD:
def __init__(self, usuario, senha):
self.usuario = usuario
self.__senha = senha # Atributo privado
def obter_senha_segura(self):
# Acesso interno permitido
return self.__senha
db = GerenciadorBD('admin', 'secret123')
# print(db.__senha) # Isso causaria um AttributeError
print(db.obter_senha_segura()) # Acesso indireto via método público
O mesmo princípio se aplica a campos estáticos e métodos:
class AppConfig:
__versao = "2.1.0" # Atributo estático privado
@staticmethod
def ler_versao():
return AppConfig.__versao
print(AppConfig.ler_versao())
3. Herança e Membros Privados
Atributos privados não são acessíveis diretamente por subclasses. Eles pertencem estritamente à classe onde foram definidos.
class Pai:
def __init__(self):
self.__heranca_privada = "Tesouro Oculto"
class Filho(Pai):
def tentar_acessar(self):
# print(self.__heranca_privada) # Falharia
pass
Métodos Mágicos (Dunder Methods)
Os membros especiais permitem que objetos customizados se comportem como tipos nativos do Python.
1. __init__ e __call__
Enquanto __init__ é o construtor disparado na criação do objeto, o __call__ permite que a instância seja executada como uma função.
class Executor:
def __init__(self):
print("Objeto inicializado")
def __call__(self, *args, **kwargs):
print("Objeto chamado como função")
exec_obj = Executor() # Dispara __init__
exec_obj() # Dispara __call__
2. Representação e Conversão: __str__ e __int__
Esses métodos definem como o objeto deve se comportar ao ser convertido para strings ou inteiros.
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
def __str__(self):
return f"Produto: {self.nome} - R$ {self.preco}"
def __int__(self):
return int(self.preco)
item = Produto("Notebook", 4500.99)
print(str(item)) # Produto: Notebook - R$ 4500.99
print(int(item)) # 4500
3. Sobrecarga de Operadores: __add__
Permite definir o comportamento do operador de adição (+) entre objetos.
class Vetor:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, outro):
return Vetor(self.x + outro.x, self.y + outro.y)
v1 = Vetor(1, 2)
v2 = Vetor(3, 4)
v3 = v1 + v2
print(f"Resultado: {v3.x}, {v3.y}")
4. Manipulação de Dicionários e Atributos: __dict__
O atributo __dict__ armazena todos os atributos de instância em formato de dicionário.
class Perfil:
def __init__(self, user, email):
self.user = user
self.email = email
p = Perfil("dev_user", "dev@example.com")
print(p.__dict__) # {'user': 'dev_user', 'email': 'dev@example.com'}
5. Acesso por Índice e Fatiamento: __getitem__, __setitem__, __delitem__
Esses métodos permitem que objetos implementem a sintaxe de colchetes obj[key].
class ContainerCustomizado:
def __init__(self):
self.dados = {}
def __getitem__(self, chave):
if isinstance(chave, slice):
print(f"Fatiamento: Início={chave.start}, Fim={chave.stop}")
return "Resultado da fatia"
return self.dados.get(chave)
def __setitem__(self, chave, valor):
self.dados[chave] = valor
def __delitem__(self, chave):
del self.dados[chave]
c = ContainerCustomizado()
c['id'] = 100 # __setitem__
print(c['id']) # __getitem__
print(c[1:5:2]) # __getitem__ com objeto slice
6. Iteração: __iter__
Para tornar um objeto iterável (compatível com for), deve-se implementar o __iter__, retornando um iterador.
class Numeros:
def __iter__(self):
return iter([1, 10, 100])
for n in Numeros():
print(n)
Conceitos de Metaclasses
Em Python, classes também são objetos. A classe type é a metaclasse padrão que cria todas as outras classes.
Criação Dinâmica de Classes
Uma classe pode ser criada usando a função type(nome, bases, dict).
def saudar(self):
print("Olá de uma classe dinâmica!")
# Criando a classe 'Dinâmica' que herda de object e possui o método 'play'
MinhaClasse = type('Dinamica', (object,), {'play': saudar})
obj_dinamico = MinhaClasse()
obj_dinamico.play()
Uso de Custom Metaclasses
Podemos interceptar a criação de classes herdando de type e usando o argumento metaclass.
class LogMeta(type):
def __init__(cls, nome, bases, dic):
print(f"Classe {nome} criada com sucesso.")
super().__init__(nome, bases, dic)
def __call__(cls, *args, **kwargs):
print(f"Instanciando novo objeto de {cls.__name__}")
return super().__call__(*args, **kwargs)
class Cliente(metaclass=LogMeta):
def __init__(self, nome):
self.nome = nome
c1 = Cliente("Carlos")
O fluxo de execução segue: LogMeta.__init__ é chamado quando a classe é definida, e LogMeta.__call__ é executado sempre que uma nova instância da classe Cliente é solicitada, antes do __init__ do próprio objeto.