7. Melhorando o comportamento da classe Fraction

Na aula anterior vimos alguns recursos básicos oferecidos para a criação de classes em Python.

Nessa aula vamos continuar essa introdução à Programação Orientada a Objetos. que facilita o desenvolvimento de algoritmos com tipos de dados abstratos que você mesmo pode criar e usar.

7.1. Tópicos

  • Mais sobre classes, objetos, atributos e métodos;
  • Método com parâmetros com valores default;
  • Métodos especiais para operadores comuns como, por exemplo, +, -, *, e /;

7.2. Introdução

Na aula anterior começamos a definir uma classe Fraction usada para abstrair o comportamento de frações.

Vimos que, ao definir uma classe, é necessário definir atributos e métodos. Alguns métodos têm nome pré-definido e são chamados de especiais pois enriquecem o comportamento das classes para facilitar a criação e uso de objetos em seus programas. Eles são fáceis de identificar pois são escritos entre dois sublinhados como __init__ e __str__.

Antes, porém, veremos outra característica importante na definição de métodos que é o uso de valores default para os parâmetros.

7.3. Objetos criados com valores “default”

Em muitos casos, é útil criar um objeto com valores pré-determinados, ou “default”. Por exemplo, podemos definir que o valor default de um Fraction seja 0/1 (por que 0/0 seria uma má ideia?). Nesse caso, basta modificar o cabeçalho do método construtor __init__ como ilustrado no trecho de código abaixo. Clique em Forward para ver a simulação passo-a-passo desse programa.

Observe na linha 2 que, agora, o valor default de n é 0 (parâmetro correspondente ao numerador) e de d é 1. Devido aos valores default, podemos definir os valores de n e d apenas quando são diferentes de 0 e 1 respectivamente. Nesse caso, observe que o valor do Fraction r1 é 0/1, o de r2 é 4/1 e o de r3, que recebeu os 2 argumentos, é 3/2. Quando apenas 1 argumento é passado, a ordem dos parâmetros se torna importante. Assim, na criação do Fraction r2, o segundo parâmetro (correspondente ao denominador) não foi fornecido e assume portanto o seu valor default 1.

7.3.1. Mas e se eu quiser passar apenas o valor do denominador?

Nesse caso, o Python permite que você passe os parâmetros usando os nomes definidos no cabeçalho do método. Assim:

r4 = Fraction(d=-1)

e até:

r5 = Fraction(d=3, n=2)

são chamadas válidas.

É possível ainda misturar parâmetros com e sem valores default. Se você decidir por misturar, procure agrupar os parâmetros com valores default no final do cabeçalho, para deixar as chamadas mais consistentes, como:

def metodo(self, a, b, c = 0, d = 1):

Experimente colocar outros parâmetros no __init__(), e crie outros métodos, usando o trecho de código abaixo:

Polimorfismo

o uso de valores default é permitido também no cabeçalho de funções comuns, não apenas métodos, do Python.

Esse recurso é um tipo de polimorfismo, visto que permite que uma mesma função seja chamada de formas diferentes (com um número diferente de argumentos).

7.4. Métodos e funções

Vimos que, por ser um novo tipo numérico, é importante que a classe Fraction permita realizar operações numéricas como:

  • add(): para adição de duas frações;
  • sub(): para subtração de frações;
  • mul(): para multiplicação de duas frações;
  • div(): para divisão entre frações.

Esse métodos definem as formas como podemos manipular objetos do tipo Fraction e, portanto, precisam estar associados ao objeto para usar a notação de ponto como no trecho a seguir:

f23 = Fraction(2, 3)
f34 = Fraction(3, 4)
soma = f23.add(f34)   # chamada do método add usando a notação de ponto

As pessoas iniciantes na POO podem as vezes confundir métodos com funções. Funções auxiliares devem ser definidas fora da definição da classe, para facilitar o reuso dessas funções por outras classes e até mesmo por outros programas.

O trecho de código abaixo ilustra uma classe Fraction cujo construtor usa a função auxiliar mdc_euclides() para criar funções irredutíveis. Essa função é chamada dentro do construtor (__init__) na linha 20, para calcular o máximo divisor comum enter o numerador e o denominador da fração. Assim, frações com valores como -12/4 são reduzidas para o valor -3/1. Estude o código abaixo e complete essa classe com os métodos sub e div.

source na pasta py/a05/classFraction01.py

7.5. Como usar os símbolos +, *, - e /

Uma inconveniência do método add, como vimos na aula anterior, é que precisamos usar a notação de ponto para usar esse método, como no trecho de código abaixo:

f23 = Fraction(2,3)   # cria um objeto do tipo Fraction
f34 = Fraction(3,4)
soma = f23.add(f34)   # chamada do método 'add'

Por ser um tipo numérico, seria mais conveniente se pudéssemos escrever:

f23 = Fraction(2,3)   # cria um objeto do tipo Fraction
f34 = Fraction(3,4)
soma = f23 + f34      # chamada do método especial '__add__'

A notação de ponto deixa explícito o uso de um método associado a um objeto. No entanto, para tipos numéricos, estamos acostamos a usar símbolos como + e * para representar operações como soma e multiplicação. Essa sintaxe mais “simples”, escrevendo f23 + f34 ao invés de f23.add(f34) facilita a leitura e manutenção do código que usa objetos do tipo Fraction.

Para fazer com que nossa classe Fraction reconheça esses símbolos, podemos trocar os nomes que usamos anteriormente para o nome do método especial correspondente. Consulte a [documentação do Python](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types) para ver o nome de outros métodos especiais numéricos que você pode utilizar. Por exemplo, para os operadores que escolhemos escrever para a nossa classe Fraction, os nomes dos métodos especiais são:

  • __add__(self, other): para adição de duas frações usando a notação “self + other”;
  • __sub__(self, other): para subtração de frações usando “self - other”;
  • __mul__(self. other): para multiplicação de duas frações usando “self * other”;
  • __truediv__(self, other): para divisão entre frações usando “self / other”.

O trecho de código a seguir modifica nossa classe Fraction que acabamos de ver anteriormente para que ela passe a fazer as operações de soma e multiplicação usando os símbolos + e *, respectivamente. Complete a classe Fraction com os métodos __sub__ e __truediv__.

7.6. Exercícios

  1. Um número complexo é representado por dois números reais. Nesse exercício, cada objeto dessa classe deve possuir dois atributos de estado de nomes:

    • real: um número real (float) que corresponde à parte real
    • imag: um número real que corresponde à parte imaginária

    Estude a seguinte função main() e verifique os resultados esperados. Implemente uma classe Complexo que deve possuir os atributos real e imag, e os métodos que permitam a execução dessa função main(), gerando exatamente os mesmos resultados esperados.

    def main():
        ''' Testes da classe Complexo
        '''
    
        print("Testes da classe Complexo\n")
    
        c0 = Complexo() # construtor chama __init__()
        print(f"Atributos: real = {c0.real} e imag = {c0.imag}")
    
        c1 = Complexo(9)
        print(f"Atributos: real = {c1.real} e imag = {c1.imag}")
    
        c2 = Complexo(7, 5)
        print(f"Atributos: real = {c2.real} e imag = {c2.imag}")
    
        print("\nChamadas dentro de print")
        print(f"c0 = {c0}")  # chama __str__
        print(f"c1 = {c1}")
        print(f"c2 = {c2}")
    
        print("\nResultados dos métodos")
        c3 = c0.some(c1)
        c4 = c1 * c2
        print(f"c3 = {c3}")
        print(f"c4 = {c4}")
    

    A saída do programa deve ser:

    Testes da classe Complexo
    
    Atributos: real = 0.0 e imag = 0.0
    Atributos: real = 9.0 e imag = 0.0
    Atributos: real = 7.0 e imag = 5.0
    
    Chamadas dentro de print
    c0 = 0.0
    c1 = 9.0
    c2 = 7.0+j5.0
    
    Resultados dos métodos
    c3 = 9.0
    c4 = 63.0+j45.0
    

7.7. Onde estamos e para onde vamos?

Como Python é uma linguagem orientada a objetos, onde todos os tipos pré-definidos como int e flost correspondem a classes pré-definidas, a criação de novas classes é relativamente simples uma vez que você domine a sintaxe para criação de atributos e métodos de uma classe. Em particular, nessa aula exploramos métodos especiais que podemos utilizar ao definir novos tipos numéricos que nos permite escrever expressões aritméticas usando símbolos matemáticos tradicionais como +, -, * e /.

Na próxima aula vamos ver que o uso desses métodos especias tem implicações bem mais poderosas pois a nossa nova classe númerica herda comportamentos dos tipos númericos primitivos do Python!

7.9. Para praticar mais

  1. Escreva uma classe Ponto3D que representa um ponto no espaço tridimensional, ou seja, os atributos podem ser 3 reais representando as coordenadas do ponto. Sua classe deve permitir as seguintes operações:
    • sua distância à origem
    • sua distância até um outro ponto
    • ponto médio entre o ponto e outro ponto
    • __str__() que devolve uma string.

    DICA: a raiz quadrada de 2 pode ser calculada por 2 ** 0.5.