.. -*- coding: utf-8 -*- 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**. 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()``; 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. 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``. .. image:: figuras/poo1.png :alt: 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``. .. image:: figuras/poo2.png :alt: 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. .. image:: figuras/poo3.png :alt: 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. 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. .. _my_ClasseFraction: 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: .. raw:: html .. code:: Python class Fraction: def __init__(self, n, d): self.num = n self.den = d def __str__(self): return f"{self.num}/{self.den}" # testes a = Fraction(2, 3) print(a) b = Fraction(1, 4) print(b) 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. 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. .. admonition:: 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. 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)``. .. raw:: html .. code:: python class Fraction: def __init__(self, n, d): self.num = n # use self. para tornar num um atributo self.den = d # assim num e den ficam associados ao objeto div = n / d # div não é um atributo pois não tem self. print(div) def __str__(self): print(div) return f"{self.num}/{self.den}" # testes a = Fraction(2, 3) print(a) b = Fraction(1, 4) print(b) 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. .. admonition:: 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()``. .. 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__`` da seguinte forma: .. code:: python class Fraction: def __init__(self, n=0, d=1): self.num = n self.den = d def __str__(self): return f"{self.num}/{self.den}" # testes r1 = Fraction() r2 = Fraction(4) r3 = Fraction(3,2) print(r1) print(r2) print(r3) Observe 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: .. 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 é conhecido como **polimorfismo**, visto que permite que uma mesma função seja chamada de **formas diferentes** (com um número diferente de argumentos). E para alterar os valores dos atributos? ........................................ Em Python, podemos alterar os atributos diretamente .. code:: python 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 ``__``). .. raw:: html .. codelens:: Fraction_v3 class Fraction: def __init__(self, n=0, d=1): self.put(n, d) def __str__(self): return f"{self.num}/{self.den}" def get(self): return self.num, self.den def put(self, n=0, d=1): self.num, self.den = n, d # testes r1 = Fraction() print(r1) r1.put(4,2) print(r1) n, d = r1.get() print("n=%d, d=%d"%(n, d)) 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. 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. 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. .. raw:: html .. activecode:: Fraction_v4 class Fraction: def __init__(self, n=0, d=1): self.put(n, d) def __str__(self): return f"{self.num}/{self.den}" def get(self): return self.num, self.den def put(self, n=0, d=1): self.num, self.den = n, d def mul(self, other): n = self.num * other.num d = self.den * other.den return Fraction(n, d) # escreva aqui o seu codigo para os metodos # div # add # sub # e ao menos um teste para cada metodo # testes r1 = Fraction(2) r2 = Fraction(1,5) print(r1, '*', r2, '=>', r1.mul(r2)) # teste do div: # print(r1, '/', r2, '=>', r1.div(r2)) # # outros testes 2. Altere a classe ``Fraction`` para que, quando o denominador for 1, ele imprima apenas o valor do numerador. .. #. Altere o construtor da classe ``Fraction`` para que, quando o numerador e o denominador possam ser reduzidos, a fração reduzida seja armazenada. .. caso o resultado de uma operação (``add``, ``sub``, ``mul`` ou ``div``) puder ser reduzido, a operação retorne a fração reduzida. Exemplo: para a chamada ``Fraction(12, 20)`` o valor de ``self.num`` e ``self.den`` devem ser, respectivamente, 3 e 5. **Dica**: crie uma função (ou, se preferir, um método) com ``mdc`` que recebe 2 números inteiros ``a`` e ``b`` e retorna o máximo divisor comum entre ``a`` e ``b`` usando o algoritmo de Euclides. 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. Para saber mais --------------- - `Tutorial de POO em Python `__. - `Classes e objetos: frações `__;