.. -*- coding: utf-8 -*- Expressões aritméticas, relacionais e lógicas com objetos ========================================================================= Nas aulas anteriores vimos que o Python permite utilizar os símbolos ``+``, ``*``, ``-``, ``/`` etc. para escrever expressões aritméticas com objetos de uma classe. Nessa aula vamos ver que, ao usar esses métodos especiais, nossa classe ``Complexo`` herda os comportamentos de outras classes numéricas, o que torna a POO ainda mais poderosa. Vamos ver também como usar outros tipos de operadores, como lógicos e relacionais, e como podemos combinar essas operações com objetos numéricos que pertençam a outras classes. Tópicos ------- - Mais sobre o uso de objetos de tipos numéricos em programas; - Métodos especiais para operadores relacionais e lógicos; - Expressões aritméticas, relacionais e lógicas com objetos de tipos numéricos; - Expressões com objetos de tipos distintos, como ``int + Fraction``. Introdução ---------- Considere a seguinte classe `Complexo` "simplificada": .. code:: python class Complexo: def __init__(self, r, i): self.real = float(r) self.imag = float(i) def __str__(self): if self.imag >= 0.0: return f"{self.real}+j{self.imag}" else: return f"{self.real}-j{abs(self.imag)}" def add(self, other): ''' (Complexo, Complexo) -> Complexo recebe dois complexos self e other e retorna um novo complexo com a soma self + other ''' r = self.real + other.real i = self.imag + other.imag return Complexo(r, i) def __mul__(self, other): ''' (Complexo, Complexo) -> Complexo recebe dois complexos self e other e retorna um novo complexo com o produto self * other ''' r = self.real * other.real - self.imag * other.imag i = self.imag * other.real + self.real * other.imag return Complexo(r, i) Embora na última atividade sugerimos apenas trocar o nome do método ``add()`` ou ``some()``, por ``__add__()``, o comportamento do método especial traz várias outras vantagens considerando que ``Complexo`` é um tipo numérico. Portanto, o método **regular** ``add()`` não é o mesmo que o método **especial** ``__add__()``. Os métodos especiais são palavras reservadas, indicadas por nomes entre asinhas, dois sublinhados ``"__"``, como em ``__mul__()``), que facilitam a escrita e leitura de programas em Python. Para usar um método regular, não especial, é necessário utilizar a **notação com ponto** para chamar o método explicitamente, como no trecho de código abaixo. .. code:: python c12 = Complexo(1, 2) c34 = Complexo(-3, 4) soma = c12.add(c34) # objeto c12, use seu método add com c34 print(f'({c12}).add({c34}) = {soma}') Estude a execução passo-a-passo do trecho de código a seguir clicando no botão ``Forward >`` para visualizar como o método regular ``add()`` é chamado. .. raw:: html .. .. code:: python def main(): c12 = Complexo(1, 2) c34 = Complexo(-3, 4) soma = c12.add(c34) print(f'({c12}).add({c34}) = {soma}') class Complexo: def __init__(self, r, i): self.real = float(r) self.imag = float(i) def __str__(self): if self.imag >= 0.0: return f"{self.real}+j{self.imag}" else: return f"{self.real}-j{abs(self.imag)}" def add(self, other): ''' (Complexo, Complexo) -> Complexo recebe dois complexos self e other e retorna um novo complexo com a soma self + other ''' r = self.real + other.real i = self.imag + other.imag return Complexo(r, i) if __name__ == '__main__': main() Observe que, na chamada ``c12.add(c34)`` (na linha 4), estamos pedindo ao objeto ``c12`` para usar seu método ``add``, passando o objeto ``c34`` como ``other``. Portanto, nesse contexto, o ``self`` na chamada de ``add`` é o próprio ``c12``. Chamada do método especial ``__mul__()`` ........................................ Para usar um método especial como ``__mul__()``, vimos que podemos até usar a notação com ponto, mas o uso do símbolo ``*`` facilita bastante o uso desses objetos em expressões numéricas, como no trecho de código abaixo. .. code:: python c12 = Complexo(1, 2) c34 = Complexo(-3, 4) prod1 = c12.__mul__(c34) # objeto c12, use seu método __mul__ com c34 print(f'({c12}).__mul__({c34}) = {prod1}') # mais natural usando * prod2 = c12 * c34 print(f'({c12}) * ({c34}) = {prod2}') Estude a execução passo-a-passo do trecho de código a seguir clicando no botão ``Forward >`` para visualizar como o método especial ``__mul__()`` pode ser chamado. Em particular, observe que na chamada ``c12 * c34`` usando o símbolo ``*``, o termo da esquerda (``c12``) usa o seu método ``__mul__()`` passando o termo da direita (``c34``) como ``other``. .. raw:: html Precedência dos operadores usando os métodos especiais ------------------------------------------------------- Além de simplificar a notação, outro grande benefício de usar os métodos especiais é que nossa classe herda os comportamentos comuns de outros tipos numéricos como, por exemplo, a prioridade dos operadores quando usados em expressões aritméticas. Você lembra qual a precedência de operadores para os números inteiros? Por exemplo qual o resultado da expressão inteira: .. code:: python x = 2 + 3 * 4 Sabemos que a multiplicação tem precedência sobre a adição e portanto o resultado deve ser ``2+(3*4)`` que é igual a ``14``, em vez de, por exemplo, de ``(2+3)*4`` que é ``20``. Portanto, a multiplicação e a divisão têm precedência sobre a soma e a subtração. Esse é o mesmo comportamento que esperamos desses operadores para números reais e, por que não, para frações, objetos do tipo ``Fraction``, e também para objetos do tipo ``Complexo``. Imagine, por hora, que nossa classe ``Complexo`` possua apenas os métodos regulares ``add()`` e ``mul()``. Considere então o seguinte trecho de código: .. code:: python a = Complexo(1, 2) b = Complexo(3, 4) c = Complexo(5, 6) x = a.add(b.mul(c)) # ou y = (a.add(b)).mul(c) Nesse caso, a prioridade das operações de soma e multiplicação é definida pelos parênteses. Felizmente, usando os métodos especiais ``__add__()`` e ``__mul__()``, podemos simplesmente escrever :math:`x = a + b * c` para obtermos o resultado desejado considerando a mesma prioridade que já estamos acostumados a usar e, ainda, podemos usar parênteses como :math:`y = (a+b)*c` para alterar essa prioridade. Você não acha que assim fica muito mais? Isso é possível pois, ao mudar o nome de ``add()`` para ``__add__()``, não estamos apenas simplificando a notação para usar o símbolo ``+``, mas estamos também herdando o comportamento da operação de soma, com relação aos outros operadores, de outros tipos numéricos nativos do Python como ``int`` e ``float``. O que fazer para somar um Complexo com um `int` ou `float`? ----------------------------------------------------------- O método especial ``__add__()``, e o regular ``add()``, apenas funciona quando o outro objeto também é ``Complexo``. O que fazer para adicionarmos um ``int`` ou ``float``? Na verdade, a rigor esse código simplificado funcionaria para qualquer objeto ``other`` que tenha os atributos ``.real`` e ``.imag``. Cá entre nós, como só conhecemos a classe ``Complexo`` que possui esses atributos, vamos dizer que essa implementação só funciona quando ``other`` for também da classe ``Complexo``. Mas e se quisermos adicionarmos um número real, objeto do tipo ``float``? Como um real não possui esses atributos, o método resultaria em erro. Experimente substituir o ``c34`` por um real e veja o que acontece. Felizmente é possível testar o tipo do objeto ``other`` antes de realizar a soma. Sabemos que, se ``other`` for do tipo ``float`` ou do tipo ``int``, então podemos considerar que ``other`` equivale a um complexo com parte imaginária nula (igual a zero). O seguinte trecho mostra uma alteração para tratar de adição com um ``float`` ou um ``int``. .. code:: python def __add__(self, other): ''' (Complexo, Complexo ou float ou int) -> Complexo recebe um complexo self e um (complexo ou float ou int) other e retorna um novo complexo com a soma self + other ''' # se o tipo de other é float ou int if type(other) is float or type(other) is int: r = self.real + other i = self.imag else: # vamos assumir que é outro Complexo, sem testar r = self.real + other.real i = self.imag + other.imag return Complexo(r, i) O seguinte programa pode ser usado para testar esse novo método: .. code:: python def main(): c12 = Complexo(1, 2) c34 = Complexo(-3, 4) soma = c12 + c34 print(f'{c12} + {c34} \t= {soma}') print(f'Correto esperado \t\t= -2.0+j6.0') print() # testa um float f154 = 15.4 sf = c12 + f154 print(f'{c12} + {f154} \t= {sf}') print(f'Correto esperado \t= 16.4+j2.0') print() # testa um int i99 = 99 si = c12 + i99 print(f'{c12} + {i99} \t= {si}') print(f'Correto esperado \t= 100.0+j2.0') Estude esse programa usando a execução passo-a-passo do trecho de código a seguir clicando no botão ``Forward >``. Observe que o Python simplesmente "decodifica" a expressão ``c12 + c34 `` para a forma com ponto ``c12.__add__(c34)``. .. raw:: html .. def main(): c12 = Complexo(1, 2) c34 = Complexo(-3, 4) soma = c12 + c34 print(f'{c12} + {c34} \t= {soma}') print(f'Correto esperado \t\t= -2.0+j6.0') print() # testa um float f154 = 15.4 sf = c12 + f154 print(f'{c12} + {f154} \t= {sf}') print(f'Correto esperado \t= 16.4+j2.0') print() # testa um int i99 = 99 si = c12 + i99 print(f'{c12} + {i99} \t= {si}') print(f'Correto esperado \t= 100.0+j2.0') class Complexo: def __init__(self, r, i): self.real = float(r) self.imag = float(i) def __str__(self): if self.imag >= 0.0: return f"{self.real}+j{self.imag}" else: return f"{self.real}-j{abs(self.imag)}" def __add__(self, other): ''' (Complexo, Complexo ou float ou int) -> Complexo recebe um complexo self e um (complexo ou float ou int) other e retorna um novo complexo com a soma self + other ''' # se o tipo de other é float ou int if type(other) is float or type(other) is int: r = self.real + other i = self.imag else: # vamos assumir que é outro Complexo, sem testar r = self.real + other.real i = self.imag + other.imag return Complexo(r, i) if __name__ == '__main__': main() Operações e a ordem de leitura ------------------------------- Observe que, como o método ``__add__()`` está definido na classe ``Complexo``, a chamada ``c12 + i99`` é válida mas a chamada ``i99 + c12``, como no trecho abaixo, não é válida. .. code:: python def main(): c12 = Complexo(1, 2) # testa um int i99 = 99 si = i99 + c12 print(f'{i99} + {c12} \t= {si}') print(f'Correto esperado \t= 100.0+j2.0') Isso porque o tipo nativo ``int`` não possui um método ``__add__()`` que recebe um ``Complexo`` com os atributos ``.real`` e ``.imag``. Como assim? ................... Lembre-se que o Python lê e decodifica a expressão ``c12 + c34`` da esquerda para a direita (ordem de leitura que nós usamos também em português) para a forma com ponto ``c12.__add__(c34)``, onde o termo a esquerda corresponde ao parâmetro ``self`` e o da direita ao parâmetro ``other`` do método ``__add__()``. Da mesma forma, o Python transforma a expressão ``c12 + i99`` na chamada ``c12.__add__(i99)``. Como ``i99`` é um objeto do tipo ``int``, o método ``__add__()`` sabe o que fazer quando ``other`` é do tipo ``int`` e não resulta em erro. Mas invertendo a ordem dos operandos na expressão, ou seja, ``i99 + c12``, ao converter a expressão para ``i99.__add__(c12)``, o tipo ``int`` não sabe o que fazer com um objeto ``Complexo``, o que resulta em erro. Fazendo o Python "ler" da direita para a esquerda ------------------------------------------------- Quando a gente escreve expressões do tipo ``2 - 3 + 4``, nossa convenção é executar os operadores de mesma prioridade, como ``-`` (subtração) e ``+`` (adição) na ordem de leitura, ou seja, da esquerda para a direita. Por isso o resultado do Python é 3 = ``(2-3)+4`` ao invés de -5 = ``2-(3+4)``. Do ponto de vista de um objeto ao executar um método especial como ``__add__()``, o objeto ``self`` tenta somar seu conteúdo com o conteúdo do objeto ``other`` a sua direita (o objeto que vem depois do ``+``). **Preste atenção nessa `gentileza` entre objetos do Python**: Quando o ``self.__add__()`` não sabe o que fazer com o ``other``, ele pergunta ao ``other`` se o ``other`` sabe o que fazer com o ``self``. Muito da hora! Como assim? .................... O ``self.__add__()`` tenta chamar o método **equivalente reverso** de ``other``. No caso do ``__add__()``, seu equivalente reverso é o método especial ``__radd__()``, ou seja, o nome entre asinhas recebe um prefixo ``r``. Você pode entender por **reverso** que o Python está tentando "ler" da direita para a esquerda para calcular a expressão, mas só quando ler da esquerda para a direita "não faz sentido". Nesse caso, se ``i99.__add__(c12)`` não faz sentido, o Python tenta então usar ``c12.__radd__(i99)``. O trecho de código abaixo ilustra uma solução para o método ``__radd__()`` da nossa classe ``Complexo``. .. code:: python def __radd__(self, other): return self + other Como assim, só isso? ..................... Observe a aparente simplicidade do trecho. No caso da chamada ``i99.__add__(c12)``, como ``i99`` não sabe o que fazer com ``c12``, o Python tenta usar ``c12.__radd__(i99)`` (**note o prefixo ``r``** em ``__radd__()``). Isso equivale a tentar resolver ``c12 + i99``. Observe que, para o método ``__radd__()``, o ``self`` é o objeto ``c12`` e o ``other`` é o objeto ``i99``. Isso o Python sabe resolver pois basta utilizar o método ``__add__()`` da classe ``Complexo``, que sabe o que fazer com um objeto ``int``. O resultado desejado é simplesmente o resultado da expressão ``self + other``. Estude o comportamento do ``__radd__()`` executando passo-a-passo o trecho de código a seguir, clicando no botão ``Forward >``. Observe que, ao chegar na linha 5, o programa desvia para a chamada ``__radd__()`` que, por sua vez, ao chamar ``self + other`` na linha 35, chama o método especial ``__add__()`` que calcula o resultado. .. raw:: html Mais sobre métodos especias ---------------------------------- O operador binário ``+`` é tipicamente usado para *adicionar* inteiros e reais, entre outros tipos numéricos. Mas vimos que em Python esse mesmo símbolo é usado também para *concatenar* listas e strings. Resumindo o que acabamos de ver, o uso desse operador binário está associado ao método especial ``__add__()``. Ao criar suas classes, você deve definir o comportamento esperado para objetos da sua classe usando expressões com esse símbolo. Além de facilitar a leitura, outra vantagem de implementar um método especial é que você ganha (herda) também outros comportamentos já associados ao uso desses símbolos, como prioridade dos operadores (o operador ``*`` tem precedência sobre ``+``), uso de parênteses, o operador ``+=`` (no caso do ``__add__()``) etc. Além dos métodos especiais associados aos operadores aritméticos como: * ``__add__(self,other)`` associado ao operador binário de adição ``+`` * ``__sub__(self,other)`` associado ao operador binário de subtração ``-`` * ``__neg__(self)`` associado ao operador unário de negação ``-`` * ``__mul__(self,other)`` associado ao operador binário de multiplicação ``*`` * ``__truediv__(self,other)`` associado ao operador binário de divisão ``/`` é possível criar uma classe que se utiliza também de operadores relacionais como * ``__le__(self,other)`` associado ao comparador ``<=`` * ``__eq__(self,other)`` associado ao comparador ``==`` * ``__ge__(self,other)`` associado ao comparador ``>=`` * ``__lt__(self,other)`` associado ao comparador ``<`` * ``__gt__(self,other)`` associado ao comparador ``>`` * ``__ne__(self,other)`` associado ao comparador ``!=`` e de operadores lógicos como ``__and__(self,other)`` e ``__or__(self,other)``. Todos os operadores **binários** tem o seu equivalente reverso, ou seja, que o Python usa quanto tenta entender a expressão da direita para a esquerda. Basta incluir o prefixo ``r``, como em ``__rmul__()``, ``__rtruediv__()``, ``__req__()`` etc. Uma lista completa dos métodos especiais você pode encontrar nessa [documentação do Python](https://docs.python.org/3/reference/datamodel.html). Exercícios ---------- Estenda a classe ``Fraction`` com os métodos especiais: * ``__add__()`` e ``__radd__()``; * ``__mul__()`` e ``__rmul__()``; * ``__sub__()`` e ``__rsub__()``; e * ``__truediv__()`` e ``__rtruediv__()``. Sua nova classe deve permitir realizar as operações de soma, multiplicação, subtração e divisão em expressões com frações, inteiros e reais como: .. code:: python f21 = Fraction(2,1) f23 = Fraction(2,3) x = f21 + f23 * 3 y = 6.0 / f23 - f21 Onde estamos e para onde vamos? ------------------------------- Nessa introdução a POO, focamos em tipos numéricos para motivar o uso de classes e objetos e explorar outros conceitos de POO e abstração de dados. A definição de classes e uso de objetos é uma importante ferramenta computacional que nos permite criar e trabalhar com novos tipos (abstratos) de dados. Nas próximas aulas vamos continuar treinando suas habilidades de abstração discutindo outros tipos de estruturas de dados. Para saber mais --------------- * `Tutorial de POO em Python `__. * `Classe Fração `__; .. Para praticar mais ------------------