The C3 class resolution algorithm for multiple class inheritance
If we are dealing with multiple inheritance, according to the newer C3 class resolution algorithm, the following applies:
Assuming that child class C inherits from two parent classes A and B, “class A should be checked before class B”.
Assuming that child class C inherits from two parent classes A and B, “class A should be checked before class B”.
If you want to learn more, please read the original blog post by Guido van Rossum.
(Original source: http://gistroll.com/rolls/21/horizontal_assessments/new)
class A(object):
def foo(self):
print("class A")
class B(object):
def foo(self):
print("class B")
class C(A, B):
pass
C().foo()
class A
So what actually happened above was that class
C
looked in the scope of the parent class A
for the method .foo()
first (and found it)!
I received an email containing a suggestion which uses a more nested example to illustrate Guido van Rossum’s point a little bit better:
class A(object):
def foo(self):
print("class A")
class B(A):
pass
class C(A):
def foo(self):
print("class C")
class D(B,C):
pass
D().foo()
class C
Here, class
D
searches in B
first, which in turn inherits from A
(note that class C
also inherits from A
, but has its own .foo()
method) so that we come up with the search order: D, B, C, A
.Assignment operators and lists - simple-add vs. add-AND operators
Python
list
s are mutable objects as we all know. So, if we are using the +=
operator on list
s, we extend the list
by directly modifying the object directly.
However, if we use the assignment via
my_list = my_list + ...
, we create a new list object, which can be demonstrated by the following code:a_list = []
print('ID:', id(a_list))
a_list += [1]
print('ID (+=):', id(a_list))
a_list = a_list + [2]
print('ID (list = list + ...):', id(a_list))
ID: 4366496544
ID (+=): 4366496544
ID (list = list + ...): 4366495472
Just for reference, the
.append()
and .extends()
methods are modifying the list
object in place, just as expected.a_list = []
print(a_list, '\nID (initial):',id(a_list), '\n')
a_list.append(1)
print(a_list, '\nID (append):',id(a_list), '\n')
a_list.extend([2])
print(a_list, '\nID (extend):',id(a_list))
[]
ID (initial): 140704077653128
[1]
ID (append): 140704077653128
[1, 2]
ID (extend): 140704077653128
Python reuses objects for small integers - use “==” for equality, “is” for identity
This oddity occurs, because Python keeps an array of small integer objects (i.e., integers between -5 and 256, see the doc).
a = 1
b = 1
print('a is b', bool(a is b))
True
c = 999
d = 999
print('c is d', bool(c is d))
a is b True
c is d False
(I received a comment that this is in fact a CPython artefact and must not necessarily be true in all implementations of Python!)
So the take home message is: always use “==” for equality, “is” for identity!
Here is a nice article explaining it by comparing “boxes” (C language) with “name tags” (Python).
This example demonstrates that this applies indeed for integers in the range in -5 to 256:
print('256 is 257-1', 256 is 257-1)
print('257 is 258-1', 257 is 258 - 1)
print('-5 is -6+1', -5 is -6+1)
print('-7 is -6-1', -7 is -6-1)
256 is 257-1 True
257 is 258-1 False
-5 is -6+1 True
-7 is -6-1 False
a = 1
b = 1
print('a is b', bool(a is b))
True
c = 999
d = 999
print('c is d', bool(c is d))
a is b True
c is d False
print('256 is 257-1', 256 is 257-1)
print('257 is 258-1', 257 is 258 - 1)
print('-5 is -6+1', -5 is -6+1)
print('-7 is -6-1', -7 is -6-1)
256 is 257-1 True
257 is 258-1 False
-5 is -6+1 True
-7 is -6-1 False
And to illustrate the test for equality (==
) vs. identity (is
):
a = 'hello world!'
b = 'hello world!'
print('a is b,', a is b)
print('a == b,', a == b)
a is b, False
a == b, True
We would think that identity would always imply equality, but this is not always true, as we can see in the next example:
a = float('nan')
print('a is a,', a is a)
print('a == a,', a == a)
a is a, True
a == a, False
a = 'hello world!'
b = 'hello world!'
print('a is b,', a is b)
print('a == b,', a == b)
a is b, False
a == b, True
a = float('nan')
print('a is a,', a is a)
print('a == a,', a == a)
a is a, True
a == a, False
Shallow vs. deep copies if list contains other structures and objects
Shallow copy:
If we use the assignment operator to assign one list to another list, we just create a new name reference to the original list. If we want to create a new list object, we have to make a copy of the original list. This can be done via a_list[:]
or a_list.copy()
.
list1 = [1,2]
list2 = list1 # reference
list3 = list1[:] # shallow copy
list4 = list1.copy() # shallow copy
print('IDs:\nlist1: {}\nlist2: {}\nlist3: {}\nlist4: {}\n'
.format(id(list1), id(list2), id(list3), id(list4)))
list2[0] = 3
print('list1:', list1)
list3[0] = 4
list4[1] = 4
print('list1:', list1)
IDs:
list1: 4346366472
list2: 4346366472
list3: 4346366408
list4: 4346366536
list1: [3, 2]
list1: [3, 2]
Deep copy
As we have seen above, a shallow copy works fine if we want to create a new list with contents of the original list which we want to modify independently.
However, if we are dealing with compound objects (e.g., lists that contain other lists, read here for more information) it becomes a little trickier.
In the case of compound objects, a shallow copy would create a new compound object, but it would just insert the references to the contained objects into the new compound object. In contrast, a deep copy would go “deeper” and create also new objects
for the objects found in the original compound object. If you follow the code, the concept should become more clear:
from copy import deepcopy
list1 = [[1],[2]]
list2 = list1.copy() # shallow copy
list3 = deepcopy(list1) # deep copy
print('IDs:\nlist1: {}\nlist2: {}\nlist3: {}\n'
.format(id(list1), id(list2), id(list3)))
list2[0][0] = 3
print('list1:', list1)
list3[0][0] = 5
print('list1:', list1)
IDs:
list1: 4377956296
list2: 4377961752
list3: 4377954928
list1: [[3], [2]]
list1: [[3], [2]]
Shallow copy:
If we use the assignment operator to assign one list to another list, we just create a new name reference to the original list. If we want to create a new list object, we have to make a copy of the original list. This can be done via a_list[:]
or a_list.copy()
.
list1 = [1,2]
list2 = list1 # reference
list3 = list1[:] # shallow copy
list4 = list1.copy() # shallow copy
print('IDs:\nlist1: {}\nlist2: {}\nlist3: {}\nlist4: {}\n'
.format(id(list1), id(list2), id(list3), id(list4)))
list2[0] = 3
print('list1:', list1)
list3[0] = 4
list4[1] = 4
print('list1:', list1)
IDs:
list1: 4346366472
list2: 4346366472
list3: 4346366408
list4: 4346366536
list1: [3, 2]
list1: [3, 2]
Deep copy
As we have seen above, a shallow copy works fine if we want to create a new list with contents of the original list which we want to modify independently.
However, if we are dealing with compound objects (e.g., lists that contain other lists, read here for more information) it becomes a little trickier.
In the case of compound objects, a shallow copy would create a new compound object, but it would just insert the references to the contained objects into the new compound object. In contrast, a deep copy would go “deeper” and create also new objects
for the objects found in the original compound object. If you follow the code, the concept should become more clear:
from copy import deepcopy
list1 = [[1],[2]]
list2 = list1.copy() # shallow copy
list3 = deepcopy(list1) # deep copy
print('IDs:\nlist1: {}\nlist2: {}\nlist3: {}\n'
.format(id(list1), id(list2), id(list3)))
list2[0][0] = 3
print('list1:', list1)
list3[0][0] = 5
print('list1:', list1)
IDs:
list1: 4377956296
list2: 4377961752
list3: 4377954928
list1: [[3], [2]]
list1: [[3], [2]]
Don’t use mutable objects as default arguments for functions!
Don’t use mutable objects (e.g., dictionaries, lists, sets, etc.) as default arguments for functions! You might expect that a new list is created every time when we call the function without providing an argument for the default parameter, but this is not the case: Python will create the mutable object (default parameter) the first time the function is defined - not when it is called, see the following code:
(Original source: http://docs.python-guide.org/en/latest/writing/gotchas/
def append_to_list(value, def_list=[]):
def_list.append(value)
return def_list
my_list = append_to_list(1)
print(my_list)
my_other_list = append_to_list(2)
print(my_other_list)
[1]
[1, 2]
Another good example showing that demonstrates that default arguments are created when the function is created (and not when it is called!):
import time
def report_arg(my_default=time.time()):
print(my_default)
report_arg()
time.sleep(5)
report_arg()
1397764090.456688
1397764090.456688
Don’t use mutable objects (e.g., dictionaries, lists, sets, etc.) as default arguments for functions! You might expect that a new list is created every time when we call the function without providing an argument for the default parameter, but this is not the case: Python will create the mutable object (default parameter) the first time the function is defined - not when it is called, see the following code:
(Original source: http://docs.python-guide.org/en/latest/writing/gotchas/
def append_to_list(value, def_list=[]):
def_list.append(value)
return def_list
my_list = append_to_list(1)
print(my_list)
my_other_list = append_to_list(2)
print(my_other_list)
[1]
[1, 2]
Another good example showing that demonstrates that default arguments are created when the function is created (and not when it is called!):
import time
def report_arg(my_default=time.time()):
print(my_default)
report_arg()
time.sleep(5)
report_arg()
1397764090.456688
1397764090.456688
Python’s LEGB scope resolution and the keywords global
and nonlocal
There is nothing particularly surprising about Python’s LEGB scope resolution (Local -> Enclosed -> Global -> Built-in), but it is still useful to take a look at some examples!
There is nothing particularly surprising about Python’s LEGB scope resolution (Local -> Enclosed -> Global -> Built-in), but it is still useful to take a look at some examples!
global
vs. local
According to the LEGB rule, Python will first look for a variable in the local scope. So if we set the variable x = 1
local
ly in the function’s scope, it won’t have an effect on the global
x
.
x = 0
def in_func():
x = 1
print('in_func:', x)
in_func()
print('global:', x)
in_func: 1
global: 0
If we want to modify the global
x via a function, we can simply use the global
keyword to import the variable into the function’s scope:
x = 0
def in_func():
global x
x = 1
print('in_func:', x)
in_func()
print('global:', x)
in_func: 1
global: 1
According to the LEGB rule, Python will first look for a variable in the local scope. So if we set the variable x = 1
local
ly in the function’s scope, it won’t have an effect on the global
x
.
x = 0
def in_func():
x = 1
print('in_func:', x)
in_func()
print('global:', x)
in_func: 1
global: 0
If we want to modify the global
x via a function, we can simply use the global
keyword to import the variable into the function’s scope:
x = 0
def in_func():
global x
x = 1
print('in_func:', x)
in_func()
print('global:', x)
in_func: 1
global: 1
local
vs. enclosed
Now, let us take a look at local
vs. enclosed
. Here, we set the variable x = 1
in the outer
function and set x = 1
in the enclosed function inner
. Since inner
looks in the local scope first, it won’t modify outer
’s x
.
def outer():
x = 1
print('outer before:', x)
def inner():
x = 2
print("inner:", x)
inner()
print("outer after:", x)
outer()
outer before: 1
inner: 2
outer after: 1
Here is where the nonlocal
keyword comes in handy - it allows us to modify the x
variable in the enclosed
scope:
def outer():
x = 1
print('outer before:', x)
def inner():
nonlocal x
x = 2
print("inner:", x)
inner()
print("outer after:", x)
outer()
outer before: 1
inner: 2
outer after: 2
Now, let us take a look at local
vs. enclosed
. Here, we set the variable x = 1
in the outer
function and set x = 1
in the enclosed function inner
. Since inner
looks in the local scope first, it won’t modify outer
’s x
.
def outer():
x = 1
print('outer before:', x)
def inner():
x = 2
print("inner:", x)
inner()
print("outer after:", x)
outer()
outer before: 1
inner: 2
outer after: 1
Here is where the nonlocal
keyword comes in handy - it allows us to modify the x
variable in the enclosed
scope:
def outer():
x = 1
print('outer before:', x)
def inner():
nonlocal x
x = 2
print("inner:", x)
inner()
print("outer after:", x)
outer()
outer before: 1
inner: 2
outer after: 2
No comments:
Post a Comment