Certas coisas me deixam meio dividido, como a sobrecarga de operadores. Deixei a sobrecarga de operadores de fora em uma decisão bastante pessoal, pois tinha visto gente demais abusar [deste recurso] no C++.[1]
Criador de Java
Em Python, podemos calcular juros compostos com esta fórmula:
interest = principal * ((1 + rate) ** periods - 1)Operadores que aparecem entre operandos, como + em 1 + rate, são
operadores infixos. No Python, operadores infixos podem lidar com qualquer
tipo arbitrário. Assim, se você está trabalhando com dinheiro de verdade, pode
armazenar principal, rate, e periods como números exatos—instâncias da
classe decimal.Decimal de Python. A mesma fórmula vai funcionar como escrita,
calculando um resultado exato.
Mas em Java, se você mudar de float para BigDecimal, para obter resultados
exatos, não é mais possível usar operadores infixos, porque naquela linguagem
eles só funcionam com tipos primitivos como float ou long.
Veja a mesma fórmula escrita em Java para funcionar com números BigDecimal:
BigDecimal interest = principal.multiply(BigDecimal.ONE.add(rate)
.pow(periods).subtract(BigDecimal.ONE));Está claro que operadores infixos tornam as fórmulas mais legíveis. A sobrecarga de operadores é necessária para suportar a notação infixa de operadores com tipos definidos pelo usuário ou em extensões compiladas, como os arrays da NumPy. Oferecer a sobrecarga de operadores em uma linguagem de alto nível e fácil de usar foi talvez uma das principais razões do grande sucesso de Python na ciência de dados, incluindo as aplicações científicas e financeiras.
Na [data_model_emulating_sec], vimos algumas implementações
triviais de operadores em uma classe básica Vector.
Escrevi os métodos __add__ e __mul__ no [ex_vector2d] do [ch_data_model]
para demonstrar como os métodos especiais suportam a sobrecarga de operadores,
mas deixei passar alguns problemas sutis naquelas implementações.
Além disso, no [ex_vector2d_v0] do [ch_pythonic_obj] notamos que o
método Vector2d.__eq__ considera True a seguinte expressão:
Vector(3, 4) == [3, 4]. Tal resultado pode fazer sentido ou não. Neste capítulo vamos
cuidar destes problemas, e
falaremos também de:
-
Como um método de operador infixo deve indicar que não consegue tratar um operando
-
Tipagem pato e tipagem ganso para lidar com operandos de tipos diferentes
-
O comportamento especial dos operadores de comparação rica (
==,>,{lte}, etc.) -
O tratamento padrão de operadores de atribuição aumentada, como
{iadd}, e como sobrecarregá-los
A tipagem ganso é uma
parte fundamental de Python, mas as ABCs numbers não são suportadas na tipagem
estática. Então, mudei o vector_v7.py: métodos do operador * adicionados para usar tipagem pato, em vez de uma
checagem explícita usando isinstance contra numbers.Real.[2]
Na primeira edição do Python Fluente, tratei do operador de multiplicação de
matrizes @ como uma mudança futura, pois o Python 3.5 ainda estava em desenvolvimento.
Agora o @ está integrado ao fluxo do capítulo na Usando @ como operador infixo.
Aproveitei a tipagem ganso para tornar a implementação de __matmul__
mais segura na primeira edição, sem comprometer sua flexibilidade.
A Para saber mais agora inclui algumas novas referências—incluindo um
post do blog de Guido van Rossum. Também inclui menções a duas bibliotecas
que demonstram usos interessantes da sobrecarga de operadores em contextos
não numéricos: pathlib e Scapy.
A sobrecarga de operadores permite que
objetos definidos pelo usuário suportem operadores infixos como + e
|, ou com operadores unários como - e ~.
De forma geral, em Python a notação de invocação de função (f()),
o acesso a atributos (p.x) e o acesso a itens e o fatiamento (v[0])
também são operadores, mas este capítulo trata dos operadores unários e infixos.
A sobrecarga de operadores tem má reputação em certos círculos. É um recurso que pode ser abusado, resultando em programadores confusos, bugs, e gargalos de desempenho inesperados. Mas se bem utilizada, possibilita APIs agradáveis de usar e código legível. Python alcança um bom equilíbrio entre flexibilidade, usabilidade e segurança, pela imposição de algumas limitações:
-
Não é permitido modificar o significado dos operadores para os tipos embutidos.
-
Não é permitido criar novos operadores, apenas sobrecarregar os existentes.
-
Alguns poucos operadores não podem ser sobrecarregados:
is,and,orenot(mas os operadores==,&,|, e~podem).
No [ch_seq_methods], na classe Vector, já apresentamos um operador infixo:
==, suportado pelo método __eq__. Neste capítulo, vamos melhorar a
implementação de __eq__ para lidar melhor com operandos de outros tipos além
de Vector. Entretanto, os operadores de comparação rica (==, !=, >, <,
>=, {lte}) são casos especiais de sobrecarga de operadores, então
começaremos sobrecarregando quatro operadores aritméticos em Vector: os
operadores unários - e +, seguido pelos infixos + e *.
Vamos começar pelo tópico mais fácil: operadores unários.
Na Referência da Linguagem Python, a seção Operações aritméticas unárias e bit a bit, cita três operadores unários, listados abaixo com os seus métodos especiais:
-implementado por__neg__-
Negativo aritmético unário. Se
xé42então-x == -42. +implementado por__pos__-
Positivo aritmético unário. Em geral,
x == +x, mas há alguns poucos casos em que isto não ocorre. Veja: Quando x e +x não são iguais (ao final desta seção). ~implementado por__invert__-
Negação binária, ou inversão bit a bit de um inteiro, definida como
~x == -(x+1). Sexé2então~x == -3, porque a representação binária de2é0010e-3é1101. Veja Complemento para dois na Wikipédia para entender esta representação de inteiros com sinal.
O capítulo Modelo de Dados na Referência da Linguagem Python_ também inclui a
função embutida abs() como um operador unário. O método especial associado é
__abs__, como já vimos.
É fácil suportar operadores unários. Basta implementar o método especial
apropriado, que receberá apenas um argumento: self. Use a lógica que fizer
sentido na sua classe, mas respeite a regra geral dos operadores: sempre
devolva um novo objeto. Em outras palavras, não modifique o receptor (self),
mas crie e devolva uma nova instância do tipo adequado.
No caso de - e +, o resultado será provavelmente uma instância da mesma
classe de self. Para o + unário, se o receptor for imutável você
deveria devolver self; caso contrário, devolva uma cópia de self. Para
abs(), o resultado deve ser um número escalar.[3]
Já no caso de ~, é difícil determinar o que seria um resultado razoável se
você não estiver lidando com bits de um número inteiro. No pacote de análise de
dados pandas, o til nega condições booleanas de
filtragem; veja exemplos na documentação do pandas, em
Boolean indexing (indexação booleana).
Como prometido acima, vamos implementar vários novos operadores na classe
Vector, do [ch_seq_methods]. O vector_v6.py: operadores unários - e + implementados. mostra o método
__abs__, que já estava no [ex_vector_v5] do [ch_seq_methods],
e os novos métodos __neg__ e __pos__ para operadores unários.
- e + implementados.link:../code/16-op-overloading/vector_v6.py[role=include]-
Para computar
-v, cria um novoVectorcom a negação de cada componente deself. -
Para computar
+v, cria um novoVectorcom cada componente deself.
Lembre-se de que instâncias de Vector são iteráveis, e o Vector.__init__
recebe um argumento iterável, por isso as implementações de __neg__ e
__pos__ ficaram tão simples.
Não vamos implementar __invert__. Se um usuário tentar escrever ~v para
uma instância de Vector, Python vai gerar um TypeError com uma mensagem
clara: “bad operand type for unary ~: 'Vector'” (operando inválido para o ~
unário: 'Vector').
O quadro a seguir trata de uma curiosidade que algum dia poderá ajudar você a
ganhar uma aposta sobre o + unário.
Todo mundo espera que x == +x, e isso é verdade no Python quase todo o tempo,
mas encontrei dois casos na biblioteca padrão onde x != +x.
O primeiro caso envolve a classe decimal.Decimal.
Você pode obter x != +x se x é uma instância de Decimal, criada em um dado
contexto aritmético e +x for então calculada em um contexto com definições
diferentes. Por exemplo, x é calculado em um contexto com uma determinada
precisão, mas a precisão do contexto é modificada e daí +x é avaliado.
Veja o Uma mudança na precisão do contexto aritmético pode fazer x se tornar diferente de +x.
x se tornar diferente de +xlink:../code/16-op-overloading/unary_plus_decimal.py[role=include]-
Obtém uma referência ao contexto aritmético global atual.
-
Define a precisão do contexto aritmético em
40. -
Computa
1/3usando a precisão atual. -
Inspeciona o resultado; há 40 dígitos após o ponto decimal.
-
one_third == +one_thirdéTrue. -
Diminui a precisão para
28—a precisão default deDecimal. -
Agora
one_third == +one_thirdéFalse. -
Inspeciona
+one_third; aqui há 28 dígitos após o'.'.
O fato é que cada ocorrência da expressão +one_third produz uma nova instância
de Decimal a partir do valor de one_third, mas usando a precisão do contexto
aritmético atual.
Encontrei o segundo caso onde x != +x na
documentação de collections.Counter. A classe Counter
implementa vários operadores aritméticos, incluindo o + infixo, para
somar a contagem de duas instâncias de Counter. Entretanto, por razões
práticas, a adição em Counter descarta do resultado qualquer item com contagem
negativa ou zero. E o + unário é um atalho para somar um Counter vazio,
produzindo um novo Counter, que preserva só as contagens maiores que
zero. Veja o O + unário produz um novo `Counter`sem as contagens negativas ou zero.
>>> ct = Counter('abracadabra')
>>> ct
Counter({'a': 5, 'r': 2, 'b': 2, 'd': 1, 'c': 1})
>>> ct['r'] = -3
>>> ct['d'] = 0
>>> ct
Counter({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})
>>> +ct
Counter({'a': 5, 'b': 2, 'c': 1})Como visto, +ct devolve um contador onde todas as contagens são maiores que
zero.
Agora voltamos à nossa programação normal.
A classe Vector é um tipo sequência, e a seção
Emulando tipos contêineres da documentação oficial do Python
diz que sequências devem suportar o operadores + para concatenação e *
para repetição. Entretanto, aqui vamos implementar + e * como operações
matemáticas de vetores, algo um pouco mais complicado porém mais útil para um
tipo Vector.
|
Tip
|
Usuários que desejem concatenar ou repetir instâncias de >>> v_concat = Vector(list(v1) + list(v2))
>>> v_repeat = Vector(tuple(v1) * 5) |
Somar dois vetores euclidianos resulta em um novo vetor cujos componentes são as somas dos componentes correspondentes dos operandos. Ilustrando:
>>> v1 = Vector([3, 4, 5])
>>> v2 = Vector([6, 7, 8])
>>> v1 + v2
Vector([9.0, 11.0, 13.0])
>>> v1 + v2 == Vector([3 + 6, 4 + 7, 5 + 8])
TrueE o que acontece se tentarmos somar duas instâncias de Vector de tamanhos
diferentes? Poderíamos gerar um erro, mas considerando as aplicações práticas
(tal como recuperação de informação), é melhor preencher o Vector menor com
zeros. Esse é o resultado que queremos:
>>> v1 = Vector([3, 4, 5, 6])
>>> v3 = Vector([1, 2])
>>> v1 + v3
Vector([4.0, 6.0, 5.0, 6.0])Dados esses requisitos básicos, podemos implementar __add__ como no
Método Vector.__add__, versão #1.
Vector.__add__, versão #1 # dentro da classe Vector
def __add__(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0.0) # (1)
return Vector(a + b for a, b in pairs) # (2)-
pairsé um gerador que produz tuplas(a, b), ondeavem deselfebdeother. Seselfeothertiverem tamanhos diferentes,fillvaluefornece os valores que faltam no o iterável mais curto. -
Um novo
Vectoré criado a partir de uma expressão geradora, produzindo uma soma para cada(a, b)depairs.
Note que __add__ devolve uma nova instância de Vector, sem modificar
self ou other.
|
Warning
|
Métodos especiais implementando operadores unários ou infixos não devem nunca
modificar o valor dos operandos. Espera-se que expressões com tais operandos
produzam resultados criando novos objetos. Só operadores de atribuição
aumentada podem modificar o primeiro operando ( |
O Método Vector.__add__, versão #1 permite somar um Vector a um Vector2d, a
uma tupla, como prova o Nossa versão #1 de Vector.__add__ também aceita objetos diferentes de Vector.
Vector.__add__ também aceita objetos diferentes de Vector>>> v1 = Vector([3, 4, 5])
>>> v1 + (10, 20, 30)
Vector([13.0, 24.0, 35.0])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> v1 + v2d
Vector([4.0, 6.0, 5.0])Os dois usos de + no Nossa versão #1 de Vector.__add__ também aceita objetos diferentes de Vector funcionam porque
__add__ usa zip_longest(…), capaz de consumir qualquer iterável, e a
expressão geradora que cria um novo Vector simplesmente efetua a operação a com os pares produzidos por
bzip_longest(…), então qualquer iterável que produza
números compatíveis com float servirá.
Entretanto, se trocarmos a ordem dos operandos, a soma de tipos diferentes falha.
Veja o A versão #1 de Vector.__add__ falha se o operador da esquerda não for um Vector.
Vector.__add__ falha se o operador da esquerda não for um Vector>>> v1 = Vector([3, 4, 5])
>>> (10, 20, 30) + v1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "Vector") to tuple
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> v2d + v1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Vector2d' and 'Vector'Para suportar operações envolvendo objetos de tipos diferentes, Python
implementa um mecanismo especial de despacho para os métodos especiais de
operadores infixos. Dada a expressão a + b, o interpretador executará os
seguintes passos (veja também a Fluxograma para computar a + b com __add__ e __radd__.):
-
Se
aimplementa__add__, Python invocaa.__add__(b)e devolve o resultado, a menos que sejaNotImplemented. -
Se
anão implementa__add__, ou a chamadaa.__add__(b)devolveNotImplemented, Python verifica sebimplementa__radd__, e então invocab.__radd__(a)e devolve o resultado, a menos que sejaNotImplemented. -
Se
bnão implementa__radd__, ou a chamadab.__radd__(a)devolveNotImplemented, Python gera umTypeErrorcom a mensagem "unsupported operand types" (tipos de operandos não suportados).
|
Tip
|
O método |
Assim, para fazer as somas de tipos diferentes no
A versão #1 de Vector.__add__ falha se o operador da esquerda não for um Vector funcionarem, precisamos implementar o método
Vector.__radd__, que Python vai invocar como alternativa, se o operando à
esquerda não implementar __add__, ou se implementar mas devolver
NotImplemented, indicando que não sabe como tratar o operando à direita.
|
Warning
|
Não confunda Por sua vez, |
A implementação viável mais simples de __radd__ aparece no Os métodos __add__ e __radd__ de Vector.
__add__ e __radd__ de Vector # dentro da classe Vector
def __add__(self, other): # (1)
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)
def __radd__(self, other): # (2)
return self + other-
Nenhuma mudança no
__add__do MétodoVector.__add__, versão #1; ele é listado aqui porque é usado por__radd__. -
__radd__apenas delega para__add__.
Muitas vezes, __radd__ pode ser simples assim: apenas a invocação do
operador apropriado, delegando para __add__ neste caso. Isso se aplica para
qualquer operador comutativo. O + é comutativo quando lida com números ou
com nossos vetores, mas não é comutativo ao concatenar sequências no Python.
Se __radd__ apenas invoca __add__, aqui está uma forma mais eficiente de
obter o mesmo efeito:
def __add__(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)
__radd__ = __add__Os métodos no Os métodos __add__ e __radd__ de Vector funcionam com objetos Vector ou com
qualquer iterável com itens numéricos, tal como um Vector2d, uma tupla de
inteiros ou um array de números de ponto flutuante. Mas se alimentado com um
objeto não-iterável, __add__ gera uma exceção com uma mensagem não muito
útil, como no O método Vector.__add__ precisa de operandos iteráveis.
Vector.__add__ precisa de operandos iteráveis>>> v1 + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "vector_v6.py", line 328, in __add__
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
TypeError: zip_longest argument #2 must support iterationE pior ainda, recebemos uma mensagem enganosa se um operando for iterável,
mas seus itens não puderem ser somados aos itens float no Vector. Veja
o O método Vector.__add__ exige um iterável com itens numéricos.
Vector.__add__ exige um iterável com itens numéricos>>> v1 + 'ABC'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "vector_v6.py", line 329, in __add__
return Vector(a + b for a, b in pairs)
File "vector_v6.py", line 243, in __init__
self._components = array(self.typecode, components)
File "vector_v6.py", line 329, in <genexpr>
return Vector(a + b for a, b in pairs)
TypeError: unsupported operand type(s) for +: 'float' and 'str'Tentei somar um Vector a uma str, mas a mensagem reclama de float e str.
Na verdade, os problemas no O método Vector.__add__ precisa de operandos iteráveis e no
O método Vector.__add__ exige um iterável com itens numéricos são mais profundos que meras mensagens de erro
obscuras: se um método especial de operando não é capaz de devolver um resultado
válido por incompatibilidade de tipos, ele tem que devolver NotImplemented e
não gerar um TypeError. Ao devolver NotImplemented, a porta fica aberta para
o outro operando executar a operação, quando Python tentar invocar o método
reverso em sua classe.
No espírito da tipagem pato, não vamos testar o tipo do operando other ou o
tipo de seus elementos. Vamos capturar as exceções e devolver NotImplemented.
Se o interpretador ainda não tiver invertido os operandos, tentará isso em seguida.
Se a invocação do método reverso devolver NotImplemented, então Python vai
gerar um TypeError com uma mensagem de erro padrão "unsupported operand
type(s) for +: 'Vector' and 'str'” (tipos de operandos não suportados para +:
Vector e `str`)
A implementação final dos métodos especiais de adição de Vector está no vector_v6.py: métodos do operador + adicionados a vector_v5.py ([ex_vector_v5] do [ch_seq_methods]).
+ adicionados a vector_v5.py ([ex_vector_v5] do [ch_seq_methods])link:../code/16-op-overloading/vector_v6.py[role=include]Observe que agora __add__ captura um TypeError e devolve NotImplemented.
|
Warning
|
Se um método de operador infixo gera uma exceção, ele interrompe o algoritmo de
despacho do operador. No caso específico de |
Agora que já sobrecarregamos o operador + com segurança, implementando __add__ e __radd__, vamos enfrentar outro operador infixo: *.
O que
significa Vector([1, 2, 3]) * x? Se x é um número escalar, isto é uma
"multiplicação por escalar", e o resultado deve ser um novo Vector com cada
componente multiplicado por x—também conhecida como multiplicação elemento a
elemento (elementwise multiplication):
>>> v1 = Vector([1, 2, 3])
>>> v1 * 10
Vector([10.0, 20.0, 30.0])
>>> 11 * v1
Vector([11.0, 22.0, 33.0])|
Note
|
Outro tipo de multiplicação envolvendo vetores é o produto escalar
(dot product). Os operandos de um produto escalar são dois vetores,
e o resultado é um número escalar (não um vetor).
É como uma multiplicação de matrizes, considerando um
vetor como uma matriz de 1 × N e o outro como uma matriz de N × 1.
Implementaremos o produto escalar em |
Voltando à nossa multiplicação por escalar, começamos novamente com os métodos
__mul__ e __rmul__ mais simples possíveis que possam funcionar:
# dentro da classe Vector
def __mul__(self, scalar):
return Vector(n * scalar for n in self)
def __rmul__(self, scalar):
return self * scalarEstes métodos funcionam, exceto quando recebem operandos incompatíveis.
O argumento scalar precisa ser um número que, quando multiplicado por um
float, produz outro float (porque nossa classe Vector armazena
um array de números de ponto flutuante). Então um complex não serve,
mas pode ser um int, um bool (bool é subclasse de int)
ou até uma instância de fractions.Fraction. No vector_v7.py: métodos do operador * adicionados, o método
__mul__ não faz nenhuma checagem de tipos explícita com scalar. Em vez
disso, o converte para float, e devolve NotImplemented se a conversão
falhar. É mais um exemplo prático de tipagem pato.
* adicionadosclass Vector:
typecode = 'd'
def __init__(self, components):
self._components = array(self.typecode, components)
# vários métodos omitidos no livro; código completo em
# https://github.com/fluentpython/example-code-2e
def __mul__(self, scalar):
try:
factor = float(scalar)
except TypeError: # (1)
return NotImplemented # (2)
return Vector(n * factor for n in self)
def __rmul__(self, scalar):
return self * scalar # (3)-
Se
scalarnão pode ser convertido parafloat… -
…não temos como lidar com ele, então devolvemos
NotImplemented, para permitir ao Python tentar__rmul__no operandoscalar. -
Neste exemplo,
__rmul__funciona bem apenas executandoself * scalar, que delega a operação para o método__mul__.
Com o vector_v7.py: métodos do operador * adicionados, é possível multiplicar um Vector por valores escalares
de tipos numéricos comuns e não tão comuns:
>>> v1 = Vector([1.0, 2.0, 3.0])
>>> 14 * v1
Vector([14.0, 28.0, 42.0])
>>> v1 * True
Vector([1.0, 2.0, 3.0])
>>> from fractions import Fraction
>>> v1 * Fraction(1, 3)
Vector([0.3333333333333333, 0.6666666666666666, 1.0])Agora que podemos multiplicar Vector por valores escalares, vamos ver como
implementar o produto de um Vector por outro Vector.
|
Note
|
Na primeira edição de Python Fluente, usei tipagem ganso no vector_v7.py: métodos do operador Outra alternativa seria checar com o protocolo Mas |
O símbolo @ é o prefixo de decoradores de função, mas desde 2015
também pode ser usado como um operador infixo.
Por muitos anos, o produto escalar (dot product) era escrito
como numpy.dot(a, b) na biblioteca NumPy.
A notação de invocação de função faz com que fórmulas mais longas sejam difíceis
de traduzir da notação matemática para Python,[4] então a comunidade de
computação numérica fez campanha pela
PEP 465—A dedicated infix operator for matrix multiplication
(Um operador infixo dedicado para multiplicação de matrizes),
que foi implementada no Python 3.5. Hoje podemos escrever a @ b
para computar o produto de dois arrays da NumPy.
O operador @ é suportado pelos métodos especiais __matmul__,
__rmatmul__ e __imatmul__, cujos nomes derivam de "matrix
multiplication". Até o Python 3.10, estes métodos não são usados em lugar algum
na biblioteca padrão, mas são reconhecidos pelo interpretador desde o Python
3.5, então nós e os desenvolvedores da NumPy podemos implementar o operador
@ em nossas classes. O analisador sintático de Python foi modificado para
aceitar o novo operador, pois a @ b era um erro de sintaxe até o Python 3.4.
Estes testes simples mostram como @ deve funcionar com instâncias de Vector:
>>> va = Vector([1, 2, 3])
>>> vz = Vector([5, 6, 7])
>>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7
True
>>> [10, 20, 30] @ vz
380.0
>>> va @ 3
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for @: 'Vector' and 'int'O resultado de va @ vz no exemplo acima é o mesmo que obtemos no NumPy
fazendo o produto escalar de arrays com os mesmos valores:
>>> import numpy as np
>>> np.array([1, 2, 3]) @ np.array([5, 6, 7])
38O vector_v7.py: métodos para o operador @ mostra o código dos métodos especiais relevantes na classe Vector.
@class Vector:
# vários métodos omitidos nesta listagem
def __matmul__(self, other):
if (isinstance(other, abc.Sized) and # (1)
isinstance(other, abc.Iterable)):
if len(self) == len(other): # (2)
return sum(a * b for a, b in zip(self, other)) # (3)
else:
raise ValueError('@ requires vectors of equal length.')
else:
return NotImplemented
def __rmatmul__(self, other):
return self @ other-
Ambos os operandos precisam implementar
__len__e__iter__… -
…e ter o mesmo tamanho, para permitir…
-
…uma linda aplicação de
sum,zipe uma expressão geradora.
|
Tip
|
O novo recurso de zip() no Python 3.10
Desde o Python 3.10, a função |
O vector_v7.py: métodos para o operador @ é um bom exemplo prático de tipagem ganso. Não usamos
isinstance(other, Vector), porque queremos oferecer mais flexibilidade para os
usuários. Suportamos operandos que sejam instâncias de abc.Sized e
abc.Iterable. Estas duas ABCs implementam o __subclasshook__, portanto
qualquer objeto que forneça __len__ e __iter__ satisfaz nosso teste—não
há necessidade de criar subclasses concretas dessas ABCs ou sequer registrar-se
com elas, como explicado na [subclasshook_sec]. Em particular, nossa classe
Vector não é subclasse nem de abc.Sized nem de abc.Iterable, mas passa os
testes de isinstance contra aquelas ABCs, pois implementa os métodos
necessários.
Vamos revisar os operadores aritméticos suportados pelo Python antes de mergulhar na categoria especial dos operadores de comparação rica (Operadores de comparação rica).
Ao implementar +, *, e @, vimos os padrões de programação mais comuns para operadores
infixos. As técnicas descritas são aplicáveis a todos os operadores listados na
Nomes dos métodos de operadores infixos (os operadores internos são usados para atribuição aumentada; operadores de comparação estão na [reversed_rich_comp_op_tbl]) (os operadores de atribuição aritmética serão tratados na
Operadores de atribuição aumentada).
| op | direto | reverso | interno | descrição |
|---|---|---|---|---|
|
|
|
|
Adição ou concatenação |
|
|
|
|
Subtração |
|
|
|
|
Multiplicação ou repetição |
|
|
|
|
Divisão exata |
|
|
|
|
Divisão inteira |
|
|
|
|
Módulo (resto) |
|
|
|
|
Devolve uma tupla com o quociente da divisão inteira e o módulo |
|
|
|
|
Exponenciação[5] |
|
|
|
|
Multiplicação de matrizes |
|
|
|
|
E binário (bit a bit) |
| |
|
|
|
OU binário (bit a bit) |
|
|
|
|
XOR binário (bit a bit) |
|
|
|
|
Deslocamento de bits para a esquerda |
|
|
|
|
Deslocamento de bits para a direita |
Operadores de comparação rica usam regras diferentes.
O tratamento
dos operadores de comparação rica ==, !=, >, <, >= e {lte} pelo
interpretador Python é similar ao que já vimos, com uma importante diferença:
não existem métodos reversos com o prefixo __r…__.
Os mesmos métodos são usados para invocações diretas ou reversas do
operador. As regras estão resumidas na Comparação rica: a última coluna mostra o resultado quando as tentativas devolvem NotImplemented ou o operando não implementa o método..
NotImplemented ou o operando não implementa o método.
| grupo | op | invocação direta | invocação reversa | quando não implementado |
|---|---|---|---|---|
igualdade |
|
|
|
Devolve |
|
|
|
Devolve |
|
ordenação |
|
|
|
Levanta |
|
|
|
Levanta |
|
|
|
|
Levanta |
|
|
|
|
Levanta |
Por exemplo, no caso de ==, tanto a chamada direta quanto a reversa invocam
__eq__, apenas permutando os argumentos. Uma chamada direta a __gt__
pode ser seguida de uma chamada reversa a __lt__, com os argumentos
permutados.
Nos casos de == e !=, se o método não existe no segundo operando,
ou devolve NotImplemented, os métodos correspondentes __eq__ e __ne__
herdados da classe object comparam os IDs dos objetos, então não ocorre TypeError.
Considerando estas regras, vamos revisar e aperfeiçoar o comportamento do método
Vector.__eq__, escrito assim no vector_v5.py ([ex_vector_v5] do [ch_seq_methods]):
class Vector:
# várias linhas omitidas
def __eq__(self, other):
return (len(self) == len(other) and
all(a == b for a, b in zip(self, other)))Este método produz os resultados do Comparando um Vector a um Vector, a um Vector2d, e a uma tupla.
Vector a um Vector, a um Vector2d, e a uma tupla>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb # (1)
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d # (2)
True
>>> t3 = (1, 2, 3)
>>> va == t3 # (3)
True-
Duas instâncias de
Vectorcom componentes numéricos iguais são iguais. -
Um
Vectore umVector2dtambém são iguais se seus componentes são iguais. -
Um
Vectortambém é considerado igual a uma tupla ou qualquer sequência com itens escalares de valor igual.
O último resultado no Comparando um Vector a um Vector, a um Vector2d, e a uma tupla pode ser indesejável. Queremos mesmo
que um Vector seja considerado igual a uma tupla contendo os mesmos números?
Não tenho uma regra fixa sobre isso; depende do contexto da aplicação. O "Zen of
Python" diz:
Em face da ambiguidade, rejeite a tentação de adivinhar.
Liberalidade excessiva na avaliação de operandos pode levar a resultados surpreendentes, e programadores odeiam surpresas.
Buscando inspiração no próprio Python, vemos que [1, 2] == (1, 2) é False.
Então, seremos conservadores e faremos checagem de tipos. Se o
segundo operando for uma instância de Vector (ou uma instância de uma
subclasse de Vector), então usaremos a mesma lógica do __eq__ atual. Caso
contrário, devolvemos NotImplemented e deixamos Python cuidar do caso. Veja o
vector_v8.py: __eq__ aperfeiçoado na classe Vector.
__eq__ aperfeiçoado na classe Vectorlink:../code/16-op-overloading/vector_v8.py[role=include]-
Se o operando
otheré uma instância deVector(ou de uma subclasse deVector), executa a comparação como antes. -
Caso contrário, devolve
NotImplemented.
Rodando os testes do Comparando um Vector a um Vector, a um Vector2d, e a uma tupla com o novo Vector.__eq__ do
vector_v8.py: __eq__ aperfeiçoado na classe Vector, obtemos os resultados do Mesmas comparações do [eq_initial_demo]: o último resultado mudou.
>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb # (1)
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d # (2)
True
>>> t3 = (1, 2, 3)
>>> va == t3 # (3)
False-
Mesmo resultado de antes, como esperado.
-
Mesmo resultado de antes, mas por quê? Explicação a seguir.
-
Resultado diferente; era o que queríamos. Mas por que isso funciona? Continue lendo…
Dos três resultados no Mesmas comparações do [eq_initial_demo]: o último resultado mudou, o primeiro não é novidade, mas os
dois últimos foram causados por __eq__ devolver NotImplemented no
vector_v8.py: __eq__ aperfeiçoado na classe Vector. Eis o que acontece no exemplo com um Vector e um
Vector2d, vc == v2d, passo a passo:
-
Para avaliar
vc == v2d, Python invocaVector.eq(vc, v2d). -
Vector.__eq__(vc, v2d)verifica quev2dnão é umVectore devolveNotImplemented. -
Diante do resultado
NotImplemented, Python tentaVector2d.__eq__(v2d, vc). -
Vector2d.__eq__(v2d, vc)transforma os dois operandos em tuplas e os compara: o resultado éTrue(o código deVector2d.__eq__está no [ex_vector2d_v3_full] do [ch_pythonic_obj]).
Já para a comparação va == t3, entre Vector e tuple no Mesmas comparações do [eq_initial_demo]: o último resultado mudou,
os passos são:
-
Para avaliar
va == t3, Python invocaVector.__eq__(va, t3). -
Vector.__eq__(va, t3)verifica quet3não é umVectore devolveNotImplemented. -
Diante do resultado
NotImplemented, Python tentatuple.__eq__(t3, va). -
tuple.__eq__(t3, va)não tem a menor ideia do que seja umVector, então devolveNotImplemented. -
No caso especial de
==, se a chamada reversa devolveNotImplemented, Python compara os IDs dos objetos, como último recurso.
Não precisamos implementar __ne__ para !=, pois o comportamento
alternativo do __ne__ herdado de object nos serve: quando __eq__ é
definido e não devolve NotImplemented, __ne__ devolve a negação booleana
do resultado de __eq__.
Em outras palavras, dados os mesmos objetos que usamos no Mesmas comparações do [eq_initial_demo]: o último resultado mudou, os
resultados de != são consistentes:
>>> va != vb
False
>>> vc != v2d
False
>>> va != (1, 2, 3)
TrueO __ne__ herdado de object funciona como o código abaixo (mas
o original é escrito em C):[6]
def __ne__(self, other):
eq_result = self == other
if eq_result is NotImplemented:
return NotImplemented
else:
return not eq_resultVimos o básico da sobrecarga de operadores infixos. Agora veremos uma categoria diferente: os operadores de atribuição aumentada.
Nossa
classe Vector já suporta os operadores de atribuição aumentada {iadd} e *=.
Isso acontece porque a atribuição aumentada trabalha com sequências imutáveis
criando novas instâncias e re-vinculando a variável à esquerda do operador.
O Usando {iadd} e *= com instâncias de Vector os mostra em ação.
{iadd} e *= com instâncias de Vector>>> v1 = Vector([1, 2, 3])
>>> v1_alias = v1 # (1)
>>> id(v1) # (2)
4302860128
>>> v1 += Vector([4, 5, 6]) # (3)
>>> v1 # (4)
Vector([5.0, 7.0, 9.0])
>>> id(v1) # (5)
4302859904
>>> v1_alias # (6)
Vector([1.0, 2.0, 3.0])
>>> v1 *= 11 # (7)
>>> v1 # (8)
Vector([55.0, 77.0, 99.0])
>>> id(v1)
4302858336-
Cria um alias, para podermos inspecionar o objeto
Vector([1, 2, 3])mais tarde. -
Verifica o
iddoVectorinicial, vinculado av1. -
Executa a adição aumentada.
-
O resultado esperado…
-
…mas foi criado um novo
Vector. -
Inspeciona
v1_aliaspara confirmar que oVectororiginal não foi alterado. -
Executa a multiplicação aumentada.
-
Novamente, o resultado é o esperado, mas um novo
Vectorfoi criado.
Se uma classe não implementa os métodos internos listados na
Nomes dos métodos de operadores infixos (os operadores internos são usados para atribuição aumentada; operadores de comparação estão na [reversed_rich_comp_op_tbl]), os operadores de atribuição aumentada funcionam
como açúcar sintático: a += b é avaliado exatamente como a = a + b. Este
é o comportamento esperado para tipos imutáveis, e se você fornecer __add__,
então {iadd} funcionará sem qualquer código adicional.
Entretanto, se você implementar um método interno tal como __iadd__,
aquele método será chamado para computar o resultado de a += b. Como indica
seu nome, espera-se que esses operadores modifiquem internamente o operando da
esquerda[7],
e não criem um novo objeto como resultado.
|
Warning
|
Nunca devemos implementar métodos internos para atribuição aumentada
em tipos imutáveis como nossa classe |
Para mostrar o código de um método interno de atribuição aumentada, vamos
estender a classe BingoCage do [ex_tombola_bingo] do [ch_ifaces_prot_abc] para implementar
__add__ e __iadd__.
Vamos chamar a subclasse de AddableBingoCage. Os doctests da classe
(O operador + cria uma nova instância de AddableBingoCage)
mostram o comportamento esperado do operador +.
+ cria uma nova instância de AddableBingoCagelink:../code/16-op-overloading/bingoaddable.py[role=include]-
Cria uma instância de
globecom cinco itens (cada uma dasvowels). -
Extrai um dos itens, e verifica que é uma das
vowels. -
Confirma que
globetem agora quatro itens. -
Cria uma segunda instância, com três itens.
-
Cria uma terceira instância pela soma das duas anteriores. Esta instância tem sete itens.
-
Tentar adicionar uma
AddableBingoCagea umalistfalha com umTypeError. A mensagem de erro é produzida pelo interpretador de Python quando nosso método__add__devolveNotImplemented.
Como uma AddableBingoCage é mutável, o Uma AddableBingoCage existente pode ser carregada com {iadd} (continuando do O operador + cria uma nova instância de AddableBingoCage) mostra como
ela funcionará quando implementarmos __iadd__.
AddableBingoCage existente pode ser carregada com {iadd} (continuando do O operador + cria uma nova instância de AddableBingoCage)link:../code/16-op-overloading/bingoaddable.py[role=include]-
Cria um alias para podermos checar a identidade do objeto mais tarde.
-
globetem quatro itens aqui. -
Uma instância de
AddableBingoCagepode receber itens de outra instância da mesma classe. -
O operador à direita de
{iadd}também pode ser qualquer iterável. -
Durante todo esse exemplo,
globesempre se refere ao mesmo objeto queglobe_orig. -
Tentar adicionar um não-iterável a uma
AddableBingoCagefalha com uma mensagem de erro apropriada.
Observe que o operador {iadd} é mais liberal que + quanto ao segundo
operando. Com +, queremos que ambos os operandos sejam do mesmo tipo
(neste caso, AddableBingoCage), pois se aceitássemos tipos diferentes, isso
poderia causar confusão quanto ao tipo do resultado, violando a propriedade
comutativa da adição. Com o {iadd}, a situação é mais clara: o objeto à
esquerda do operador é atualizado internamente, então não há dúvida quanto ao
tipo do resultado.
|
Tip
|
Validei os comportamentos diversos de |
Agora que vimos o comportamento desejado para AddableBingoCage, podemos
estudar sua implementação no bingoaddable.py: AddableBingoCage é subclasse de BingoCage com suporte aos operadores + e {iadd}. Lembre-se de que BingoCage,
([ex_tombola_bingo] do [ch_ifaces_prot_abc]), é uma subclasse concreta da ABC Tombola do
[ex_tombola_abc] do [ch_ifaces_prot_abc].
AddableBingoCage é subclasse de BingoCage com suporte aos operadores + e {iadd}link:../code/16-op-overloading/bingoaddable.py[role=include]-
AddableBingoCageestendeBingoCage. -
Nosso
__add__só vai funcionar se o segundo operando for uma instância deTombola. -
Em
__iadd__, obtém os itens deother, se for uma instância deTombola. -
Caso contrário, tenta obter um iterador sobre
other(estudaremos a função embutidaiterno [ch_generators]). -
Se aquilo falhar, gera uma exceção explicando o que o usuário deve fazer. Sempre que possível, mensagens de erro devem orientar o usuário para a solução.
-
Se chegamos até aqui, podemos carregar o
other_iterableemself. -
Muito importante: os métodos especiais de atribuição aumentada de objetos mutáveis devem devolver
self. É o que os usuários esperam.
Podemos resumir toda a ideia dos operadores de atribuição interna comparando
as instruções return que devolvem os resultados em __add__ e em
__iadd__ no bingoaddable.py: AddableBingoCage é subclasse de BingoCage com suporte aos operadores + e {iadd}:
__add__: O resultado é computado chamando o construtor AddableBingoCage
para criar uma nova instância.
__iadd__: O resultado é self, após ele ter sido modificado.
Uma última observação sobre o bingoaddable.py: AddableBingoCage é subclasse de BingoCage com suporte aos operadores + e {iadd}: não implementei __radd__
em AddableBingoCage, porque não há necessidade. O método direto __add__ só
vai lidar com operandos do mesmo tipo à direita, então se Python tentar computar
a + b, onde a é uma AddableBingoCage e b não, devolvemos
NotImplemented—talvez a classe de b possa fazer isso funcionar. Mas
se a expressão for b + a e b não for uma AddableBingoCage, e devolver
NotImplemented, então é melhor deixar Python desistir e gerar um TypeError,
pois não temos como tratar b.
|
Tip
|
Se um método de operador infixo direto (por exemplo |
Assim terminamos nossa exploração de sobrecarga de operadores no Python.
Começamos o capítulo revisando
algumas restrições impostas pelo Python à sobrecarga de operadores: é impossível
redefinir operadores nos tipos embutidos, a sobrecarga está limitada aos
operadores existentes, e alguns operadores não podem ser sobrecarregados (is,
and, or, not).
Colocamos a mão na massa com os operadores unários, implementando __neg__ e
__pos__. A seguir vieram os operadores infixos, começando por +,
suportado pelo método __add__. Vimos que operadores unários e infixos devem
produzir resultados criando novos objetos, sem nunca modificar seus operandos.
Para suportar operações com outros tipos, devolvemos o valor especial
NotImplemented (não uma exceção) permitindo ao interpretador tentar novamente
chamando o método especial reverso do segundo operando (por exemplo,
__radd__). O algoritmo usado pelo Python para tratar operadores infixos está
resumido no fluxograma da Fluxograma para computar a + b com __add__ e __radd__..
Misturar operandos de mais de um tipo exige detectar os operandos que não
podemos tratar. Neste capítulo fizemos isso de duas maneiras: ao modo da tipagem
pato, apenas fomos em frente e tentamos a operação, capturando uma exceção de
TypeError se ela acontecesse; mais tarde, em __mul__ e __matmul__,
usamos um teste isinstance explícito. Há prós e contras nas duas abordagens:
tipagem pato é mais flexível, mas a checagem explícita de tipo é mais
previsível.
De modo geral, bibliotecas devem aproveitar a tipagem pato para lidar objetos
de diferentes tipos, desde que eles suportem as operações
necessárias. Entretanto, o algoritmo de despacho de operadores de Python pode
produzir mensagens de erro enganosas ou resultados inesperados quando combinado
com a tipagem pato. Por essa razão, a disciplina da checagem de tipos com
invocações de isinstance contra ABCs é muitas vezes útil quando escrevemos
métodos especiais para sobrecarga de operadores. Esta é a técnica batizada de
tipagem ganso (goose typing) por Alex Martelli—como vimos na
[goose_typing_sec]. A tipagem ganso é um compromisso entre a flexibilidade e a
segurança, porque os tipos definidos pelo usuário, existentes ou futuros, podem
ser declarados como subclasses reais ou virtuais de uma ABC. Além disso, se uma
ABC implementa o __subclasshook__, objetos podem então passar por checagens
com isinstance contra aquela ABC apenas fornecendo os métodos exigidos—sem
necessidade de ser uma subclasse ou de se registrar com a ABC.
O próximo tópico tratado foram os operadores de comparação rica. Implementamos
== com __eq__ e descobrimos que Python oferece uma implementação
conveniente de != no __ne__ herdado da classe base object. A forma como
Python avalia esses operadores, bem como >, <, >=, e {lte}, é um pouco
diferente, com uma lógica especial para a escolha do método reverso, e um
tratamento alternativo para == e != que nunca gera erros, pois a classe
object já implementa os métodos necessários.
Na última seção, nos concentramos nos operadores de atribuição aumentada. Vimos
que Python os trata, por default, como uma combinação do operador simples
seguido de uma atribuição: a {iadd} b é avaliado exatamente como
a = a + b.
Isto sempre cria um novo objeto, então funciona para tipos mutáveis ou
imutáveis.
Para objetos mutáveis, podemos implementar métodos especiais de atualização
interna, tal como __iadd__ para {iadd}, e alterar o valor do operando à
esquerda. Para demonstrar isto na prática, implementamos uma subclasse de
BingoCage, suportando {iadd} para adicionar itens ao reservatório de itens
para sorteio, de modo similar à forma como o tipo embutido list suporta
{iadd} como um atalho para o método list.extend(). Vimos que +
tende a ser mais estrito que {iadd} em relação aos tipos aceitos. Em
sequências, + normalmente exige que ambos os operandos sejam do mesmo
tipo, enquanto {iadd} muitas vezes aceita qualquer iterável como o operando à
direita do operador.
Guido van Rossum escreveu uma boa apologia da sobrecarga de operadores em Why operators are useful (Porque operadores são úteis). Trey Hunner postou Tuple ordering and deep comparisons in Python (Ordenação de tuplas e comparações profundas em Python), argumentando que os operadores de comparação rica de Python são mais flexíveis e poderosos do que os programadores vindos de outras linguagens costumam pensar.
A sobrecarga de operadores é uma área da programação em Python onde testes com
isinstance são comuns. A melhor prática relacionada a tais testes é a tipagem
ganso, tratada na [goose_typing_sec]. Se você pulou essa parte, assegure-se de
voltar lá e ler aquela seção.
A principal referência para os métodos especiais de operadores é o capítulo
Modelo de Dados na documentação de Python. Outra
leitura relevante é
Implementando as operações aritméticas
no módulo numbers da biblioteca padrão de Python.
Um exemplo brilhante de sobrecarga de operadores apareceu no pacote
pathlib, a partir do Python 3.4. Sua classe Path
sobrecarrega o operador / para construir caminhos do sistema de arquivos a
partir de strings, como mostra o exemplo abaixo, da documentação:
>>> p = Path('/etc')
>>> q = p / 'init.d' / 'reboot'
>>> q
PosixPath('/etc/init.d/reboot')Outro exemplo não aritmético de sobrecarga de operadores está na biblioteca
Scapy, usada para "enviar, farejar, dissecar e forjar
pacotes de rede". Na Scapy, o operador / cria pacotes empilhando campos de
diferentes camadas da rede. Veja Stacking layers
(Empilhando camadas) para mais detalhes.
Se você está prestes a implementar operadores de comparação, estude
functools.total_ordering. Esse é um decorador de classes que gera
automaticamente os métodos para todos os operadores de comparação rica em
qualquer classe que defina ao menos alguns deles. Veja a
documentação do módulo functools.
Se tiver curiosidade sobre o despacho de métodos de operadores em linguagens com tipagem dinâmica, duas leituras fundamentais são A Simple Technique for Handling Multiple Polymorphism (Uma técnica simples para tratar polimorfismo múltiplo), de Dan Ingalls (membro da equipe original de Smalltalk), e Arithmetic and Double Dispatching in Smalltalk-80 (Aritmética e despacho duplo no Smalltalk-80), de Kurt J. Hebel e Ralph Johnson (Johnson ficou famoso como um dos autores do livro Padrões de Projetos original).
Os dois artigos discutem em profundidade o poder do polimorfismo em linguagens com tipagem dinâmica, como Smalltalk, Python e Ruby. Python não implementa despacho duplo exatamente como descrito naqueles artigos. O algoritmo de despacho duplo em Python, usando operadores diretos e reversos, é mais fácil de suportar em classes definidas pelo usuário que o despacho duplo clássico, mas exige tratamento especial pelo interpretador. Por outro lado, o despacho duplo clássico é uma técnica geral, que pode ser usada no Python ou em qualquer linguagem orientada a objetos, para além do contexto específico de operadores infixos. E, de fato, Ingalls, Hebel e Johnson usam exemplos muito diferentes para descrever essa técnica.
O texto The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling (A Família de Linguagens C: entrevista com Dennis Ritchie, Bjarne Stroustrup, e James Gosling), de onde tirei a epígrafe deste capítulo, apareceu na Java Report, 5(7), julho de 2000, e na C++ Report, 12(7), julho/agosto de 2000, juntamente com outros trechos que usei no Ponto de Vista deste capítulo (logo adiante). Se você se interessa pelo design de linguagens de programação, faça um favor a si mesmo e leia aquela entrevista.
Sobrecarga de operadores: prós e contras
James Gosling, citado no início deste capítulo, tomou a decisão consciente de excluir a sobrecarga de operadores quando projetou o Java. Na entrevista The C Family of Languages ele diz:
Talvez uns 20 a 30% da população acha que sobrecarga de operadores é obra do demônio; alguém fez algo com sobrecarga de operadores que realmente os tirou do sério, porque usaram algo como + para inserção em listas, e isso torna a vida muito, muito confusa. Muito do problema vem do fato de existirem apenas uma meia dúzia de operadores que podem ser sobrecarregados de forma razoável, mas existem milhares ou milhões de operadores que as pessoas gostariam de definir—então é preciso escolher, e muitas vezes as escolhas entram em conflito com a sua intuição.
Guido van Rossum escolheu o caminho do meio no suporte à sobrecarga de
operadores: ele não deixou a porta aberta para que os usuários criassem novos
operadores arbitrários como {lte}> ou :-), evitando uma Torre de Babel
de operadores customizados, e que o analisador sintático de Python
continue simples. Python também não permite a sobrecarga dos operadores dos
tipos embutidos, outra limitação que promove a legibilidade e o desempenho
previsível.
Gosling continua:
E então há uma comunidade de aproximadamente 10% que havia de fato usado a sobrecarga de operadores de forma apropriada, e que realmente gostavam disso, e para quem isso era realmente importante; essas são quase exclusivamente pessoas que fazem trabalho numérico, onde a notação é muito importante para avivar a intuição [das pessoas], porque elas vêm com uma intuição sobre o que
+significa, e a poder dizera + b, onde a e b são números complexos ou matrizes ou alguma outra coisa, realmente faz sentido.
Claro, há benefícios em não permitir a sobrecarga de operadores em uma linguagem. Já ouvi o argumento de que C é melhor que C++; para programação de sistemas, porque a sobrecarga de operadores em C++ pode fazer com que operações dispendiosas pareçam triviais. Duas linguagens modernas bem sucedidas, que compilam para executáveis binários, fizeram escolhas opostas: Go não tem sobrecarga de operadores, Rust tem.
Mas operadores sobrecarregados, quando usados de forma sensata, tornam o código mais fácil de ler e escrever. É um ótimo recurso em uma linguagem de alto nível moderna.
Um exemplo de avaliação preguiçosa
Se você olhar de perto o traceback no O método Vector.__add__ exige um iterável com itens numéricos, vai
encontrar evidências da avaliação preguiçosa de
expressões geradoras. O Mesmo que o [ex_vector_error_iter_not_add] é o mesmo
traceback, agora com explicações.
>>> v1 + 'ABC'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "vector_v6.py", line 329, in __add__
return Vector(a + b for a, b in pairs) # (1)
File "vector_v6.py", line 243, in __init__
self._components = array(self.typecode, components) # (2)
File "vector_v6.py", line 329, in <genexpr>
return Vector(a + b for a, b in pairs) # (3)
TypeError: unsupported operand type(s) for +: 'float' and 'str'-
A chamada a
Vectorrecebe uma expressão geradora como seu argumentocomponents. Nenhum problema nesse estágio. -
A genexp
componentsé passada para o construtor dearray. Dentro do construtor dearray, Python tenta iterar sobre a genexp, causando a avaliação do primeiro itema + b. É quando ocorre oTypeError. -
A exceção se propaga para a chamada ao construtor de
Vector, onde é relatada.
Isso mostra como a expressão geradora é avaliada no último instante possível, e não onde é definida no código-fonte.
Se, por outro lado, o construtor de Vector fosse invocado como
Vector([a + b for a, b in pairs]), então a exceção ocorreria bem ali,
porque a compreensão de lista tentou criar uma list para ser passada como
argumento para a chamada a Vector(). O corpo de Vector.__init__
nunca seria alcançado.
O [ch_generators] vai tratar das expressões geradoras em detalhes, mas eu não queria deixar essa demonstração acidental de sua natureza preguiçosa passar despercebida.
numbers é explicado na [numbers_abc_proto_sec].
int, float, decimal.Decimal e fraction.Fraction são escalares, mas um complex não é um escalar.
pow pode receber um terceiro argumento opcional, modulo: pow(a, b, modulo), também suportado pelos métodos especiais quando invocados diretamente (por exemplo, a.__pow__(b, modulo)).
object.__eq__ e object.__ne__ está na função object_richcompare em Objects/typeobject.c, no código-fonte do CPython.
