Here are the explanations for the Python Quiz Advent Calender puzzles which appear over December 2020.
What is the output of the following:
a = 1 + 1, 2 + 1
assert a == 2, 3
AssertionError: 3
In the first line, the variable name a
is attached to the tuple
object (2, 3)
as probably intended. The assert
statement, however takes the form:
assert <expression>[, <message>]
where the message to output upon an assertion failure is optional and separated from the expression to be tested by a comma. This means that to assert equality to a tuple
requires the tuple to be defined with parentheses. As it stands, the assert
tests to see if a
is equal to 2
(which it isn't) and outputs 3
if the AssertionError
is raised (which it is). What was probably intended was:
assert a == (2, 3)
flist = [lambda a: a**i for i in range(5)]
print(flist[2](3))
outputs 81
because of Python's late binding of closures: the variable i
is resolved at the time the function is called, by which time it has the value 4
. A closure is a function that makes reference to an object, such as i
here, in an enclosing scope: Python's rules about how the value of such an object is determined say that rather than being copied across when the function is defined, a reference to the i
object is kept and used when the function is called: there is only one i
object and its final value (4
) is the one that is used in the calculation $3^4 = 8$.
To force Python to use the intended value of i
, a common approach is to pass it as a default argument to the lambda
, forcing it to be bound to the function when it is defined:
flist = [lambda a, i=i: a**i for i in range(5)]
print(flist[2](3))
returns $3^2$ as 9
.
The expession
1 and 2 or False
Comparisons can be chained in Python, so this is equivalent to:
1 and 2 and 2 or False
Furthermore, the boolean operations return the first object encountered in the boolean test satisfying its conditions, not (necessarily) just True
or False
. Thus, 1 and 2
evaluates to 2
(which is equivalent to True
), and 2 or False
also evaluates to 2
. ~Therefore, 1 and 2 or False
is equivalent to 2 and 2
and the returned value is 2
.
Python's unary minus operator is allowed to be separated from its operand by whitespace; the statement a -=- 1
is equivalent to a -= -1
which is the same as a += 1
.
Dictionary keys are considered the same if their hashes are the same and if their values are equal. The integer, float and complex values 1
, 1.0
, and (1+0j)
are all the same according to this test; the string object '1.0'
is distinct, as is the float resulting from the calculation (1-1.e-16)
. However, the limited precision of floating point arithmetic makes (1-1.e-17)
indistinguishable from the integer 1
and it is used as such to become the key for the value 'f'
. The assignment:
d = {1: 'a', 1.0: 'b', 1.+0j: 'c', '1.0': 'd', (1-1.e-16): 'e', (1-1.e-17): 'f'}
therefore results in the dictionary:
{1: 'f', '1.0': 'd', 0.9999999999999999: 'e'}
The difference in behaviour between 'augmented assignment', +=
, using the special method __iadd__
and simple list extension with +
is that the former changes the list object in place, and whereas +
creates a new list
object. Therefore,
a = [1, 2]
b = c = a
a += [3, 4]
extends the single list
referred to by a
, b
, and c
, and then
b = b + [5, 6]
creates a new list
with the contents [1, 2, 3, 4, 5, 6]
and binds it to the variable name b
(which loses its reference to the previous list
). We therefore end up with:
print(a, b, c, sep='\n')
[1, 2, 3, 4]
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4]
To parse the statement
x, y = x[y] = {}, 42
recall that chained assignments are evaluated sequentially. The statement is therefore equivalent to:
temp = {}, 42
x, y = temp
x[y] = temp
The dictionary x
is assigned with the key 42
to a value which is a tuple, (x, y)
of itself and the integer 42
. The circular reference in x
is indicated by {...}
:
print(x, y)
{42: ({...}, 42)}, 42
The expression
{1 for i in range(5)}
generates a set
from the a sequence of five 1
s. Sets only contain unique values, so the object created is {1}
.
This is another chained comparison:
3 > 2 == True
is equivalent to
3 > 2 and 2 == True
which evaluates to False
.
a, b, c = 0.7, 0.2, 0.1
print(a + b + c == a + (b + c))
outputs False
because the finite precision of floating point arithmetic means that addition of floating point numbers is not associative. In particular,
In [1]: a, b, c = 0.7, 0.2, 0.1
In [2]: a + b
Out[2]: 0.8999999999999999
In [3]: (a + b) + c
Out[3]: 0.9999999999999999
but
In [4]: b + c
Out [4]: 0.30000000000000004
In [5]: a + (b + c)
Out [5]: 1.0
The bitwise representations of these (double precision floating point numbers) illustrate the problem:
a + b: 0011111111101100110011001100110011001100110011001100110011001100
c: 0011111110111001100110011001100110011001100110011001100110011010
(a + b) + c: 0011111111101111111111111111111111111111111111111111111111111111
whereas:
a: 0011111111100110011001100110011001100110011001100110011001100110
b + c: 0011111111010011001100110011001100110011001100110011001100110100
a + (b + c): 0011111111110000000000000000000000000000000000000000000000000000
The actual calculation involved in floating point addition is well-described here.
In the code
for i in range(3):
print(i)
i = 3
the variable i
receives a new value from the range
object at each iteration, so the assignment to 3
is discarded at the start of each pass through the loop. The output is therefore
0
1
2
The statement
x: str = 4
defines a type hint on variable name x
. impying that x
is a str
. This is valid syntax, but Python does not check that the type is as hinted: x
is still assigned to the integer object 4
.
In the floating point arithmetic used by Python, zero is a signed quantity. The modifier j
creates a complex number consisting of floating point real and imaginary components, and the unary minus operator returns the negative of each of those components. That is, first 1j
is evaluated to generate (0+1j)
, and the negative of this is (-0-1j)
.
In Python, the integer division a // b
returns the floor of the division a/b
: the largest integer not greater than a/b
. As Guido van Rossum says in his blog this maintains the mathematically nice property that a/b = q
with remainder r
satisfies b*q + r = a and 0 <= r < b
.
-22/7
is about -3.14
, and the floor of this is -4
.
The loop syntax can assign its iterated values into a mutable sequence, such as a list
. In the program:
arr = [1, 2, 3]
for arr[0] in range(3):
print(arr)
the first element of arr
is changed (before it is printed) at each iteration to 0
, 1
, and then 2
, giving:
[0, 2, 3]
[1, 2, 3]
[2, 2, 3]
The Python split
function uses two different algorithms, depending on whether it is passed a seperator argument, sep
, or not.
If sep
is not specified (or is None
), runs of consecutive whitespace are treated as a single separator: this is so that the result list will contain no empty strings at the start or end if the string has leading or trailing whitespace. This is usually the behaviour desired: ' a b c '.split()
should return ['a', 'b', 'c']
, not ['', 'a', 'b', 'c', '']
. In the first example, therefore, ''.split())
is the empty list, []
, with length0
.
If sep
is given, consecutive delimiters are not grouped together and are considered to delimit empty strings. This is so that, for example, splitting a comma-separated string with missing values behaves as expected: 'a,,c'.split(',')
returns ['a', '', 'c']
, not ['a', 'c']
. In the case of splitting an empty string with a whitespace delimiter specified, the algorithm just returns that empty string in a list: ['']
, which has length 1
.
If a string consisting of a single space is split on the space delimiter, the empty strings either side of it are returned: ' '.split(' ')
returns ['', '']
, a list of length 2
.
sorted
returns a sorted list
of the provided iterable object. In this case, the iterable object is a tuple
, however, which is not equal to this list
, even though its elements are in the same order:
In [1]: a = 1, 2, 3
In [2]: a
Out[2]: (1, 2, 3)
In [3]: sorted(a)
Out[3]: [1, 2, 3]
Sorting the list twice is fine, and returns two equal lists
:
In [4]: sorted(a), sorted(a)
Out[4]: ([1, 2, 3], [1, 2, 3])
reversed(a)
returns an iterator which yields the contents of the sequence a
in reverse order. It can be used once so the first test, sorted(a) == sorted(b)
compares two lists of the sorted values in a
, [1, 2, 3]
, and returns True
. The second test now compares this sorted list with an empty list returned by sorting the, now exhausted iterator, b
, and the result is False
.
Generally, it is not a good idea to modify a sequence you are iterating over.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for i in range(len(numbers)):
del numbers[i]
In the above the first element of the list
(1
) is deleted on the first iteration, so that 2
becomes the new first element. On the second time round the loop, we delete numbers[1]
which now points to the integer 3
; next we delete the integer 5
which is now indexed at i=2
, and so on until we have deleted the odd numbers and numbers
has shrunk to [2, 4, 6, 8, 10]
; the next attempt to index into this list with i=5
fails, and an IndexError
is raised.
Note that the range
object is created with the length of the original numbers
list and not modified as this list is altered.
As the official documentation explains, an empty iterable passed to all
will return True
:
In [1]: all([])
Out[1]: True
In the second case, the iterable contains a single element, an empty list
. The empty list
is equivalent to False
, and so:
In [2]: all([[]])
Out[2]: False
In the third case, the single element of the iterable passed to all
is not empty: it contains a single element, namely another empty list
. A non-empty list
is equivalent to True
(no matter what the list contains), and so:
In [3]: all([[[]]])
Out[3]: True
Unlike in C++, for example, there is no "inline" incrementor operator like ++
(augmented assignment is a single statement, e.g. a += 1
). The expression a++4
is equivalent to a+(+4)
and returns 7
, since the value of a
is 3
. The "unary plus"(?) operator does nothing and is syntactically allowed for symmetry with unary minus: any number of +
s are therefore allowed: a+++4
is the same as:
a + (+(+4))
which is also 7
, of course.
Python tuple
s are immutable collections of references to other objects. Those objects can be mutable themselves (though the tuple won't be hashable if they are), as long as their identity doesn't change (i.e. the object reference remains the same). For example the following is fine:
In [1]: a = ([1],)
In [2]: a[0].extend([2, 3, 4])
In [3]: print(a)
([1, 2, 3, 4],)
Attempting the extend the list using the +
operator (which calls the list
's __add__
method) will fail:
In [4]: a = ([1],)
In [5]: a[0] = a[0] + [2, 3, 4]
...
TypeError: 'tuple' object does not support item assignment
In [6]: print(a)
([1],)
The expression a[0] + [2, 3, 4]
returns a new list
object, as can be seen from:
In [7]: b = [1]
In [8]: id(b)
Out[8]: 140200480518384
In [9]: b = b + [2, 3, 4]
In [10]: id(b)
Out[10]: 140200431232976
This new object cannot replace the existing element a[0]
and an exception is raised when the assignment to the tuple
(a[0] = ...
) is attempted.
The augmented assignment, a[0] += [2, 3, 4]
changes the list
in-place, through its __iadd__
method: a
is altered before the attempted assignment to the tuple
. Therefore, once the erroneous change to an immutable object is detected and an exception raised, a
already has its new contents.
More information from the official documentation and this blog article by Ned Batchelor.
The code
class A:
food = 'eggs'
defines a class, A
, with a class variable name, food
, set to the string object eggs
.
The two further code blocks
class B(A):
pass
class C(A):
pass
define classes, B
and C
, which inherit from A
. At this point, food
points to a single string object. Changing the class variable for the B
class creates a reference to a new string object and does not change the value of A.food
(which is the same as C.food
):
B.food = 'spam'
print(A.food, B.food, C.food)
eggs spam eggs
Now changing the object that A.food
points to will update this class variable value the classes A
and C
(which inherits from it), but not for B
(which already has food
bound to a separate object): this is usually the desired behaviour:
A.food = 'toast'
print(A.food, B.food, C.food)
toast spam toast
Internally, class variables are stored in a dictionary-like object and not copied across to inherited classes (instead, they are looked up in the inheritance chain). The vars
built-in makes this clear:
In [1]: class A:
...: food = 'eggs'
...:
...: class B(A):
...: pass
...:
...: class C(A):
...: pass
...:
In [2]: vars(A) # contains the reference to food.
Out[2]:
mappingproxy({'__module__': '__main__',
'food': 'eggs',
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>,
'__doc__': None})
In [3]: vars(B) # no reference to food: resolved in parent class, A
Out[3]: mappingproxy({'__module__': '__main__', '__doc__': None})
In [4]: vars(C) # no reference to food: resolved in parent class, A
Out[4]: mappingproxy({'__module__': '__main__', '__doc__': None})
In [5]: B.food = 'spam'
In [6]: vars(B) # new reference to food added to B
Out[6]: mappingproxy({'__module__': '__main__', '__doc__': None, 'food': 'spam'})
In [7]: A.food = 'toast'
In [8]: vars(A) # reference to food in A changed
Out[8]:
mappingproxy({'__module__': '__main__',
'food': 'toast',
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>,
'__doc__': None})
In [9]: vars(C) # still no reference to food: looked-up in parent class A
Out[9]: mappingproxy({'__module__': '__main__', '__doc__': None})
def func(a=[]):
a.append(10)
return a
print(func(), func())
outputs:
[10, 10] [10, 10]
The default argument to the function func
is the same list
object every time the function is called. The arguments to the print
function are evaluated before being printed, so calling func()
twice leaves a
with the contents [10, 10]
. The above code is equivalent to:
lst = func() # lst is [10]
lst = func() # lst is [10, 10]
print(lst, lst)
Comments
Comments are pre-moderated. Please be patient and your comment will appear soon.
There are currently no comments
New Comment