Encapsulamento em Python

Encapsulamento é um dos pilares da programação orientada a objetos, segundo o qual procuramos esconder de clientes (usuários de uma classe) todas as informação que não são necessárias ao uso da classe.

Por exemplo, suponha que precisássemos criar uma classe para armazenar informações de funcionários de uma empresa, como ilustrado no exemplo abaixo.

class Funcionario:
    def __init__(self, nome, cargo, valor_hora_trabalhada):
        self.nome = nome
        self.cargo = cargo
        self.valor_hora_trabalhada = valor_hora_trabalhada
        self.horas_trabalhadas = 0
        self.salario = 0

    def registra_hora_trabalhada(self):
        self.horas_trabalhadas += 1

    def calcula_salario(self):
        self.salario = self.horas_trabalhadas * self.valor_hora_trabalhada

Na classe acima, o salário de um funcionário é calculado com base no valor por hora trabalhada e na quantidade de horas trabalhadas. A classe acima é razoável, mas possui alguns problemas. Informações sigilosas de funcionários, como o salário, são expostas a clientes da classe, o que nem sempre é desejável. Além disso, clientes da classe podem simplesmente alterar o salário final de um funcionário sem utilizar a função calcula salário. Por exemplo, na implementação acima, nada impede que um cliente da classe faça algo como f.salario = 1000000, o que alteraria o salário final do funcionário sem que este seja atrelado ao número de horas trabalhadas. O mesmo problema acontece com a variável horas_trabalhadas.

O que desejamos é encapsular (esconder) as informações de salário do funcionário. Como fazer isso?

Diferentes linguagens de programação possuem mecanismos diferentes para implementar encapsulamento. Python adota uma abordagem diferente para encapsulamento se comparado a linguagens como Java e C++. Nessas linguagens, existem formas de se garantir que algo não seja visível a clientes da classe, ao passo que em Python existe uma convenção (um "acordo") sobre quais coisas da classe os clientes devem acessar.

Por exemplo, em Java e C++ existe a palavra-chave private para indicar que um dado ou método não é visível fora da classe. Em Python, existe uma convenção de que dados ou métodos cujo nome começa com _ (dois _underscores) não deveriam ser acessados fora da classe, como ilustrado no exemplo abaixo.

class Funcionario:
    def __init__(self, nome, cargo, valor_hora_trabalhada):
        self.nome = nome
        self.cargo = cargo
        self.valor_hora_trabalhada = valor_hora_trabalhada
        self.__horas_trabalhadas = 0 (1)
        self.__salario = 0 (2)

    def registra_hora_trabalhada(self):
        self.horas_trabalhadas += 1

    def calcula_salario(self):
        self.__salario = self.__horas_trabalhadas * self.valor_hora_trabalhada
1 Mudamos o nome da variável horas_trabalhadas para começar com __.
2 Fazemos o mesmo com a variável salario.

Isso já é um avanço, pois sinaliza a clientes da classe que as variáveis horas_trabalhadas e salario não devem ser alteradas arbritrariamente.

Além disso, para o programador, encapsulamento permite que a implementação das funcionalidades da classe seja alterada sem que o código que usa a classe precise mudar. Em outras palavras, dado que a interface da classe (o que é exposto aos clientes) não mude, o programador tem a liberdade de mudar a implementação da funcionalidade que a classe oferece. Por exemplo, caso a forma de cálculo do salário mude, basta que o programador altere a implementação do método calcula_salario() e clientes da classe continuarão a usar o método sem precisar sofrer alterações.

Porém, dado que essa forma de encapsulamento é somente um indicativo de que dados e métodos cujo nome começa com __ não devem (mas podem) ser acessados, ainda assim podemos alterar a variável salário, conforme mostrado no exemplo abaixo.

pedro = Funcionario('Pedro', 'Gerente de Vendas', 50)
pedro.__salario = 100000
print(pedro.__salario)
100000

Como impedir esse tipo de alteração?

Python possui outro mecanismo para garantir que certas variáveis de uma classe não sejam alteradas. Esse mecanismo consite do uso do decorador @property, que nos permite restringir acesso a variáveis de uma classe. O exemplo abaixo ilustra o uso desse mecanismo.

class Funcionario:
    def __init__(self, nome, cargo, valor_hora_trabalhada):
        self.nome = nome
        self.cargo = cargo
        self.valor_hora_trabalhada = valor_hora_trabalhada
        self.__salario = 0
        self.__horas_trabalhadas = 0

    @property
    def salario(self): (1)
        return self.__salario

    @salario.setter
    def salario(self, novo_salario): (2)
        raise ValueError("Impossivel alterar salario diretamente. Use a funcao calcula_salario().")

    def registra_hora_trabalhada(self):
        self.__horas_trabalhadas += 1

    def calcula_salario(self):
        self.__salario = self.__horas_trabalhadas * self.valor_hora_trabalhada

pedro = Funcionario('Pedro', 'Gerente de Vendas', 50)
pedro.salario = 100000 (3)
Traceback (most recent call last):
  File "teste.py", line 26, in <module>
    f.salario = 1000
  File "teste.py", line 16, in salario
    raise ValueError("Impossivel alterar salario diretamente. Use a funcao calcula_salario().")
ValueError: Impossivel alterar salario diretamente. Use a funcao calcula_salario().
1 Criamos uma propriedade salario
2 Restringimos acesso à propriedade salário e instruímos os clientes a alterarem o valor da variável salario usando a função calcula_salario().
3 Uma tentativa de alterar a propriedade salario sem usar a função calcula_salario() resulta em um erro.

Da forma mostrada acima, ao tentar alterar o valor da propriedade salario, o cliente recebe um erro avisando a forma correta de mudar o salário de um funcionário.

Até o momento, vimos que o comportamento padrão em Python é que variáveis e métodos são completamente visívels a clientes de uma classe. Vimos também que podemos restringir a visibilidade de variáveis e métodos de uma classe, por meio de convenções de nome (uso do __) e de outros recursos como decoradores (@property).

Além destes dois níveis de visibilidade, Python nos permite também criar variáveis ou métodos que são visíveis somente na classe e em classes dela derivadas. A convenção é que variáveis ou métodos cujo nome começa com _ (um underscore apenas) são visíveis em uma hierarquia de classes, ou seja, na classe base e nas classes derivadas, mas não devem ser acessados fora destes casos.

Agora que já aprendemos sobre herança e encapsulamento (dois dos conceitos mais importantes de OOP), podemos avançar para outro conceito importantíssimo, polimorfismo.