This tutorial is an introduction to object oriented programming in Python and
Sage. It requires basic knowledges on imperative/procedural programming (the
most common programming style) that is conditional instructions, loops,
functions (see Tutorial: Programming in Python and Sage), but now further knowledge
about objects and classes is assumed. It is designed as an alternating
sequence of formal introduction and exercises. Solutions to the exercises are given at
the end.
Object oriented programming paradigm
The object oriented programming paradigm relies on the two following
fundamental rules:
- Any thing of the real (or mathematical) world which needs to be manipulated
by the computer is modeled by an object.
- Each object is an instance of some class.
At this point, those two rules are a little meaningless, so let’s give some
more or less precise definition of the terms:
- object
- a portion of memory which contains the information needed to model
the real world thing.
- class
- defines the data structure used to store the objects which are instance
of the class together with their behavior.
Let’s start with some examples: We consider the vector space over
whose
basis is indexed by permutations, and a particular element in it:
{{{id=0|
F = CombinatorialFreeModule(QQ, Permutations())
el = 3*F([1,3,2])+ F([1,2,3])
el
///
B[[1, 2, 3]] + 3*B[[1, 3, 2]]
}}}
In python, everything is an object so there isn’t any difference between types
and classes. On can get the class of the object el by:
{{{id=1|
type(el)
///
}}}
As such, this is not very informative. We’ll go back to it later. The data
associated to objects are stored in so called attributes. They are
accessed through the syntax obj.attributes_name:
{{{id=2|
el._monomial_coefficients
///
{[1, 2, 3]: 1, [1, 3, 2]: 3}
}}}
Modifying the attribute modifies the objects:
{{{id=3|
el._monomial_coefficients[Permutation([3,2,1])] = 1/2
el
///
B[[1, 2, 3]] + 3*B[[1, 3, 2]] + 1/2*B[[3, 2, 1]]
}}}
Warning
as a user, you are not supposed to do that by yourself (see
note on private attributes below).
As an element of a vector space el has a particular behavior:
{{{id=4|
2*el
///
2*B[[1, 2, 3]] + 6*B[[1, 3, 2]] + B[[3, 2, 1]]
}}}
{{{id=5|
el.support()
///
[[1, 2, 3], [1, 3, 2], [3, 2, 1]]
}}}
{{{id=6|
el.coefficient([1, 2, 3])
///
1
}}}
The behavior is defined through methods (support, coefficient). Note
that this is true, even for equality, printing or mathematical operations. For
example the call a == b actually is translated to the method call
a.__eq__(b). The names of those special methods which are usually called
through operators are fixed by the Python language and are of the form
__name__. Example include __eq__, __le__ for operators == and
<=, __repr__ (see Sage specifics about classes) for printing, __add__ and
__mult__ for operators + and * (see
http://docs.python.org/library/) for a complete list:
{{{id=7|
el.__eq__(F([1,3,2]))
///
False
}}}
{{{id=8|
el.__repr__()
///
'B[[1, 2, 3]] + 3*B[[1, 3, 2]] + 1/2*B[[3, 2, 1]]'
}}}
{{{id=9|
el.__mul__(2)
///
2*B[[1, 2, 3]] + 6*B[[1, 3, 2]] + B[[3, 2, 1]]
}}}
Some particular actions allows to modify the data structure of el:
{{{id=10|
el.rename("bla")
el
///
bla
}}}
Note
The class is stored in a particular attribute called __class__ the
normal attribute are stored in a dictionary called __dict__:
{{{id=11|
F = CombinatorialFreeModule(QQ, Permutations())
el = 3*F([1,3,2])+ F([1,2,3])
el.rename("foo")
el.__class__
///
}}}
{{{id=12|
el.__dict__
///
{'_monomial_coefficients': {[1, 2, 3]: 1, [1, 3, 2]: 3}, '__custom_name': 'foo'}
}}}
Lots of sage objects are not Python objects but compiled Cython
objects. Python sees them as builtin objects and you don’t have access to
the data structure. Examples include integers and permutation group
elements:
{{{id=13|
e = Integer(9)
type(e)
///
}}}
{{{id=14|
e.__dict__
///
}}}
{{{id=15|
e.__dict__.keys()
///
['__module__', '_reduction', '__doc__', '_sage_src_lines_']
}}}
{{{id=16|
id4 = SymmetricGroup(4).one()
type(id4)
///
}}}
{{{id=17|
id4.__dict__
///
}}}
Note
Each objects corresponds to a portion of memory called its identity in
python. You can get the identity using id:
{{{id=18|
el = Integer(9)
id(el) # random
///
139813642977744
}}}
{{{id=19|
el1 = el; id(el1) == id(el)
///
True
}}}
{{{id=20|
el1 is el
///
True
}}}
This is different from mathematical identity:
{{{id=21|
el2 = Integer(9)
el2 == el1
///
True
}}}
{{{id=22|
el2 is el1
///
False
}}}
{{{id=23|
id(el2) == id(el)
///
False
}}}
Summary
To define some object, you first have to write a class. The class will
defines the methods and the attributes of the object.
- method
- particular kind of function associated with an object used to get
information about the object or to manipulate it.
- attribute
- variables where the info about the object are stored;
An example: glass of beverage in a restaurant
Let’s write a small class about glasses in a restaurant:
{{{id=24|
class Glass(object):
def __init__(self, size):
assert size > 0
self._size = float(size)
self._content = float(0.0)
def __repr__(self):
if self._content == 0.0:
return "An empty glass of size %s"%(self._size)
else:
return "A glass of size %s cl containing %s cl of water"%(
self._size, self._content)
def fill(self):
self._content = self._size
def empty(self):
self._content = float(0.0)
///
}}}
Let’s create a small glass:
{{{id=25|
myGlass = Glass(10); myGlass
///
An empty glass of size 10.0
}}}
{{{id=26|
myGlass.fill(); myGlass
///
A glass of size 10.0 cl containing 10.0 cl of water
}}}
{{{id=27|
myGlass.empty(); myGlass
///
An empty glass of size 10.0
}}}
Some comments:
- The method __init__ is used to initialize the object, it is used by the
so called constructor of the class that is executed when calling Glass(10).
- The method __repr__ is supposed to return a string which is used to
print the object.
Note
Private Attributes
- most of the time, the user should not change directly the
attribute of an object. Those attributes are called private. Since
there is no mechanism to ensure privacy in python, the usage is to prefix
the name by an underscore.
- as a consequence attribute access is only made through methods.
- methods which are only for internal use are also prefixed with an
underscore.
Exercises
- add a method is_empty which returns true if a glass is empty.
- define a method drink with a parameter amount which allows to
partially drink the water in the glass. Raise an error if one asks to
drink more water than there is in the glass or a negative amount of
water.
- Allows the glass to be filled with wine, beer or other beverage. The method
fill should accept a parameter beverage. The beverage is stored in
an attribute _beverage. Update the method __repr__ accordingly.
- Add an attribute _clean and methods is_clean and wash. At the
creation a glass is clean, as soon as it’s filled it becomes dirty, and must
be washed to become clean again.
- Test everything.
- Make sure that everything is tested.
- Test everything again.
Inheritance
The problem: object of different classes may share a common behavior.
For example, if one wants to deal now with different dishes (forks, spoons
...) then there is common behavior (becoming dirty and being washed). So the
different classes associated to the different kinds of dishes should have the
same clean, is_clean and wash methods. But copying and pasting
code is bad and evil ! This is done by having a base class which factorizes
the common behavior:
{{{id=28|
class AbstractDish(object):
def __init__(self):
self._clean = True
def is_clean(self):
return self._clean
def state(self):
return "clean" if self.is_clean() else "dirty"
def __repr__(self):
return "An unspecified %s dish"%self.state()
def _make_dirty(self):
self._clean = False
def wash(self):
self._clean = True
///
}}}
Now one can reuse this behavior within a class Spoon:
{{{id=29|
class Spoon(AbstractDish):
def __repr__(self):
return "A %s spoon"%self.state()
def eat_with(self):
self._make_dirty()
///
}}}
Let’s tests it:
{{{id=30|
s = Spoon(); s
///
A clean spoon
}}}
{{{id=31|
s.is_clean()
///
True
}}}
{{{id=32|
s.eat_with(); s
///
A dirty spoon
}}}
{{{id=33|
s.is_clean()
///
False
}}}
{{{id=34|
s.wash(); s
///
A clean spoon
}}}
Summary
Any class can reuse the behavior of another class. One says that the
subclass inherits from the superclass or that it derives from it.
Any instance of the subclass is also an instance its superclass:
{{{id=35|
type(s)
///
}}}
{{{id=36|
isinstance(s, Spoon)
///
True
}}}
{{{id=37|
isinstance(s, AbstractDish)
///
True
}}}
If a subclass redefines a method, then it replaces the former one. One says
that the subclass overloads the method. One can nevertheless explicitly
call the hidden superclass method.
{{{id=38|
s.__repr__()
///
'A clean spoon'
}}}
{{{id=39|
Spoon.__repr__(s)
///
'A clean spoon'
}}}
{{{id=40|
AbstractDish.__repr__(s)
///
'An unspecified clean dish'
}}}
Note
Advanced superclass method call
Sometimes one wants to call an overloaded method without knowing in which
class it is defined. On use the super operator
{{{id=41|
super(Spoon, s).__repr__()
///
'An unspecified clean dish'
}}}
A very common usage of this construct is to call the __init__ method of the
super classes:
{{{id=42|
class Spoon(AbstractDish):
def __init__(self):
print "Building a spoon"
super(Spoon, self).__init__()
def __repr__(self):
return "A %s spoon"%self.state()
def eat_with(self):
self.make_dirty()
s = Spoon()
///
Building a spoon
}}}
{{{id=43|
s
///
A clean spoon
}}}
Exercises
- Modify the class Glasses so that it inherits from Dish.
- Write a class Plate whose instance can contain any meals together with
a class Fork. Avoid at much as possible code duplication (hint:
you can write a factorized class ContainerDish).
- Test everything.