Python's horror show
Here you will find a collection of strange and odd python snippets showing apparent odd behavior. The purpose of these scripts is to mess with your head but some people have reported strange new Python knowledge as a secondary effect.
Hidden memory things
>>> a = 5
>>> b = 5
>>> a is b
True
>>> a = -4
>>> b = -4
>>> a is b
True
>>> a = 300
>>> b = 300
>>> a is b
False
>>> a = 300; b = 300
>>> a is b
True
From "Integer Objects":
The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you actually just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behaviour of Python in this case is undefined. :-)
We can check this thing using the id
operator:
>>> a = 5
>>> b = 5
>>> id(a)
4531116864
>>> id(b)
4531116864
>>> a = 300
>>> b = 300
>>> id(a)
4537522896
>>> id(b)
4537523216
>>> a = 300; b = 300
>>> id(a)
4537523696
>>> id(b)
4537523696
The brief explanation is that as in Python everything is an object, each time you use a number (integer, float...) it must be created so this could be very inefficient. So what Python does is pre-allocate integers from -5
to 256
because these are often used. The last trick (a = 300; b = 300
) is interpreter-dependent, but in the basic Python interpreter (among others) as the two assignations occur in the same line both variables will refer to the same object to avoid wasting space.
Indexes for noobs
>>> a = [1,2]
>>> a.index(1)
0
>>> a.index(2)
1
>>> a[a.index(1)]
1
>>> a[a.index(2)]
2
>>> a[a.index(1)],a[a.index(2)] = 2,1
>>> a
[1, 2]
Woaaaa! WTF is happening here? Easy: we are forgetting that everything must be evaluated sequentially. Let's check this again but one statement after another:
>>> a = [1,2]
>>> a[a.index(1)] = 2
>>> a
[2, 2]
>>> a.index(2)
0
>>> a[a.index(2)] = 1
>>> a
[1, 2]
Aha! So the thing is that when we assign a[a.index(1)] = 2
as a.index(2)
will give us the first index in which 2
appears it gives 1
and therefore a[a.index(2)] = 1
will reset a
to its initial value.
Too many equals
This is one of my favourites:
>>> a, b = a[b] = {},5
>>> a
{5: ({...}, 5)}
>>> b
5
Hummmmmm.... I think the problem with this is that as soon as we see the double equal we think in the formal propositional logic implications of the statement. But as we have seen already, things must be evaluated sequentially. In this case the rules are two:
- Left before right
a = b = c
is sugar fora=c & b =c
So the only thing we have to do in order to undo this mess is execute the code following this rules as
>>> a, b = {},5
>>> a[b] = a,5 # As a is an empty dic and b=5 this is equivalent to {}[5] = ({},5)
>>> a
{5: ({...}, 5)}
>>> b
5
Hashable objects
>>> d = {1: 'a', True: 'b', 1.0: 'c'}
>>> d
{1: 'c'}
And... whatโs happening here? The answer has already been given: Things must be evaluated sequentially.
>>> d = dict()
>>> d[1] = โaโ
>>> d[True] = โbโ
>>> d[1.0] = โcโ
>>> d
{1: 'c'}
Ok, but ยฟwhy True is evaluated as 1?
>>> 1 is True
False
>>> 1 == True
True
The reason: any two strings, numbers, etc. that equate will have the same hash, allowing the dict
(implemented as a hashmap) to find those strings very efficiently. dict
keys are equaled on value rather on identity.
In fact, thatโs the same reason why, as a general rule, we canโt use mutable objects as key in a dictionary: the hash of a list or another mutable object is not based on it's value, but rather the instance of the list, and changes when its content changes.
The same approach is true when creating sets:
>>> s = {1, True, 1.0}
>>> s
{1}
or using another values as 0
and False
:
>>> d2 = {0: 'a', False: 'b'}
>>> d2
{0: 'b'}
Enter the void
>>> all([])
True
>>> all([[]])
False
>>> all([[[]]])
True
When converted too a bool type, []
decay into False
because it's empty, and [[]]
becomes True
since it's not empty. Therefore all([[]])
is equivalent to all([False])
, and all([[[]]])
is the same as all([True])
. As in all([])
there is no False
then is trivially True
.
iter
method
Consumed by the >>> a = 2, 1, 3
>>> sorted(a) == sorted(a)
True
>>> reversed(a) == reversed(a)
False
Unlike sorted
which returns a list, reversed
returns an iterator. Iterators compare equal to themselves, but not to other iterators that contain the same values.
>>> b = reversed(a)
>>> sorted(b) == sorted(b)
False
The iterator b
is consumed by the first sorted
call. Calling sorted(b)
once b
is consumed simply returns []
.
False
is the new True
>>> False == False in [False]
True
Neither the ==
nor the in
happens first. They're both comparison operators, with the same precedence, so they're chained. The line is equivalent to False == False and False in [False]
, which is True
.
Return to childhood
>>> x = (1 << 53) + 1
>>> x + 1.0 < x
True
The value of x
can be exactly represented by a Python int
, but not by a Python float
, which has 52 bits of precision. When x
is converted from int
to a float
, it needs to be rounded to a nearby value. According to the rounding rules, that nearby value is x - 1
, which can be represented by a float
.
When x + 1.0
is evaluated, x
is first converted to a float
in order to perform the addition. This makes its value x - 1
. Then 1.0
is added. This brings the value back up to x, but since the result is a float, it is again rounded down to x - 1
.
Next the comparison happens. This is where Python differs from many other languages. In C, for instance, if a double
is compared to an int
, the int
is first converted to a double
. In this case, that would mean the right-hand side would also be rounded to x - 1
, the two sides would be equal, and the <
comparison would be false. Python, however, has special logic to handle comparison between float
s and int
s, and it's able to correctly determine that a float
with a value of x - 1
is less than an int
with a value of x.