Semana passada tive a oportunidade de ver 3 questões interessantes a discutur nos grupos do Telegram, mas que a explicação seria grande demais para apresentar em um chat.

  1. Por que True, True, True == (True, True, True) retorna True, True, False?

  2. Por que -2 * 5 // 3 + 1 retorna -3?

  3. Como adicionar tempos em Python?

Por que True, True, True == (True, True, True) retorna True, True, False?

Esta questão foi apresentada como sendo uma sintaxe bizarra do Python, mas na realidade é uma pegadinha visual. Repare no operador == (igual igual).

Python 3.8.2 (default, Apr 27 2020, 15:53:34) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> True, True, True == (True, True, True)
(True, True, False)

Algumas pessoas ficaram em dúvida e perguntaram por que (True, True, False) em vez de (True, True, True). Primeiramente, devemos lembrar que Python permite criarmos tuplas sem utilizar parênteses, apenas com vírgulas. E é exatamente esta sintaxe que causou certa confusão, pois olhando rápido você pode pensar que estamos comparando duas tuplas, o que não é o caso. Por exemplo:

>>> (True, True, True) == (True, True, True)
True

A diferença são os parênteses. Sem os parênteses, estamos comparando apenas True == (True, True, True) e não a primeira tupla com a segunda. É uma questão de prioridade de operadores. Desta forma, para criar a tupla, o interpretador avalia primeiramente a comparacão de True com (True, True, True). Como o primeiro é do tipo bool e o segundo uma tupla, o resultado é False. Assim, a tupla gerada tem os primeiros True, True, seguidos do False que é o resultado da comparação. Quando escrevemos entre parênteses, estamos comparando duas tuplas e o resultado é True.

Por que -2 * 5 // 3 + 1 retorna -3?

Esta aqui é mais complicada. Vários resultados foram apresentados e a prioridade do operador // foi questionada. Vejamos o que diz o Python:

>>> -2 * 5 // 3 + 1
-3

Qual resultado você esperava? Algumas pessoas esperavam -4, outras -2. Como resulta em -3?

Primeiro, devemos rever a prioridade do operador // que é exatamente a mesma da divisão. No caso, a divisão e a multiplicação tem a mesma prioridade e devem ser avalidas da esquerda para a direita. Isto é especialmente importante, pois o // faz um arredondamento.

Agora vejamos a definição do // na documentação do Python:

Division of integers yields a float, while floor division of integers results in an integer; the result is that of mathematical division with the ‘floor’ function applied to the result…

Que podemos traduzir como: A divisão de inteiros resulta em um número de ponto flutuante (float), enquanto a divisão piso(floor) de inteiros resulta em um número inteiro (int); o resultado é o da divisão matemática com a aplicação da função piso(floor) aplicada ao resultado.

O ponto que não ficou claro é o comportamento de floor com números negativos.

>>> 10 // 3
3
>>> -10 // 3
-4
>>> -10 / 3
-3.3333333333333335

Naturalmente, se espera que o resultado de -10 // 3 fosse igual ao de 10 // 3, porém com sinal diferente. Você pode consultar a definição destas duas funções na Wikipedia e na documentação do Python:

math.floor(x) Return the floor of x, the largest integer less than or equal to x. If x is not a float, delegates to x.__floor__(), which should return an Integral value

Que pode ser traduzido como: retorna o valor do piso de x, o maior número inteiro menor or igual a x. Se x não é um número de ponto flutuante, delega a x.__floor()__ que deve retornar um valor inteiro.

Para números negativos, a parte de retornar o menor inteiro engana facilmente. Pois o menor inteiro relativo a -3.33 é -4, lembrando que -3 > -4!

Assim a expressão é corretamente avaliada pelo interpretador:

-2 * 5
-10
-10 // 3
-4
-4 + 1
-3

Olhando os fontes do Python (3.8.2) no github, fica fácil de ver o ajuste (linha 3768):

3751/* Fast floor division for single-digit longs. */
3752static PyObject *
3753fast_floor_div(PyLongObject *a, PyLongObject *b)
3754{
3755    sdigit left = a->ob_digit[0];
3756    sdigit right = b->ob_digit[0];
3757    sdigit div;
3758
3759    assert(Py_ABS(Py_SIZE(a)) == 1);
3760    assert(Py_ABS(Py_SIZE(b)) == 1);
3761
3762    if (Py_SIZE(a) == Py_SIZE(b)) {
3763        /* 'a' and 'b' have the same sign. */
3764        div = left / right;
3765    }
3766    else {
3767        /* Either 'a' or 'b' is negative. */      
3768        div = -1 - (left - 1) / right;              
3769    }
3770
3771    return PyLong_FromLong(div);
3772}

Nota: recordo de chamar a função piso de solo ou de mínima.

Como adicionar tempos em Python?

Um colega do grupo perguntou como adicinar durações de tempo em Python. Apresentando o seguinte código:

from datetime import time
t0 = time.fromisoformat('06:52:00')
t1 = time.fromisoformat('00:08:15') 
t2 = time.fromisoformat('00:07:12') 
t3 = t0 + t1 + t2

que resulta no erro seguinte:

Traceback (most recent call last):
  File "tdeltao.py", line 5, in <module>
    t3 = t0 + t1 + t2
TypeError: unsupported operand type(s) for +: 'datetime.time' and 'datetime.time'

Isto contece porque a classe time não define a operação de soma. A classe correta para este tipo de cálculo é timedelta. Para calcular corretamente esta soma, precisamos primeiro converter a string em um objeto timedelta. Isto pode ser feito com uma função simples:

from datetime import time, timedelta, datetime

def string_para_timedelta(str_time: str) -> timedelta:
    valor = time.fromisoformat(str_time)
    return timedelta(hours=valor.hour, 
                     minutes=valor.minute, 
                     seconds=valor.second)

t0 = string_para_timedelta('06:52:00')
t1 = string_para_timedelta('00:08:15') 
t2 = string_para_timedelta('00:07:12') 
t3 = t0 + t1 + t2
print(t3)
print(datetime.now() + t3)

que resulta em:

7:07:27
2020-05-03 22:48:55.473647

Uma dica para lembrar da diferença entre time e timedelta é que time não pode representar mais de 24h e timedelta pode representar mais de um século (270 anos). Outra vantagem de timedelta é poder ser utilizada em operações com date e datetime.