Skip to content

Latest commit

 

History

History
1548 lines (1257 loc) · 63.6 KB

File metadata and controls

1548 lines (1257 loc) · 63.6 KB

Sobrecarga de operadores

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]

— James Gosling
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

Novidades neste capítulo

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.

Introdução à sobrecarga de operadores

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, or e not (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.

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 é 42 entã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). Se x é 2 então ~x == -3, porque a representação binária de 2 é 0010 e -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.

Example 1. vector_v6.py: operadores unários - e + implementados.
link:../code/16-op-overloading/vector_v6.py[role=include]
  1. Para computar -v, cria um novo Vector com a negação de cada componente de self.

  2. Para computar +v, cria um novo Vector com cada componente de self.

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.

Quando x e +x não são iguais

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.

Example 2. Uma mudança na precisão do contexto aritmético pode fazer x se tornar diferente de +x
link:../code/16-op-overloading/unary_plus_decimal.py[role=include]
  1. Obtém uma referência ao contexto aritmético global atual.

  2. Define a precisão do contexto aritmético em 40.

  3. Computa 1/3 usando a precisão atual.

  4. Inspeciona o resultado; há 40 dígitos após o ponto decimal.

  5. one_third == +one_third é True.

  6. Diminui a precisão para 28—a precisão default de Decimal.

  7. Agora one_third == +one_third é False.

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

Example 3. 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.

Sobrecarregando + para adição de Vector

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 Vector podem convertê-las para tuplas ou listas, aplicar o operador e convertê-las de volta—graças ao fato de Vector ser iterável e poder ser criado a partir de um iterável:

>>> 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])
True

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

Example 4. Método 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)
  1. pairs é um gerador que produz tuplas (a, b), onde a vem de self e b de other. Se self e other tiverem tamanhos diferentes, fillvalue fornece os valores que faltam no o iterável mais curto.

  2. Um novo Vector é criado a partir de uma expressão geradora, produzindo uma soma para cada (a, b) de pairs.

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 (self), quando ele é mutável, como discutido na Operadores de atribuição aumentada.

Example 5. Nossa versão #1 de 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
b
com os pares produzidos por zip_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.

Example 6. A versão #1 de 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__.):

  1. Se a implementa __add__, Python invoca a.__add__(b) e devolve o resultado, a menos que seja NotImplemented.

  2. Se a não implementa __add__, ou a chamada a.__add__(b) devolve NotImplemented, Python verifica se b implementa __radd__, e então invoca b.__radd__(a) e devolve o resultado, a menos que seja NotImplemented.

  3. Se b não implementa __radd__, ou a chamada b.__radd__(a) devolve NotImplemented, Python gera um TypeError com a mensagem "unsupported operand types" (tipos de operandos não suportados).

Tip

O método __radd__ é chamado de variante "reversa" ou "refletida" de __add__. Adotei o termo geral "métodos especiais reversos". A documentação de Python usa os dois termos. O capítulo Modelo de Dados usa "refletido", mas em Implementando operações aritméticas, a documentação do módulo menciona métodos "adiante" (forward) e "reverso" (reverse), uma terminologia que considero melhor, pois "adiante" e "reverso" descrevem sentidos opostos, mas o oposto de "refletido" não é tão evidente.

Fluxograma de operador
Figure 1. Fluxograma para computar a + b com __add__ e __radd__.

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 NotImplemented com NotImplementedError. O primeiro é um valor singleton especial, que um método especial de operador infixo deve devolver para informar o interpretador que não consegue tratar um dado operando.

Por sua vez, NotImplementedError é uma exceção que um método abstrato pode levantar para avisar que subclasses devem sobrescrever este método. Esta exceção é antiga no Python; atualmente a melhor forma de marcar um método abstrato é usar o decorador @abc.abstractmethod.

A implementação viável mais simples de __radd__ aparece no Os métodos __add__ e __radd__ de Vector.

Example 7. Os métodos __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
  1. Nenhuma mudança no __add__ do Método Vector.__add__, versão #1; ele é listado aqui porque é usado por __radd__.

  2. __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.

Example 8. O método 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 iteration

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

Example 9. O método 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]).

Example 10. vector_v6.py: métodos do operador + 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 TypeError, geralmente é melhor capturar esta exceção e devolver NotImplemented. Isto permite que o interpretador tente chamar o método reverso do segundo operando.

Agora que já sobrecarregamos o operador + com segurança, implementando __add__ e __radd__, vamos enfrentar outro operador infixo: *.

Sobrecarregando * para multiplicação por escalar

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 Vector na Usando @ como operador infixo.

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

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

Example 11. vector_v7.py: métodos do operador * adicionados
class 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)
  1. Se scalar não pode ser convertido para float…​

  2. …​não temos como lidar com ele, então devolvemos NotImplemented, para permitir ao Python tentar __rmul__ no operando scalar.

  3. Neste exemplo, __rmul__ funciona bem apenas executando self * 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 * adicionados: checava o argumento scalar de __mul__ com isinstance(scalar, numbers.Real). Agora evito usar as ABCs de numbers, por não serem suportadas pelas anotações de tipo introduzidas na PEP 484. Usar durante a execução tipos que não podem ser também checados de forma estática me parece uma má ideia.

Outra alternativa seria checar com o protocolo typing.SupportsFloat, que vimos na [runtime_checkable_proto_sec]. Escolhi usar tipagem pato naquele exemplo por considerar que pythonistas fluentes devem se sentir confortáveis com esse padrão de programação.

Mas __matmul__, no vector_v7.py: métodos para o operador @, que é novo e foi escrito para essa segunda edição, é um bom exemplo de tipagem ganso.

Usando @ como operador infixo

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])
38

O vector_v7.py: métodos para o operador @ mostra o código dos métodos especiais relevantes na classe Vector.

Example 12. vector_v7.py: métodos para o operador @
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
  1. Ambos os operandos precisam implementar __len__ e __iter__…​

  2. …​e ter o mesmo tamanho, para permitir…​

  3. …​uma linda aplicação de sum, zip e uma expressão geradora.

Tip
O novo recurso de zip() no Python 3.10

Desde o Python 3.10, a função zip aceita um argumento opcional apenas nomeado, strict. Quando strict=True, a função gera um ValueError se um iterável termina antes de outro. O default é False. Este comportamento se alinha à filosofia de «falhar rápido» de Python. No vector_v7.py: métodos para o operador @, poderíamos trocar o if interno por um try/except ValueError e acrescentar strict=True à invocação de zip. Neste caso específico, como self e other suportam __len__, prefiro o teste explícito com if, por clareza. O strict é mais útil quando o zip vai lidar com iteradores, que não têm __len__.

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

Resumindo os operadores aritméticos

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

Table 1. 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])
op direto reverso interno descrição

+

__add__

__radd__

__iadd__

Adição ou concatenação

-

__sub__

__rsub__

__isub__

Subtração

*

__mul__

__rmul__

__imul__

Multiplicação ou repetição

/

__truediv__

__rtruediv__

__itruediv__

Divisão exata

//

__floordiv__

__rfloordiv__

__ifloordiv__

Divisão inteira

%

__mod__

__rmod__

__imod__

Módulo (resto)

divmod()

__divmod__

__rdivmod__

__idivmod__

Devolve uma tupla com o quociente da divisão inteira e o módulo

**, pow()

__pow__

__rpow__

__ipow__

Exponenciação[5]

@

__matmul__

__rmatmul__

__imatmul__

Multiplicação de matrizes

&

__and__

__rand__

__iand__

E binário (bit a bit)

|

__or__

__ror__

__ior__

OU binário (bit a bit)

^

__xor__

__rxor__

__ixor__

XOR binário (bit a bit)

<<

__lshift__

__rlshift__

__ilshift__

Deslocamento de bits para a esquerda

>>

__rshift__

__rrshift__

__irshift__

Deslocamento de bits para a direita

Operadores de comparação rica usam regras diferentes.

Operadores de comparação rica

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

Table 2. Comparação rica: a última coluna mostra o resultado quando as tentativas devolvem NotImplemented ou o operando não implementa o método.
grupo op invocação direta invocação reversa quando não implementado

igualdade

a == b

a.__eq__(b)

b.__eq__(a)

Devolve id(a) == id(b)

a != b

a.__ne__(b)

b.__ne__(a)

Devolve not (a == b)

ordenação

a > b

a.__gt__(b)

b.__lt__(a)

Levanta TypeError

a < b

a.__lt__(b)

b.__gt__(a)

Levanta TypeError

a >= b

a.__ge__(b)

b.__le__(a)

Levanta TypeError

a {lte} b

a.__le__(b)

b.__ge__(a)

Levanta TypeError

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)))
Example 13. Comparando um 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
  1. Duas instâncias de Vector com componentes numéricos iguais são iguais.

  2. Um Vector e um Vector2d também são iguais se seus componentes são iguais.

  3. Um Vector també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.

Example 14. vector_v8.py: __eq__ aperfeiçoado na classe Vector
link:../code/16-op-overloading/vector_v8.py[role=include]
  1. Se o operando other é uma instância de Vector (ou de uma subclasse de Vector), executa a comparação como antes.

  2. Caso contrário, devolve NotImplemented.

Example 15. 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
  1. Mesmo resultado de antes, como esperado.

  2. Mesmo resultado de antes, mas por quê? Explicação a seguir.

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

  1. Para avaliar vc == v2d, Python invoca Vector.eq(vc, v2d).

  2. Vector.__eq__(vc, v2d) verifica que v2d não é um Vector e devolve NotImplemented.

  3. Diante do resultado NotImplemented, Python tenta Vector2d.__eq__(v2d, vc).

  4. Vector2d.__eq__(v2d, vc) transforma os dois operandos em tuplas e os compara: o resultado é True (o código de Vector2d.__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:

  1. Para avaliar va == t3, Python invoca Vector.__eq__(va, t3).

  2. Vector.__eq__(va, t3) verifica que t3 não é um Vector e devolve NotImplemented.

  3. Diante do resultado NotImplemented, Python tenta tuple.__eq__(t3, va).

  4. tuple.__eq__(t3, va) não tem a menor ideia do que seja um Vector, então devolve NotImplemented.

  5. No caso especial de ==, se a chamada reversa devolve NotImplemented, 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)
True

O __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_result

Vimos o básico da sobrecarga de operadores infixos. Agora veremos uma categoria diferente: os operadores de atribuição aumentada.

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.

Example 16. Usando {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
  1. Cria um alias, para podermos inspecionar o objeto Vector([1, 2, 3]) mais tarde.

  2. Verifica o id do Vector inicial, vinculado a v1.

  3. Executa a adição aumentada.

  4. O resultado esperado…​

  5. …​mas foi criado um novo Vector.

  6. Inspeciona v1_alias para confirmar que o Vector original não foi alterado.

  7. Executa a multiplicação aumentada.

  8. Novamente, o resultado é o esperado, mas um novo Vector foi 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 Vector. Pode ser óbvio, mas vale a pena enfatizar. Por este motivo, deixaremos de lado o tema dos vetores nos próximos exemplos.

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

Example 17. O operador + cria uma nova instância de AddableBingoCage
link:../code/16-op-overloading/bingoaddable.py[role=include]
  1. Cria uma instância de globe com cinco itens (cada uma das vowels).

  2. Extrai um dos itens, e verifica que é uma das vowels.

  3. Confirma que globe tem agora quatro itens.

  4. Cria uma segunda instância, com três itens.

  5. Cria uma terceira instância pela soma das duas anteriores. Esta instância tem sete itens.

  6. Tentar adicionar uma AddableBingoCage a uma list falha com um TypeError. A mensagem de erro é produzida pelo interpretador de Python quando nosso método __add__ devolve NotImplemented.

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

Example 18. Uma 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]
  1. Cria um alias para podermos checar a identidade do objeto mais tarde.

  2. globe tem quatro itens aqui.

  3. Uma instância de AddableBingoCage pode receber itens de outra instância da mesma classe.

  4. O operador à direita de {iadd} também pode ser qualquer iterável.

  5. Durante todo esse exemplo, globe sempre se refere ao mesmo objeto que globe_orig.

  6. Tentar adicionar um não-iterável a uma AddableBingoCage falha 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 + e {iadd} observando como funciona o tipo embutido list. Ao escrever my_list + x, você só pode concatenar uma list a outra list, mas se você escrever my_list += x, você pode estender a lista da esquerda com itens de qualquer iterável x à direita do operador. É assim que o método list.extend() funciona: ele aceita qualquer argumento iterável.

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

Example 19. bingoaddable.py: AddableBingoCage é subclasse de BingoCage com suporte aos operadores + e {iadd}
link:../code/16-op-overloading/bingoaddable.py[role=include]
  1. AddableBingoCage estende BingoCage.

  2. Nosso __add__ só vai funcionar se o segundo operando for uma instância de Tombola.

  3. Em __iadd__, obtém os itens de other, se for uma instância de Tombola.

  4. Caso contrário, tenta obter um iterador sobre other (estudaremos a função embutida iter no [ch_generators]).

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

  6. Se chegamos até aqui, podemos carregar o other_iterable em self.

  7. 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 __mul__) é projetado para funcionar apenas com operandos do mesmo tipo de self, é inútil implementar o método reverso correspondente (por exemplo, __rmul__) pois, por definição, esse método só será invocado quando estivermos lidando com um operando de um tipo diferente.

Assim terminamos nossa exploração de sobrecarga de operadores no Python.

Resumo do capítulo

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.

Para saber mais

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.

Ponto de Vista

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 dizer a + 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.

Example 20. Mesmo que o [ex_vector_error_iter_not_add]
>>> 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'
  1. A chamada a Vector recebe uma expressão geradora como seu argumento components. Nenhum problema nesse estágio.

  2. A genexp components é passada para o construtor de array. Dentro do construtor de array, Python tenta iterar sobre a genexp, causando a avaliação do primeiro item a + b. É quando ocorre o TypeError.

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


1. Fonte: 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).
2. As demais ABCs na biblioteca padrão de Python funcionam bem para tipagem ganso e tipagem estática. O problema com as ABCs numbers é explicado na [numbers_abc_proto_sec].
3. Em matemática, um "escalar" é um número que pode ser representado por um ponto em uma linha, ou "escala". Em Python, instâncias de int, float, decimal.Decimal e fraction.Fraction são escalares, mas um complex não é um escalar.
4. Veja o Ponto de Vista para uma discussão deste problema.
5. 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)).
6. A lógica de object.__eq__ e object.__ne__ está na função object_richcompare em Objects/typeobject.c, no código-fonte do CPython.
7. NT: O prefixo "i" nos nomes destes métodos se refere a in-place, traduzido como "interno" na documentação brasileira oficial de Python.