Adicionar comportamento com métodos

Concluído

A meta final de um sistema é produzir uma saída útil. Para chegar lá, você precisa processar a entrada. Durante o processamento, talvez você precise da ajuda de vários métodos e dados. Na OOP (programação orientada a objeto), seus métodos e dados são colocados em objetos. Para processar a entrada e produzir um resultado na OOP, você precisa de métodos.

Métodos na OOP

Independentemente do paradigma usado, os métodos podem executar uma ação. Essa ação pode ser uma computação que só se baseia em entradas ou pode alterar o valor de uma variável.

Métodos em objetos na OOP, são de dois tipos:

  • Métodos externos, que outros objetos podem invocar.
  • Métodos internos, que não podem ser acessados por outros objetos. Além disso, métodos internos ajudam a executar uma tarefa iniciada por uma invocação a um método externo.

Independentemente do tipo de método, eles podem alterar o valor do atributo de um objeto; em outras palavras, o estado dele.

A noção de estado e quem e o que pode alterá-lo, é um assunto importante. É uma parte importante da criação de classes e objetos. Essas perguntas nos levam à nossa próxima seção, encapsulamento.

Encapsulamento: proteger seus dados

A ideia geral do encapsulamento é que os dados em um objeto são internos, algo que só diz respeito ao objeto. Os dados são necessários para o objeto e os métodos para fazerem o trabalho deles, que é executar uma tarefa. Quando você diz que os dados são internos, está dizendo que eles devem ser protegidos contra manipulação externa, ou melhor, manipulação externa não controlada. A pergunta é "por quê"?

Por que você precisa disto

Vamos explicar o motivo pelo qual os dados não devem ser diretamente tocados por outro objeto. Estes são alguns exemplos:

  • Você não precisa conhecer os elementos internos. Ao dirigir um carro, você pisa em um pedal para controlar a embreagem ou para acelerar ou frear. Como você está operando seu carro em um nível mais alto, não se importa com o que acontece nos bastidores, como o carro executa a ação. É a mesma coisa com o código. Na maioria das vezes, você não precisa saber como um objeto faz alguma coisa, desde que haja um método que você possa invocar que faz o que você deseja.

  • Você não deve conhecer o mecanismo interno. Em vez de fazer um pedal interagir com o carro, imagine que você tenha uma chave de fenda ou um kit de soldagem para tentar acelerar. Parece assustador, certo? É porque é assustador mesmo. Ou digamos que você tenha um exemplo mais concreto, uma classe square, com o seguinte código:

    class Square:
         def __init__(self):
             self.height = 2
             self.width = 2
         def set_side(self, new_side):
             self.height = new_side
             self.width = new_side
    
    square = Square()
    square.height = 3 # not a square anymore
    

    No exemplo de quadrado, você tem a noção do que é um quadrado definindo a variável height. Da forma como o quadrado é codificado, ele precisa invocar o método set_side() para que o quadrado funcione corretamente. Deixar o objeto cuidar dos próprios dados é considerado mais seguro. Em quase todas as instâncias, você deve optar por interagir por meio de um método versus definir os dados explicitamente.

Níveis de acesso

Como você pode proteger sua classe e seu objeto contra manipulação indesejada de dados? A resposta é: com níveis de acesso. Você pode ocultar dados do mundo exterior e de outros objetos, marcando dados e funções com palavras-chave específicas. Essas palavras-chave são conhecidas como modificadores de acesso.

A maneira como o Python realiza a ocultação de dados é adicionando prefixos a nomes de atributos. Um sublinhado à esquerda, _, é uma mensagem para o mundo exterior de que esses dados provavelmente não devem ser tocados. Quando você modifica a classe square, você acaba tendo este código:

  class Square:
      def __init__(self):
          self._height = 2
          self._width = 2
      def set_side(self, new_side):
          self._height = new_side
          self._width = new_side

  square = Square()
  square._height = 3 # not a square anymore

Um sublinhado à esquerda ainda permite que os dados sejam modificados, aos quais o Python se refere como protegidos. Podemos fazer isso melhor? Sim, com dois sublinhados à esquerda, __, que é conhecido como privado. Agora a classe square será parecida com este código:

  class Square:
      def __init__(self):
          self.__height = 2
          self.__width = 2
    def set_side(self, new_side):
          self.__height = new_side
          self.__width = new_side

  square = Square()
  square.__height = 3 # raises AttributeError

Ótimo, então estamos seguros. Protegemos nossos dados? Bem, não totalmente. O Python apenas altera o nome dos dados subjacentes. Inserindo este código, você ainda pode alterar o valor dele:

square = Square()
square._Square__height = 3 # is allowed

Muitas outras linguagens que implementam a proteção de dados resolvem esse problema de maneira diferente. O Python é exclusivo, pois a proteção de dados é mais semelhante a níveis de sugestão em vez de ser estritamente implementada.

O que são getters e setters?

Dissemos até aqui que os dados, em geral, não devem ser tocados por alguém de fora. Os dados interessam ao objeto. Assim como acontece com todas as regras e recomendações fortes, há exceções. Às vezes, você precisa alterar os dados ou alterar é mais simples do que ter que adicionar uma quantidade significativa de código.

Os getters e setters, que também são conhecidos como acessadores e modificadores, são métodos dedicados à leitura ou à alteração de seus dados. Os getters desempenham o papel de tornar seus dados internos legíveis para o mundo exterior, o que não parece tão ruim, não é? Setters são métodos que podem alterar seus dados diretamente. A ideia é que um setter atue como uma proteção para que não seja possível definir um valor indevido. Vamos abrir nossa classe square novamente e ver getters e setters em ação:

  class Square:
      def __init__(self):
          self.__height = 2
          self.__width = 2
      def set_side(self, new_side):
          self.__height = new_side
          self.__width = new_side
      def get_height(self):
          return self.__height
      def set_height(self, h):
          if h >= 0:
              self.__height = h
          else:
              raise Exception("value needs to be 0 or larger")

  square = Square()
  square.__height = 3 # raises AttributeError

O método set_height() impede que você defina o valor como algo negativo. Se você fizer isso, ele vai gerar uma exceção.

Usar decoradores para getters e setters

Os decoradores são uma entidade importante em Python. Eles fazem parte de uma entidade maior chamada metaprogramação. Os decoradores são funções que usam sua função como uma entrada. A ideia é codificar a funcionalidade reutilizável como funções decoradoras e, em seguida, decorar outras funções com ela. A finalidade é dar à sua função um recurso que ela não tinha antes. Um decorador pode, por exemplo, adicionar campos ao seu objeto, medir o tempo necessário para invocar uma função e fazer muito mais.

No contexto da OOP e getters e setters, um decorador específico @property pode ajudar você a remover um código clichê quando adiciona getters e setters. O decorador @property faz o seguinte para você:

  • Cria um campo de suporte: quando você decora uma função com o decorador @property, ele cria um campo de suporte específico. Você poderá substituir esse comportamento se desejar, mas é bom ter um comportamento padrão.
  • Identifica um setter: um método setter pode alterar o campo de suporte.
  • Identifica um getter: essa função deve retornar o campo de suporte.
  • Identifica uma função de exclusão: essa função pode excluir o campo.

Vamos ver esse decorador em ação:

class Square:
    def __init__(self, w, h):
        self.__height = h
        self.__width = w
  
    def set_side(self, new_side):
        self.__height = new_side
        self.__width = new_side

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, new_value):
        if new_value >= 0:
            self.__height = new_value
        else:
            raise Exception("Value must be larger than 0")

No código anterior, a função height() é decorada pelo decorador @property. Essa ação de decoração cria o campo privado __height. O campo __height não está definido no construtor __init__() porque o decorador já faz isso. Há também outra decoração acontecendo, ou seja, @height.setter. Essa decoração aponta para um método height() semelhante ao setter. O novo método height recebe outro parâmetro value como o segundo parâmetro.

A capacidade de manipular a altura separada da largura ainda causará um problema. Você precisará entender o que a classe faz antes de você considerar a possibilidade de permitir getters e setters, pois você está introduzindo o risco.