OOP II.
5
is an instance of int
.
__init__
)
self
, it is defined inside a method. It is the property of an instance, different instances can have different values.
self
.
Let's write a python class Person
which stores a name and a title (Mr, Ms, Dr ...).
Let's also write a __str__
method which prints the name nicely.
After that we make a Knight
class inherited from the Person
. The knights are just like the persons but their title is "Sir"
.
See how the child class calles the parent's constructor (super
).
class Person:
def __init__(self, name, title):
self.name = name
self.title = title
def __str__(self):
return self.title + " " + self.name
class Knight(Person):
def __init__(self, name):
super().__init__(name, 'Sir')
smith = Person('Smith', 'Mr')
launcelot = Knight('Launcelot')
print(smith)
print(launcelot)
You can also define members in the child class, but they are only present in the child, not in the parent.
For example a Knight can have an optional epithet (a descriptive byname)!
class Person:
def __init__(self, name, title):
self.name = name
self.title = title
def __str__(self):
return self.title + " " + self.name
class Knight(Person):
def __init__(self, name, epithet=""):
super().__init__(name, 'Sir')
self.epithet = epithet
launcelot = Knight('Launcelot', 'the brave')
print(launcelot)
Now we override the inherited __str__
method with a new one, a same method but in the child class.
If you don't write a new method, the parent (inherited) method is used instead.
class Person:
def __init__(self, name, title):
self.name = name
self.title = title
def __str__(self):
return self.title + " " + self.name
class Knight(Person):
def __init__(self, name, epithet=""):
super().__init__(name, 'Sir')
self.epithet = epithet
def __str__(self):
if len(self.epithet) > 0:
return self.title + " " + self.name + ", " + self.epithet
else:
return super().__str__()
launcelot = Knight('Launcelot', 'the brave')
black = Knight('Black')
robin = Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')
print(launcelot)
print(black)
print(robin)
One can define a variable in a class but outside a method. This is a class member, it is the same for all of the instances.
For example the "Sir"
title of knights is the same for all knights.
Let's call this special_title
, not to interfere with the Person
s title
member.
The Knight.special_title
will be the same as any instance's .special_title
.
class Person:
def __init__(self, name, title):
self.name = name
self.title = title
def __str__(self):
return self.title + " " + self.name
class Knight(Person):
special_title = 'Sir'
def __init__(self, name, epithet=""):
super().__init__(name, "")
self.epithet = epithet
def __str__(self):
return Knight.special_title + " " + self.name + ", " + self.epithet
launcelot = Knight('Launcelot', 'the brave')
robin = Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')
print(launcelot)
print(robin)
print(robin.special_title, launcelot.special_title, Knight.special_title)
The child class can be a parent of a third class, the inheritence structure can be deeper and more complicated.
There are cases when your code does something wrong or encounters some invalid value.
In this case an object of type Exception
is raised (or thrown).
What happens:
Unless
try:
...
except ... :
...
An exception can be handled at any place with this block. If it is not handled, then the exception can break out from any functions.
For example let's raise an exception:
try:
raise Exception('spam', 'eggs')
except Exception as inst:
print(type(inst))
print(inst.args)
print(inst)
x, y = inst.args
print('x =', x)
print('y =', y)
Now handle an other exception:
try:
str(5) + 5
except TypeError as inst:
print(type(inst))
print(inst.args)
print(inst)
It is a natural way to handle exceptions in order to correct mistakes.
For example read an integer from the user, but prepare that the user might not enter a proper float.
while True:
try:
x = float(input("Please enter a real number: "))
break
except ValueError:
print("Oops! That was not a real number. Try again...")
print(2*x)
Look where the exception happens and where is it handled.
def f(x):
return x + 5 # not good if x is a string
def g(x):
return x + x # good for integers and strings too
x = "5"
y = g(x)
z = f(y)
def read(n):
maximum = float("-inf")
for i in range(n):
x = float(input())
if x > maximum:
maximum = x
return maximum
try:
y = read(3)
print(y)
except ValueError as e:
print(e)
You can write your own exception class. You can use it to mark that the error happened in one of your own code.
All you have to do is to inherit from the built-in Exception
class.
Optionally, you can have other functions or members in the exception class.
class KnightException(Exception):
pass
try:
lancelot = Person("Lancelot", "Mr")
x = str(lancelot)
if x[:3] != "Sir":
raise KnightException("Use correct title")
except KnightException as s:
print(s)
As we have seen, the for loop can work on several data types:
for i in L:
L
can be list, tuple, dict.
What type of objects can follow for ... in
?
What happens in a for loop?
for ... in OBJ:
calls the iter(OBJ)
function (the __iter__(OBJ)
method), which returns an iterator object. If OBJ
has this method, it means, OBJ
is iterable.__next__()
method which returns the next elements.StopIteration
exception if it ran out of elements.An object is iterable if one can iterate over, like the list, tuple. It generates an iterator when passed to iter()
function (or __iter__()
method). Iterators have __next__()
method, which returns the next item of the object. Every iterator is also an iterable, but not vicaversa. For example, a list is iterable but it is not an iterator. An iterable object needs a method __iter__()
, which returns an iterator (or a __getitem__()
method with sequential indexes starting with 0).
These are special methods!
r = range(3) # iterable
print(type(r))
it = iter(r) # iterator
print(type(it))
next(it)
print(next(it))
print(next(it))
next(it)
You can write your own iterable and tell python how to iterate over it.
First you need an __iter__
method, the returned object can be the self
or a built-in iterable (list, set, tuple
).
The iterator object have to define a __next__()
method which returns the elements one after the other.
Raise StopIteration when you want to stop.
class Group:
def __init__(self, name, persons):
self.persons = persons
self.name = name
def __iter__(self):
self.index = 0
return self
def __next__(self):
if self.index >= len(self.persons):
raise StopIteration # tell to stop
self.index += 1
return self.persons[self.index - 1]
kotrt = Group('Knights of The Round Table',
[Knight('Launcelot', 'the brave'),
Knight('Galahad', 'the pure'),
Knight('Bedevere', 'the wise'),
Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')])
for knight in kotrt:
print(knight)
In this case you could solve this easier, since a list
is already iterable:
class Group:
def __init__(self, name, persons):
self.persons = persons
self.name = name
def __iter__(self):
return iter(self.persons)
kotrt = Group('Knights of The Round Table',
[Knight('Launcelot', 'the brave'),
Knight('Galahad', 'the pure'),
Knight('Bedevere', 'the wise'),
Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')])
for knight in kotrt:
print(knight)
Of course, we may iterate over the persons of the class, as .persons
is iterable, but this solution is not so nice, as we have to know the structure of the data. It is not enough to know the interface:
class Group:
def __init__(self, name, persons):
self.persons = persons
self.name = name
kotrt = Group('Knights of The Round Table',
[Knight('Launcelot', 'the brave'),
Knight('Galahad', 'the pure'),
Knight('Bedevere', 'the wise'),
Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')])
# kotrt is not iterable but kotrt.persons is
for knight in kotrt.persons:
print(knight)