8. Expressões aritméticas, relacionais e lógicas com objetos

8. Expressões aritméticas, relacionais e lógicas com objetos

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.

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

8.2. Introdução

Considere a seguinte classe Complexo “simplificada”:

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.

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.

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.

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

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.

8.3. 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:

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:

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 \(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 \(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.

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

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:

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

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

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.

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

8.6. 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!

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

def __radd__(self, other):
    return self + other

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

8.7. 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).

8.8. 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:

f21 = Fraction(2,1)
f23 = Fraction(2,3)
x = f21 + f23 * 3
y = 6.0 / f23 - f21

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