Copyright 2020 Doulos Ltd
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
There is an executable version of this document available on Google Colab, allowing you to run every Python code fragment from this document, online, without having to install Python on your computer. Click here to register and gain access.
Every Python object has a collection of built-in attributes, which can be listed using the built-in function dir. In effect, these are all the things you can do with the given object. In this example, the object is an integer.
v = 1
dir(v)
v = -1
v.__abs__()
abs(v)
v = 2
v.__add__(3)
The normal way to call _add__ is using the + operator.
v + 3
There are also non-dunder attributes which are functions that are intended to be called directly. For example, the built-in attribute bit_length, which is a Python function:
v = 0b10011001
v.bit_length
v.bit_length()
def do_it(arg):
arg.do_this()
arg.do_that()
class C:
def do_this(self):
print('do_this from C')
def do_that(self):
print('do_that from C')
def do_c(self):
print('do_c')
obj_c = C()
do_it(obj_c)
class D:
def do_this(self):
print('do_this from D')
def do_that(self):
print('do_that from D')
def do_d(self):
print('do_d')
obj_d = D()
do_it(obj_d)
def do_it(arg):
assert isinstance(arg, C)
arg.do_this()
arg.do_that()
try:
do_it(obj_d)
except AssertionError as details:
print('Wrong argument type', details)
class C:
def __init__(self, value):
self.value = value
def __eq__(self, other):
print(f'Test for equality, '
f'self={self.value}, other={other.value}')
return self.value == other.value
def __add__(self, other):
return C(self.value + other.value)
obj1 = C(5)
obj2 = C(5)
obj3 = C(10)
assert obj1 == obj2
assert not obj2 == obj3
assert obj2 != obj3
assert obj1 is not obj2
assert obj1 + obj2 == obj3
assert C(1) + C(2) == C(3)
assert C(-4) + C(4) == C(0)
class C:
def __init__(self, value):
assert isinstance(value, float)
self.value = value
def __round__(self, n):
return C(round(self.value, n))
def get(self):
return self.value
obj = C(1.9788)
obj2 = round(obj, 2)
obj2.get()
try:
int(obj)
except TypeError as details:
print(details)
class C:
def __init__(self, value):
assert isinstance(value, float)
self.value = value
def __int__(self):
return int(self.value)
obj = C(1.9788)
int(obj)
Functions are callable. By default, objects are not.
def f():
pass
f()
class C:
pass
obj = C()
try:
obj()
except TypeError as details:
print(details)
class C:
#def __init__(self):
# print('__init__')
# self.value = 0
def __call__(self, arg1=None, arg2=None):
print(f'__call__ with arg1={arg1}, arg2={arg2}')
self.value = arg1
return self.value
obj = C()
obj()
obj(1, 2)
callable(C)
callable(obj)
hasattr(obj, "__call__")
callable(lambda x: x)
callable(99)
An integer value such as 4 is not iterable.
try:
for i in 4:
pass
except TypeError as details:
print(details)
for i in range(4):
pass
try:
next(range(4))
except TypeError as details:
print(details)
it = iter(range(4))
next(it)
class C_iter:
def __init__(self, values):
assert isinstance(values, list)
self.index = 0
self.values = values
def __iter__(self):
return self
def __next__(self):
next_obj = None
while (next_obj is None):
if self.index >= len(self.values):
raise StopIteration
next_obj = self.values[self.index]
self.index += 1
print("C_iter returns " + str(next_obj))
return next_obj
obj = C_iter([1, 2, 3])
for i in obj:
print(i)
obj = C_iter([1, 2, 3])
try:
while(True):
print(next(obj))
except StopIteration:
print('Done!')
class Copyable():
def __init__(self):
self.data = [1, 2, [3, 4, 5]]
import copy
obj = Copyable()
print(f"obj.data = {obj.data}")
shallow_copy = copy.copy(obj)
deep_copy = copy.deepcopy(obj)
assert shallow_copy.data == [1, 2, [3, 4, 5]]
assert shallow_copy is not obj
assert shallow_copy.data[2] is obj.data[2]
assert deep_copy.data == [1, 2, [3, 4, 5]]
assert deep_copy is not obj
assert deep_copy.data[2] is not obj.data[2]
import warnings
class Uncopyable():
def __init__(self):
self.data = [1, 2, [3, 4, 5]]
def __copy__(self):
warnings.warn("Shallow copy disabled, returning a reference instead")
return self
def __deepcopy__(self, memo):
warnings.warn("Deep copy disabled, returning a reference instead")
return self
obj = Uncopyable()
print(f"obj.data = {obj.data}")
shallow_copy = copy.copy(obj)
deep_copy = copy.deepcopy(obj)
assert shallow_copy is obj
assert deep_copy is obj
s = [1, 2, 3, 3]
3 in s # Membership
s + s # Concatenation
s * 3 # Concatenating a sequence with itself N times
s[0] # Indexing
s[1:3] # Slicing
len(s) # Length (number of items)
max(s) # Largest item
s.index(2) # Index of first occurrence of an item
s.count(3) # Number of occurrences of an item
s[2] = 0 # Assignment to an item
s
s[1:3] = [5, 6] # Assignment to a slice
s
del s[1] # Deleting an item or slice
s
s.append(4)
s
s.insert(1, 2)
s
s.reverse()
s
s.pop()
s
s.clear()
s
class C:
def __init__(self, data):
self.data = data;
obj = C("abcde")
try:
obj[1]
except TypeError as details:
print(details)
class C:
def __init__(self, data):
self.data = data;
def __getitem__(self, index):
return self.data[index]
obj1 = C("abcde")
assert obj1[1] == 'b'
assert 'd' in obj1
obj2 = C([1, 2, 3])
assert obj2[2] == 3
assert 3 in obj2
try:
obj2[2] = 4
except TypeError as details:
print(details)
try:
assert len(obj2) == 3
except TypeError as details:
print(details)
class C:
def __init__(self, data):
self.data = data;
def __getitem__(self, index):
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value
def __len__(self):
return len(self.data)
obj3 = C([1, 2, 3])
obj3[0] = 0
obj3[2] = 4
assert obj3[0] == 0 and obj3[2] == 4
assert len(obj3) == 3
class C:
def __init__(self, size):
self.size = size
self.data = [None] * size;
def check_key(self, key):
if key < 0 or key >= self.size:
raise KeyError(f'key={key} out of range [0:{self.size}]')
def __getitem__(self, key):
self.check_key(key)
return self.data[key]
def __setitem__(self, key, value):
self.check_key(key)
self.data[key] = value
obj4 = C(4)
try:
obj4[6] = 1
except KeyError as details:
print(details)
obj4 = C(4)
try:
obj4[1:3] = [1, 2]
except TypeError as details:
print(details)
indices = slice(1, 6, 2)
assert indices.start == 1
assert indices.stop == 6
assert indices.step == 2
assert isinstance(indices, slice)
data = [i for i in range(10)]
assert data[indices] == [1, 3, 5]
class C:
def __init__(self, size):
self.size = size
self.data = [None] * size;
def check_key(self, key):
msg = f'key={key} out of range [0:{self.size}]'
if isinstance(key, int):
if key < 0 or key >= self.size:
raise KeyError(msg)
elif isinstance(key, slice):
if key.start < 0 or key.stop > self.size:
raise KeyError(msg)
else:
raise KeyError(msg)
def __getitem__(self, key):
self.check_key(key)
return self.data[key]
def __setitem__(self, key, value):
self.check_key(key)
self.data[key] = value
obj5 = C(4)
obj5[0:4] = [1, 2, 3, 4]
assert obj5[1:3] == [2, 3]
try:
[item for item in obj5]
except Exception as details:
print(details)
class C:
def __init__(self, size):
self.size = size
self.data = [None] * size;
def check_key(self, key):
msg = f'key={key} out of range'
if isinstance(key, int):
if key < 0 or key >= self.size:
raise KeyError(msg)
elif isinstance(key, slice):
if key.start < 0 or key.stop > self.size:
raise KeyError(msg)
else:
raise KeyError(msg)
def __getitem__(self, key):
self.check_key(key)
return self.data[key]
def __setitem__(self, key, value):
self.check_key(key)
self.data[key] = value
def __iter__(self):
for i in self.data:
if i is not None:
yield i
obj5 = C(4)
obj5[0:4] = [0, 3, 5, 2]
list(obj5)
tuple(obj5)
set(obj5)
all(obj5)
any(obj5)
min(obj5)
max(obj5)
sum(obj5)
sorted(obj5)
[item for item in obj5]
for i, item in enumerate(obj5):
print(i, item, end=', ')
tuple(filter(lambda x: x > 1, obj5))
tuple(map(lambda x: -x, obj5))
tuple(zip(obj5, obj5))
try:
reversed(obj5)
except Exception as details:
print(details)
class C:
def __init__(self, size):
self.size = size
self.data = [None] * size
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
def __iter__(self):
for i in self.data:
if i is not None:
yield i
def __reversed__(self):
for i in self.data[::-1]:
if i is not None:
yield i
obj6 = C(4)
obj6[0:4] = [1, 2, 3, 4]
assert list(reversed(obj6)) == [4, 3, 2, 1]
class C:
def __init__(self, size):
self.size = size
self.data = [None] * size
def check_key(self, key):
msg = f'key={key} out of range'
if isinstance(key, int):
if key < 0 or key >= self.size:
raise KeyError(msg)
elif isinstance(key, slice):
if key.start < 0 or key.stop > self.size:
raise KeyError(msg)
else:
raise KeyError(msg)
def __getitem__(self, key):
self.check_key(key)
return self.data[key]
def __setitem__(self, key, value):
self.check_key(key)
self.data[key] = value
def __delitem__(self, key):
self.check_key(key)
del self.data[key]
def __iter__(self):
for i in self.data:
if i is not None:
yield i
def __add__(self, other):
assert isinstance(other, C)
tmp = C(0)
tmp.data.extend(self.data)
tmp.data.extend(other.data)
tmp.size = self.size + other.size
return tmp
def __mul__(self, n):
assert isinstance(n, int)
tmp = C(0)
for i in range(n):
tmp.data.extend(self.data)
tmp.size = self.size * n
return tmp
obj7 = C(3)
obj7[0:3] = [1, 2, 3]
assert list(obj7 + obj7) == [1, 2, 3, 1, 2, 3]
assert list(obj7 * 3) == [1, 2, 3, 1, 2, 3, 1, 2, 3]
del obj7[1]
assert list(obj7) == [1, 3]
If neither __str__ nor __repr__ are defined, the default is a fairly useless string that contains just the class name and object address.
class C:
def __init__(self, value):
self.value = value
obj8 = C(0)
str(obj8)
repr(obj8)
class C:
def __init__(self, value):
self.value = value
def __str__(self):
return f'{self.value}'
def __repr__(self):
return f'C({self.value})'
obj9 = C(9)
print(obj9)
print(str(obj9))
print(repr(obj9))
obj9
class C:
def __init__(self, value):
self.value = value
def __repr__(self):
return f'C({self.value})'
obj = C(0)
str(obj)
class C:
def __init__(self, size):
self.size = size
self.data = [None] * size
def check_key(self, key):
msg = f'key={key} out of range'
if isinstance(key, int):
if key < 0 or key >= self.size:
raise KeyError(msg)
elif isinstance(key, slice):
if key.start < 0 or key.stop > self.size:
raise KeyError(msg)
else:
raise KeyError(msg)
def __getitem__(self, key):
self.check_key(key)
return self.data[key]
def __setitem__(self, key, value):
self.check_key(key)
self.data[key] = value
def __delitem__(self, key):
self.check_key(key)
del self.data[key]
def __len__(self):
return self.size
def __iter__(self):
for i in self.data:
if i is not None:
yield i
def __reversed__(self):
for i in self.data[::-1]:
if i is not None:
yield i
def __add__(self, other):
assert isinstance(other, C)
tmp = C(0)
tmp.data.extend(self.data)
tmp.data.extend(other.data)
tmp.size = self.size + other.size
return tmp
def __mul__(self, n):
assert isinstance(n, int)
tmp = C(0)
for i in range(n):
tmp.data.extend(self.data)
tmp.size = self.size * n
return tmp
def __repr__(self):
txt = '['
for i in self.data:
txt += str(i)
txt += ', '
txt = txt[:-2]
txt += ']'
return txt
obj = C(4)
obj[0:4] = [1, 2, 3, 4]
assert len(obj) == 4
assert sum(obj) == 10
assert list(obj) == [1, 2, 3, 4]
assert list(reversed(obj)) == [4, 3, 2, 1]
assert list(obj + obj) == [1, 2, 3, 4, 1, 2, 3, 4]
assert list(obj * 2) == [1, 2, 3, 4, 1, 2, 3, 4]
assert str(obj) == '[1, 2, 3, 4]'
obj
A content manager is an object that is used with the Python with syntax. It should define __enter__ and __exit__ methods, which are called at the top and bottom of the with construct, respectively. The __enter__ method is for running setup code, and the __exit__ method for running tear-down code. The context manager guarantees that the setup and tear-down code will be executed, meaning that any resources needed by the code inside the with will be allocated at the top and deallocated at the bottom, even if exceptions are raised inside the with construct.
One of the most common use cases for a context manager is to open and close files. The built-in open function returns a context manager as well as opening a file. The __enter__ method of that context manager returns the file object just opened. The __exit__ method closes the file.
%%writefile foo.txt
abc
123
with open('foo.txt') as f:
for line in f.read().splitlines():
print(line)
try:
with open('nonexistentfile.txt') as f:
pass
except Exception as details:
print(details)
class CM:
def __init__(self, obj):
print('Context manager created with argument of type', type(obj))
self.obj = obj
def __enter__(self):
print('Context manager setup code')
try:
self.obj.open()
except AttributeError:
print('Object cannot be opened')
return self.obj
def __exit__(self, exception_type, exception_val, trace):
print('Context manager tear-down code')
try:
self.obj.close()
except AttributeError:
print('Object cannot be closed')
return False
class Helper:
def open(self):
print('Opening object of class', Helper.__name__)
def close(self):
print('Closing object of class', Helper.__name__)
def printme(self):
print('Helper.print called')
with CM(Helper()):
pass
try:
with CM(Helper()):
1. / 0.
except ZeroDivisionError as details:
print(details)
class CMsilent(CM):
def __exit__(self, exception_type, exception_val, trace):
super().__exit__(exception_type, exception_val, trace)
return True
with CMsilent(Helper()):
1. / 0.
with CM(Helper()) as x:
x.printme()
with CM(0):
pass
Both built-in and user-defined objects have attributes. The int object is built into Python, so its attributes are fixed:
v = -7
type(v)
We start with an empty class:
class C:
pass
obj = C()
dir(obj)
setattr(obj, "foo1", 11)
obj.foo2 = 22
dir(obj)
getattr(obj, "foo2")
obj.foo1
Now a built-in attribute:
getattr(obj, "__class__")
hasattr(obj, "foo1")
hasattr(obj, "foo2")
hasattr(obj, "foobar")
Naturally, we can delete attributes too:
delattr(obj, "foo1")
delattr(obj, "foo2")
hasattr(obj, "foo1")
The normal practice in object-oriented programming (OOP) is to access the values of attributes using so-called getter and setter methods. The built-in function property gives a way to define managed attributes, that is, attributes of a class which, although they have user-defined getter, setter, and deleter methods, can be accessed directly without explicitly calling those methods. These user-defined methods will be called automatically whenever the attribute is accessed. This makes it possible to execute arbitrary user-defined code whenever an attribute is accessed; the user-defined methods do not need to be called explicitly. This facility could be used to ensure that the values of attributes are kept mutually consistent or to implement side-effects whenever an attribute is accessed.
The getter, setter, and deleter methods are first defined (as normal methods) and are then passed as arguments to the built-in property function, which returns the managed attribute:
class C:
def __init__(self):
self._x = None
def getx(self):
print('C.getx() returns', self._x)
return self._x
def setx(self, value):
if not isinstance(value, int):
raise TypeError('Expecting an int')
if value < 0:
raise ValueError('Expecting a non-negative int')
print('C.setx(', value, ')', sep='')
self._x = value
def delx(self):
print('C.delx()')
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
# property() is implemented using a data descriptor (the descriptor protocol)
c = C()
c.x = 99
assert c.x == 99
assert hasattr(c, 'x')
assert getattr(c, 'x') == 99
del c.x
assert not hasattr(c, 'x')
class C2:
def __init__(self):
self._x = None
@property
def x(self):
print('C2.getx() returns', self._x)
return self._x
@x.setter
def x(self, value):
if not isinstance(value, int):
raise TypeError('Expecting an int')
if value < 0:
raise ValueError('Expecting a non-negative int')
print('C2.setx(', value, ')', sep='')
self._x = value
@x.deleter
def x(self):
print('C2.delx()')
del self._x
c = C2()
c.x = 99
assert c.x == 99
del c.x
assert not hasattr(c, 'x')