6. Programação orientada a objetos - classe Fraction

6. Programação orientada a objetos - classe Fraction

6. Programação orientada a objetos - classe Fraction

Na aula anterior vimos que a quantização na representação de números reais causam imprecisões ao calcular números harmônicos e, para mitigar esse problema, sugerimos o uso de frações.

Observe que, assim como pensamos em manipular inteiros e reais usando operações como soma, subtração, multiplicação etc., podemos imaginar essas mesmas operações usando frações. Nessa e nas próximas aulas vamos ver como podemos “abstrair” frações como se fossem, simplesmente, mais um tipo numérico. Para isso, vamos começar a introduzir conceitos de orientação à objetos, como os conceitos de classes, objetos, atributos e métodos.

6.1. Tópicos

  • Classes, objetos, atributos e métodos;
  • Métodos especiais (= mágicos) do Python: __init__() e __str__();
  • Métodos comuns como, por exemplo, add(), sub(), mul(), e div();

6.2. Introdução

A capacidade de representar e abstrair conceitos é fundamental para o nosso pensamento, tornando possível a generalização de coisas e a construção de conceitos cada vez mais complexos.

Um tipo abstrato de dados (= TAD) é uma descrição de como vemos os dados e as operações permitidas sem a gente se preocupar com a implementação.

Isso permite que nosso pensamento permaneça focado em “o que” os dados representam e “como” usá-los, e podemos ignorar a forma como os dados são armazenados e manipulados pelo computador. Nós já temos feito isso, por exemplo, quando trabalhamos com números inteiros que, internamente, são representados como números binários.

Além de inteiros (= tipo int), há vários outros tipos nativos como float, str, bool e list que podemos usar no Python. Agora veremos como definir novos tipos, não nativos. Esses novos tipos definidos pelo programador são criados por meio de classes.

Na evolução das linguagens de programação, a necessidade de se representar e manipular informações complexas resultou no conceito de classes e objetos. Classes são abstrações que definem como os dados serão representados e manipulados. Já objetos são instâncias de classes que mantém e permitem a manipulação da informação durante a execução de um programa.

Python é uma linguagem de programação orientada a objetos (= POO de object-oriented programming language). Uma das característica de linguagens orientadas a objetos é facilitar que programadores criem novas classes para modelar os dados necessários para resolver seus problemas. Um objeto contém as informações (= valores) de um tipo (= classe) definido pelo programador. O uso de abstração de tipos de dados nos permitem fazer uma descrição de um objeto (= seu estado) e o que ele pode fazer (= seus métodos).

Vamos ilustrar esses conceitos criando a classe Fraction e depois implementando programas que criam e manipulam objetos desse tipo.

6.3. Frações como um tipo de dados

Em Python, o número 123 é representado por um objeto do tipo int, o número 1.23 é representado por um objeto do tipo float, o texto "como é bom estudar programação!" é representado por um objeto do tipo str (= string), e assim por diante.

Quanto mais coisas conseguimos abstrair na forma de objetos, ou seja, que possamos tratar como uma entidade que possui um valor ou estado e que pode ser manipulado por meio de algumas funções ou operações sobre essas entidades, mais poderosa se torna a nossa linguagem.

Por exemplo, vimos anteriormente que uma fração (= número racional) pode ser representada por dois valores inteiros: o numerador (num) e o denominador (den), como na figura abaixo. Em orientação a objetos, dizemos que as informações armazenadas no objeto são seus atributos. Portanto os inteiros num e den devem ser atributos de objetos do tipo (= classe) Fraction.

ilustração: Abstração de um número racional que deve possuir 2 valores inteiros.

Dizemos que o estado do objeto Fraction é definido pelos valores de seus atributos, no caso, num e den. A figura abaixo mostra um objeto Fraction no estado num=0 e den=1.

ilustração: Objeto ``Fraction`` no estado ``num=0`` e ``den=1``.

Assim como podemos fazer operações com números inteiros e reais e com strings que resultam em objetos com outros valores, é conveniente termos funções que manipulam objetos da classe Fraction. Funções associadas a objetos de uma classe são chamadas de métodos. Por exemplo, é frequente termos métodos que nos permitem conhecer o estado do objeto. Esses métodos muitas vezes recebem o nome de get(). Também é comum termos métodos para modificar o estado de algum objeto, colocando novos valores em seus atributos, que tipicamente recebem o nome de put(). É frequente ainda termos métodos que produzem outro objeto da classe. No casso da nossa classe Fraction será conveninete termos um método add() que recebe dois objetos da classe Fraction e retornam um novo objeto da classe Fraction que representa a soma das frações recebidas.

ilustração: Abstração de uma fração que deve possuir 2 valores inteiros

Veremos agora como definir a classe Fraction em Python, seus atributos e os métodos para manipular objetos dessa classe. Tudo isso será feito utilizando como pano de fundo o problema de calcular números harmônicos usando frações, em vez de calcular aproximadamente através do tipo float, nativo do Python.

6.4. Classes e objetos

Assim como precisamos escrever a função antes de usá-la em um programa, precisamos escrever a classe que define como um objeto desse tipo é representado e manipulado antes de usá-lo. Ao escrever uma função, estamos definindo como a função se comporta, ou seja, o que ela faz com os parâmetros de entrada, descrevendo através de código como os parâmetros são manipulados a fim de obtermos o resultado gerado pela função.

Ao escrever a classe que define os objetos de um tipo necessitamos especificar seus atributos e métodos. Podemos considerar uma classe como um molde usado para criar objetos do tipo, que pode definir os valores iniciais (= estado inicial) e seus comportamentos (= métodos). Uma classe é portanto um trecho de código que define os atributos e métodos dos objetos do mesmo tipo, como as frações.

Usando uma metáfora de construção civil, uma classe seria o equivalente à planta de uma casa. A mesma planta pode ser usada para construir várias casas. Cada casa, depois de construída, seria o objeto, uma instância da classe. Digamos que os atributos de uma casa sejam cor e proprietário. Dessa forma, casas com a mesma planta podem ter estados diferentes, uma pode ser verde e outra azul, por exemplo. Podem ter inclusive a mesma cor e proprietário, mas são casas diferentes, se uma muda de cor ou de proprietário, o estado da outra casa permanece inalterado. Podemos definir um método venda que altera o valor de proprietário e assim altera o estado de um objeto, mas não do outro.

Essa metáfora é interessante pois basta nos lembramos que a classe é um molde para construirmos um objeto e como ele é manipulado. Quando um objeto é criado, os atributos são inicializados e, a partir desse estado inicial, fica a disposição do programador para ser manipulado por meio de qualquer dos métodos oferecidos pela classe a qual pertence.

6.5. Classe Fraction

Ao definir uma classe em Python, tradicionalmente usamos um nome com a primeira letra em maiúscula e as demais em minúscula. Nossa classe portanto vai se chamar Fraction.

Já vimos que, como um número racional é definido por seu numerador e denominador, precisamos de ao menos dois atributos inteiros para representar uma fração. Esses atributos podem ser carregados com valores adequados quando um objeto Fraction é criado.

Além disso, gostaríamos de exibir uma fração usando uma string contendo o valor do numerador seguido pelo denominador, separados pelo símbolo "/". Exibir o conteúdo de um objeto na forma de texto é um comportamento bastante útil quando, por exemplo, queremos mostrar o estado de um objeto Fraction usando a função print() do Python.

Uma primeira versão da classe Fraction com a definição desses atributos e métodos pode ser:

Nesse exemplo, a palavra reservada class indica a definição de uma classe de nome Fraction. Como na definição de funções em Python, o nome deve ser seguido por : e o corpo da classe com a definição de seus métodos vem logo a seguir deslocado por alguns espaços (ou tab). Nesse livro usamos 4 espaços em branco.

A criação e exibição do conteúdo de um objeto são comportamentos básicos necessários para qualquer tipo de objeto. Esses comportamentos precisam ser definidos por meio de métodos especiais. Os nomes desses métodos são escritos entre __ para serem facilmente identificados como especiais. Observe que, nesse exemplo, a classe Fraction define dois métodos especiais, o __init__(self) e o __str__(self).

O método especial __init__(self) é chamado automaticamente sempre que um objeto da classe correspondente é criado. Tipicamente, esse método é utilizado para criar os atributos com seus valores iniciais, e por isso esse método é conhecido como construtor da classe.

O método especial __str__(self) deve retornar uma string com a representação textual do objeto. Essa função é utilizada por outras funções do Python, em particular pela função print(), como veremos nos exemplos. Mas antes disso, talvez você esteja curioso para saber o que é esse self que aparece na definição dos métodos.

6.5.1. O que é esse self?

Um dos princípios da linguagem Python é tornar as coisas explícitas. O self é a forma do Python para permitir que objetos possam usufruir de seus próprios (self ~ próprio) métodos e atributos.

Para ver como o self funciona, o exemplo abaixo cria 2 objetos (ou 2 instâncias distintas dessa classe), o objeto a e o objeto b. Execute o exemplo passo-a-passo clicando no botão Next. Essa simulação começa com a seta vermelha na linha 1, que define a classe Fraction. Após o primeiro clique, observe do lado direito ao código que o Python agora tem a sua disposição a “planta” de um objeto Fraction e portanto sabe criar esses objetos. A simulação pula para a linha 10, que é a primeira linha após a definição da classe.

A linha 10 contém a atribuição a = Fraction(2, 3). Ao executar essa linha, observe que uma nova instância de Fraction é criada e a variável a passa a fazer referência para essa instância.

Veja que, na linha 2, o método construtor __init__(self, n, d) foi definido para receber 3 argumentos. Quando escrevemos a Fraction(2,3), o Python chama automaticamente o construtor, considerando 2 como valor de n e 3 como valor de d na __init__. Mas e o self?

Em Python, self é sempre o primeiro parâmetro na definição de todos os métodos e é a forma do Python receber, automaticamente também, uma referência ao próprio objeto. Como, para todos os métodos, self é sempre o primeiro parâmetro, na chamada dos métodos (como em Fraction(2,3) que chama o construtor) em nossos programas não é necessário passar o valor de self. Além de reduzir a quantidade de texto que precisamos digitar, essa notação melhora a legibilidade do código.

Nota sobre self

Na verdade, ao escrever as suas classes em Python, você pode utilizar uma outra palavra além de self, como ref, ou prop, ou outra qualquer. O que importa é que esse primeiro parâmetro é sempre uma referência para o próprio objeto.

6.5.2. Para que usamos o self?

Você deve usar o self para criar atributos e chamar os próprios método. Assim, para tornar claro e explícito que num e den são atributos do objeto e não apenas variáveis auxiliares locais do método, num e den precisam ser associados a self (ou seja, associados ao objeto) utilizando o . (ponto) como separador. O trecho abaixo ilustra o caso com a variável div, definida localmente no método __init__(self), mas por não ficar associada ao objeto, não pode ser usada em __str__(self).

Verifique no entanto que esse código dá erro no método __str__(self) pois a div só foi definida em __init__(self). Leia a mensagem de erro e observe que o Python reclama de uma variável não definida em __str__. Edite esse programa colando self. antes de cada div para tornar div um atributo como self.num e self.den e execute o programa novamente para ver o resultado das divisões.

Não use print() dentro de __str__(self)

O print() nesse exemplo apenas ilustra que podemos usar print em qualquer lugar e mostrar difereças entre variáveis locais e atributos. O método especial __str__(self) define a string que representa o objeto, como queremos que os dados apareçam quando o objeto for usado em um print().

6.5.3. E para alterar os valores dos atributos?

Em Python, podemos alterar os atributos diretamente

r1 = Fraction(0, 1)
r1.num, r1.den = 2, 3

Obviamente nesse caso seria mais simples escrever r1 = Fraction(2,3). Em orientação à objetos também é comum usar um método para ler e escrever os valores de atributos, como os métodos put() e get(). Apesar de comuns, observe que put() e get() são métodos comuns (sem as __).

Na prática de orientação a objetos, o uso de put() e get() ajuda a proteger os atributos e organiza a forma de acesso ao conteúdo.

6.6. Operações com Frações

Por ser um novo tipo numérico, é natural considerar operações como soma e multiplicação de frações, entre outras. Vamos então incluir na classe Fraction os métodos para resolver as seguintes operações (você pode incluir outras depois):

  • add(): para adição de dois racionais;
  • sub(): para subtração de racionais;
  • mul(): para multiplicação de dois racionais; e
  • div(): para divisão entre racionais.

As operações de multiplicação e divisão são relativamente simples. Na multiplicação basta multiplicar os numeradores e os denominadores, e na divisão basta multiplicar o primeiro pelo inverso da segunda fração.

No caso da adição e subtração, é necessário transformar as frações para um mesmo denominador, antes de somar e subtrair os numeradores.

6.7. Exercícios

  1. No exercício abaixo, escreva os métodos div(), add() e sub() (para divisão, adição e subtração), e alguns testes. Antes de escrever os seus métodos, observe o código do método mul() que preserva os estados dos objetos self e other, e retorna um outro (nova instância) Fraction. Faça o mesmo para div(), add() e sub(). Caso desejar, copie o código abaixo em um arquivo no seu computador para desenvolver sua solução no Spyder.
  1. Altere a classe Fraction para que, quando o denominador for 1, ele imprima apenas o valor do numerador.

6.8. Onde estamos e para onde vamos?

Nessa aula vimos como criar uma classe em Python. Uma classe contém a definição de um novo tipo abstrato de dado que podemos usar em nossos programas. Ilustramos isso com a criação de uma classe Fraction. Por meio dessa abstração somos capazes de organizar melhor o código para criação e manipulação desses dados. Uma vez definidas as classes, conseguimos focar nosso pensamento no uso desses tipos e não precisamos mais pensar na sua construção.

Nas próximas aulas vamos continuar a introdução de conceitos sobre orientação à objetos, abstração de dados e como esses conceitos pode ser implementados em Python.