Tutorial: Objects and Classes in Python and Sage
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 :ref:`tutorial-programming-python`), but now further knowledge about objects and classes is assumed. It is designed as an alternating sequence of formal introduction and exercises. :ref:`solutions` are given at the end.
Unknown interpreted text role "ref". Unknown interpreted text role "ref".The object oriented programming paradigm relies on the two following fundamental rules:
At this point, those two rules are a little meaningless, so let's give some more or less precise definition of the terms:
Let's start with some examples: We consider the vector space over $QQ$ 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 :ref:`private attributes <private_attributes>` below).
Unknown interpreted text role "ref".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 :ref:`sage_specifics`) for printing, __add__ and __mult__ for operators + and * (see http://docs.python.org/library/) for a complete list:
Unknown interpreted text role "ref". {{{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__ ///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:
sage: e = Integer(9) sage: type(e) <type 'sage.rings.integer.Integer'> sage: e.__dict__ <dictproxy object at 0x...> sage: e.__dict__.keys() ['__module__', '_reduction', '__doc__', '_sage_src_lines_'] sage: id4 = SymmetricGroup(4).one() sage: type(id4) <type 'sage.groups.perm_gps.permgroup_element.PermutationGroupElement'> sage: id4.__dict__ <dictproxy object at 0x...>
Note
Each objects corresponds to a portion of memory called its identity in python. You can get the identity using id:
{{{id=13| el = Integer(9) id(el) # random /// 139813642977744 }}} {{{id=14| el1 = el; id(el1) == id(el) /// True }}} {{{id=15| el1 is el /// True }}}This is different from mathematical identity:
sage: el2 = Integer(9) sage: el2 == el1 True sage: el2 is el1 False sage: id(el2) == id(el) False
To define some object, you first have to write a class. The class will defines the methods and the attributes of the object.
Let's write a small class about glasses in a restaurant:
{{{id=16| 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=17| myGlass = Glass(10); myGlass /// An empty glass of size 10.0 }}} {{{id=18| myGlass.fill(); myGlass /// A glass of size 10.0 cl containing 10.0 cl of water }}} {{{id=19| myGlass.empty(); myGlass /// An empty glass of size 10.0 }}}Some comments:
Note
Private Attributes
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=20| 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=21| class Spoon(AbstractDish): def __repr__(self): return "A %s spoon"%self.state() def eat_with(self): self._make_dirty() /// }}}Let's tests it:
{{{id=22| s = Spoon(); s /// A clean spoon }}} {{{id=23| s.is_clean() /// True }}} {{{id=24| s.eat_with(); s /// A dirty spoon }}} {{{id=25| s.is_clean() /// False }}} {{{id=26| s.wash(); s /// A clean spoon }}}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=27| type(s) ///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=30| s.__repr__() /// 'A clean spoon' }}} {{{id=31| Spoon.__repr__(s) /// 'A clean spoon' }}} {{{id=32| 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=33| 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:
sage: 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() sage: s = Spoon() Building a spoon sage: s A clean spoon
Compared to Python, Sage has its particular way to handles objects:
Here is a solution to the first exercise:
{{{id=34| class Glass(object): def __init__(self, size): assert size > 0 self._size = float(size) self.wash() 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 %s"%( self._size, self._content, self._beverage) def content(self): return self._content def beverage(self): return self._beverage def fill(self, beverage = "water"): if not self.is_clean(): raise ValueError, "Don't want to fill a dirty glass" self._clean = False self._content = self._size self._beverage = beverage def empty(self): self._content = float(0.0) def is_empty(self): return self._content == 0.0 def drink(self, amount): if amount <= 0.0: raise ValueError, "amount must be positive" elif amount > self._content: raise ValueError, "not enough beverage in the glass" else: self._content -= float(amount) def is_clean(self): return self._clean def wash(self): self._content = float(0.0) self._beverage = None self._clean = True /// }}}Let's check that everything is working as expected:
{{{id=35| G = Glass(10.0) G /// An empty glass of size 10.0 }}} {{{id=36| G.is_empty() /// True }}} {{{id=37| G.drink(2) /// Traceback (most recent call last): ValueError: not enough beverage in the glass }}} {{{id=38| G.fill("beer") G /// A glass of size 10.0 cl containing 10.0 cl of beer }}} {{{id=39| G.is_empty() /// False }}} {{{id=40| G.is_clean() /// False }}} {{{id=41| G.drink(5.0) G /// A glass of size 10.0 cl containing 5.0 cl of beer }}} {{{id=42| G.is_empty() /// False }}} {{{id=43| G.is_clean() /// False }}} {{{id=44| G.drink(5) G /// An empty glass of size 10.0 }}} {{{id=45| G.is_clean() /// False }}} {{{id=46| G.fill("orange juice") /// Traceback (most recent call last): ValueError: Don't want to fill a dirty glass }}} {{{id=47| G.wash() G /// An empty glass of size 10.0 }}} {{{id=48| G.fill("orange juice") G /// A glass of size 10.0 cl containing 10.0 cl of orange juice }}}Here is the solution to the second exercice:
TODO !!!!
That all folks !