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.