.. -*- 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
------------------