Explorando Membros Especiais e Modificadores de Acesso em Python

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.

Tags: Python OOP metaclasses magic-methods Encapsulation

Publicado em 7-14 11:40