Consultas do Telegram - Questões interessantes
Semana passada tive a oportunidade de ver 3 questões interessantes a discutir nos grupos do Telegram, mas que a explicação seria grande demais para apresentar em um chat.
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 comparaçã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 adicionar 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 acontece 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
.