.. -*- coding: utf-8 -*-
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.
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 ``/``;
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.
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.
.. raw:: html
..
.. code:: python
class Fraction:
def __init__(self, n=0, d=1): # o valor default de n é 0 e de d é 1
self.num = n
self.den = d
def __str__(self):
return f"{self.num}/{self.den}"
# testes
r1 = Fraction() ## usa os dois valores default, n=0 e d=1
r2 = Fraction(4) ## usa o valor default d = 1
r3 = Fraction(3,2)
print(r1)
print(r2)
print(r3)
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``.
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:
.. code:: python
r4 = Fraction(d=-1)
e até:
.. code:: python
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:
.. code:: python
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:
.. raw:: html
..
.. code:: python
# Fraction_v2
class Fraction:
def __init__(self, a, b, n=0, d=1):
self.num = n
self.den = d
print("Construtor: ", a, b, n, d)
# observe que a e b não se tornam atributos de Fraction
def __str__(self):
return f"{self.num}/{self.den}"
# testes
r1 = Fraction('in', 'out')
r2 = Fraction('a', 'b', 4)
r3 = Fraction('x', 'y', d=5, n=3)
.. admonition:: 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).
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:
.. code:: python
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``.
.. raw:: html
..
source na pasta py/a05/classFraction01.py
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:
.. code:: python
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:
.. code:: python
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__``.
.. raw:: html
Exercícios
----------
#. 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.
.. code:: python
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:
.. code:: python
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
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!
Para saber mais
---------------
* `Tutorial de POO em Python `__.
* `Classe Fração `__;
..
* `Tutorial sobre método mágicos `__.
Para praticar mais
------------------
#. 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``.