Telegram Queries - Interesting Questions
Last week I had the opportunity to see three interesting questions discussed in the Telegram groups, but the explanation would be too long for a chat.
Why True, True, True == (True, True, True)
returns True, True, False
?#
This question was presented as a weird syntax of Python, but it’s actually a visual trap. Look at the operator ==
(equal equal).
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)
Some people were in doubt and asked why (True, True, False)
instead of (True, True, True)
.
Firstly, we should remember that Python allows us to create tuples without using parentheses, only with commas. And it’s exactly this syntax that caused some confusion, because looking quickly you might think we’re comparing two tuples, which is not the case. For example:
>>> (True, True, True) == (True, True, True)
True
The difference are the parentheses. Without parentheses, we’re only comparing True == (True, True, True)
and not the first tuple with the second. It’s a matter of operator priority. So, to create the tuple, the interpreter evaluates the comparison of True
with (True, True, True)
first. Since the first is of type bool
and the second is a tuple, the result is False
. Thus, the generated tuple has the first two True
s followed by the False
that’s the result of the comparison.
When we write between parentheses, we’re comparing two tuples and the result is True
.
Why -2 * 5 // 3 + 1
returns -3
?#
This one is more complicated. Several results were presented and the priority of the operator //
was questioned. Let’s see what Python says:
>>> -2 * 5 // 3 + 1
-3
What result did you expect? Some people expected -4
, others -2
. How does it result in -3
?
Firstly, we should review the priority of the operator //
which is exactly the same as division. In this case, the division and multiplication have the same priority and must be evaluated from left to right. This is especially important because //
performs a rounding down.
Now let’s see the definition of //
in the Python documentation:
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…
Which we can translate as: Division of integers results in a floating-point number (float), while floor division of integers results in an integer (int); the result is that of mathematical division with the application of the floor function applied to the result.
The point that was not clear is the behavior of floor
with negative numbers.
>>> 10 // 3
3
>>> -10 // 3
-4
>>> -10 / 3
-3.3333333333333335
Naturally, we would expect the result of -10 // 3
to be equal to that of 10 // 3
, but with a negative sign. You can check the definition of these two functions in Wikipedia and the Python documentation:
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
Which can be translated as: returns the floor of x, the largest integer less than or equal to x. If x is not a floating-point number, delegates to x.__floor__()
which should return an integral value.
For negative numbers, the part of returning the smallest integer misleads easily. Because the smallest integer relative to -3.33
is -4
, keeping in mind that `-3 > -4!
So the expression is correctly evaluated by the interpreter:
-2 * 5
-10
-10 // 3
-4
-4 + 1
-3
Looking at the sources of Python (3.8.2) on GitHub, it’s easy to see the adjustment (line 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}
Note: I recall calling the floor function of sole or minimum.
How to add times in Python?#
A colleague from the group asked how to add time durations in Python. Presenting the following code:
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
which results in the following error:
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'
This happens because the time
class does not define the addition operation. The correct class for this type of calculation is timedelta
.
To correctly calculate this sum, we need to first convert the string to a timedelta
object. This can be done with a simple function:
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)
which results in:
7:07:27
2020-05-03 22:48:55.473647
A tip to remember the difference between time
and timedelta
is that time
cannot represent more than 24 hours and timedelta
can represent more than a century (270 years). Another advantage of timedelta
is that it can be used in operations with date
and datetime
.