None .. An html document created by ipypublish

outline: ipypublish.templates.outline_schemas/rst_outline.rst.j2 with segments: - nbsphinx-ipypublish-content: ipypublish sphinx content

8. Classes, object and OOP paradigm


*** Basile Marchand (Materials Center - Mines ParisTech / CNRS / PSL University)** *

8.1. Python an object language

8.1.1. Items

Now we’ll cover a key point, although we haven’t covered it so far, I want to talk about the fact that Python is an object-oriented language. This means that everything that we manipulate in the Python language (lists, dictionaries, character strings, module, …) is an object.

The question you ask yourself then is what is an object? This is a vast subject, which can be subject to many personal interpretations. For the moment we will be satisfied to say that an object represents a type of variable containing variables (which will be called later attributes) as well as functions (which will be called by the following methods). In other words, we can see the objects at the moment as being particular data structures.

To be honest, you have to admit that python is an object language because all the variables you handle are actually objects. Indeed all the variables, integers, floats, list, functions, … that you have handled until now being without your knowing it objects.

8.1.2. What are the advantages of handling objects?

We must be aware that many programming languages ​​do not have the concept of object and yet they are widely used. So what is the point in using objects? For the moment we can mainly establish the fact that in many applications we need to manipulate composite data.

For example for the management of a todo-list we need to manage tasks. We can define these tasks as a set of data: identifier, description, status, percentage of progress, … A solution to implement this data structures may be to use dictionaries

[1]:
task={"id": 0, "description": "Finir le cours python à temps", "status": "en cours", "stats": 50}

This is an approach that works for a simple program but quickly becomes a bit cumbersome for more complex applications.

L’objet de cette section est donc de voir comment nous pouvons mettre en place un objet de type Task qui nous permettra de manipuler plus facilement les données associées à chaque tâchee.

8.2. Define your objects in python

8.2.1. The class keyword

To define your own objects in Python you have to define what we call a class. Throughout what follows, the terms class and object will be used without distinction.

The syntax for defining our Task object is as follows:

[2]:
class Task:
    def __init__(self, tid, desc):
        self._tid = tid
        self._description = desc
        self._status = "todo"
        self._stats = 0

Once the class is defined, it can be used to define objects. We then speak of an instance of Task. For example, to create a variable t1 an instance ofTask, we proceed as follows:

[3]:
t1 = Task(0, "Finir le cours python à temps")
print( t1 )
<__main__.Task object at 0x7f2c501402d0>

The first thing noticed is that we define a method __init__. This method is a special method (recognizable as double underscore), it defines the way in which we initialize the class when we create an instance of the latter. It is important to note that the __init__ (self, tid, desc) method is defined with 3 input arguments while at the instantiation of Task we only provide twoarguments 0 and " Finish the python course on time ". This is normal. The self argument is in fact a particular argument in Python which represents the instance of the object that we are handling, heret1.

You’ve probably noticed that in the __init__ method we use this self argument for the expressionself. _tid = tid. This line means that we assign to the name attribute of theTask instance being created, the value contained in the tid variable given as argument. In the same way, we define an attribute _ description in which we store the value ofdesc as well as two attributes _status andstats each with a fixed default value.

We will see later that this self argument is always present in the definition of the methods of a class and not only in the__init__method.

8.2.2. Attributes and methods

Previously we defined a very simple Task class with four attributes. And we created a t1 instance of this class. An attribute can therefore be considered as a variable attached to a class instance. For example, if you want to access the value of the _description attribute of thet1 instance, just proceed as follows:

[4]:
t1._description
[4]:
'Finir le cours python à temps'

The question you may be asking yourself is whether it is then possible to define methods that manipulate the attributes of an instance. The answer is yes, objects are made for that!

For example let’s see how to implement the special method __repr__. What is this method for? An example

[5]:
print(t1)
<__main__.Task object at 0x7f2c501402d0>

It’s still very ugly isn’t it? And that doesn’t help us too much. The __repr__ method is the special method (recognizable by the double underscore) which is responsible for returning a character string when we want to display an object. For example a possible implementation is the following:

[6]:
class Task:
    def __init__(self, tid, desc):
        self._tid = tid
        self._description = desc
        self._status = "todo" ## todo, in progress or finished
        self._stats = 0

    def __repr__(self):
        return f"{self._tid} [{self._stats} %] : {self._description}"
[7]:
t2 = Task(0, "Finir le cours Python à temps")
print(t2)
0 [0 %] : Finir le cours Python à temps

You can then notice that we manipulate the values ​​of the t2 instance in the__repr__method using theself attribute. Another way of looking at it is to say that Task represents a namespace in which we store all the” functions “operating on variables of type Task. For example we can completely write:

[8]:
Task.__repr__(t2)
[8]:
'0 [0 %] : Finir le cours Python à temps'

Obviously this syntax is very rarely used and is of no particular interest, except to illustrate here the fact that self represents the instance of the class.

For the moment we have only seen special methods, that is to say methods predefined in the Python language and having a paraticular meaning. But we can obviously define methods according to our desires and our moods. For example, let’s implement a method in our task object that declares a task completed.

[9]:
class Task:
    def __init__(self, tid, desc):
        self._tid = tid
        self._description = desc
        self._status = "todo" ## todo, in progress or finished
        self._stats = 0

    def __repr__(self):
        return f"{self._tid} [{self._stats} %] : {self._description}"

    def setFinished(self):
        self._status = "finished"
        self._stats = 100
[10]:
t1 = Task(0, "Finir le cours Python à temps")
print(t1)
t1.setFinished()
print(t1)
0 [0 %] : Finir le cours Python à temps
0 [100 %] : Finir le cours Python à temps

Of course, everything you know about Python functions is applicable and usable in defining the methods of your classes. For example if you want to implement an update method which increments the default_stats attribute by 10 points.

[11]:
class Task:
    def __init__(self, tid, desc):
        self._tid = tid
        self._description = desc
        self._status = "todo" ## todo, in progress or finished
        self._stats = 0

    def __repr__(self):
        return f"{self._tid} [{self._stats} %] : {self._description}"

    def setFinished(self):
        self._status = "finished"
        self._stats = 100

    def update(self, incr=10):
        self._stats += incr
        if self._stats >= 100:
            print("stats >= 100 we close the task")
            self.setFinished()
[12]:
t1 = Task(0, "Finir le cours Python à temps")
t1.update() ; print(t1)
t1.update(85) ## j'ai beaucoup travailler aujourd'hui
print(t1)
t1.update()
print(t1)
0 [10 %] : Finir le cours Python à temps
0 [95 %] : Finir le cours Python à temps
stats >= 100 we close the task
0 [100 %] : Finir le cours Python à temps

8.3. Integrate your objects into the Python ecosystem

8.3.1. But we are already in Python !!

What do we mean by “integrating your objects in Python”? This is already the case since we wrote our Python classes and can instantiate them in a classic Python program. This is true, but it does not mean that your object is fully integrated into the language. Or in other words, how can we make our objects benefit from all the advantages and all the syntactic subtleties of the Python language?

For example things like:

[13]:
ma_liste = [10, 23, 25, 19, 47]
if ma_liste and ma_liste[0]>0:
    for x in ma_liste:
        print( x )
10
23
25
19
47

We see in the previous example that we first use a test to check if the list is not empty. Then we use the iterator aspect of the list to iterate through all the elements of the latter.

In order to have the possibility of using similar “Pythonesque” syntaxes with our own objects, we will have to go through the implementation of a certain number of special methods. To illustrate the implementation and operation of these special methods, we will continue to develop our Task class as well as aTaskManager class which will be responsible for managing all the tasks.

8.3.2. Opérations booléennes, comparaisons

Tout d’abord nous allons implémenter la méthode __bool__. Cette dernière est la méthode implicitement appelée par Python lorsque l’on est dans une expression de la forme

### Soit task_instance une instance de la classe Task

if task_instance:
    ### On souhaite arriver dans le bloc if
    ### uniquement si la tache n'est pas terminée

To have the desired behavior, we just need to implement the __bool__ method as follows:

[14]:
class Task:
    def __init__(self, tid, desc):
        self._tid = tid
        self._description = desc
        self._status = "todo" ## todo, in progress or finished
        self._stats = 0

    def __repr__(self):
        return f"{self._tid} [{self._stats} %] : {self._description}"

    def setFinished(self):
        self._status = "finished"
        self._stats = 100

    def __bool__(self):
        if self._status == "finished":
            return False
        return True
[15]:
t1 = Task(0, "Finir le cours Python à temps")

if t1:
    print("t1 is not finished => we enter in the if block")

t1.setFinished()

if t1:
    print("t1 is not finished => we enter in the if block")
else:
    print("t1 is finished")
t1 is not finished => we enter in the if block
t1 is finished
[16]:
if not t1:
    print("t1 is finished")
t1 is finished

Next we will add an attribute to our tasks, the priority. This will then allow us to classify our tasks. For this we consider three priority levels: (i) " current "; (ii) " priority "; (iii) " urgent ". And what we want then is to be able to compare two instances of Task and thus be able to classify a set of tasks according to their priority level. For that we will have to define the operators <, >,<= et > =allowing the comparison of twoTask. The definition of its operators requires the implementation of the special methods __lt__, __gt__, __le__, <__> ge <__ >.

Voici ci-dessous une implémentation possible

[17]:
class Task:

    priority_level = {'courante': 0, "prioritaire": 1, "urgente": 2}

    def __init__(self, tid, desc, priority="courante"):
        self._tid = tid
        self._description = desc
        self._status = "todo"
        self._stats = 0
        self._priority = priority if priority in self.priority_level.keys() else None
        if self._priority is None:
            raise Exception(f"Not available priority level {priority}")

    def __repr__(self):
        return f"{self._tid} [{self._stats} %] : {self._description}"

    def __lt__(self, other):
        if self.priority_level[self._priority] < self.priority_level[other._priority]:
            return True
        else:
            return False

    def __gt__(self, other):
        return other < self

    def __le__(self, other):
        if self.priority_level[self._priority] <= self.priority_level[other._priority]:
            return True
        else:
            return False

    def __ge__(self, other):
        return other <= self
[18]:
t1 = Task(0, "Finir le cours Python à temps")
t2 = Task(0, "Finir le cours Python à temps", "urgente")
[19]:
t1 > t2
[19]:
False
[20]:
t1 < t2
[20]:
True
[21]:
t1 >= t2
[21]:
False
[22]:
t1 <= t2
[22]:
True

We can thus observe that with only the implementation of 4 (relatively simple) methods we obtain the desired behavior and we can thus, if we wish, sort a set of tasks.

8.3.3. for loop or the magic of iterators

Now we’ll see how we can make our objects look like iterables, for the for loop for example. For that we will have to implement the special method __iter__ as well as the special method __next__.

In order to work on something concrete, or at least not too abstract, we are going to implement a TaskManager class responsible for managing a set of tasks. And it is in this TaskManager class that we will implement the__iter__and__next__methods in order to be able to loop (in a Pythonesque way) on the set ofTask from the TaskManager.

[23]:
class TaskManager:
    def __init__(self, name="My Task Group"):
        self._name = name
        self._tasks = []
        self._cid = None

    def createTask(self, **kwargs):
        self._tasks.append( Task(**kwargs) )

    def addTask(self, task):
        self._tasks.append( task )

    def __iter__(self):
        self._cid = 0
        return self

    def __next__(self):
        if self._cid >= len(self._tasks):
            raise StopIteration
        else:
            ret = self._tasks[self._cid]
            self._cid += 1
            return ret
[24]:
manager = TaskManager()
manager.createTask(tid=0, desc="Finir le cours Python à temps", priority="urgente")
manager.createTask(tid=1, desc="Trouver des idées pour les projets du cours")
[25]:
for t in manager:
    print(t)
0 [0 %] : Finir le cours Python à temps
1 [0 %] : Trouver des idées pour les projets du cours

Some explanations all the same!

The __iter__ method returns in this case the self attribute, i.e. the current instance of the class. This therefore means that it is directly on this current instance that we will seek to iterate. You notice in passing that we initialize at this time the attribute _cid to0.

Secondly, the _ _next_ _ method takes care of checking the current index of _ cid and whether it is greater than or equal to number of tasks then we throw an exception of type StopIteration. This exception is of course caught by the for loop, thus triggering its stop. If the attribute _cid is not greater than or equal to the number of tasks, we get the task with index_ cid in the list _tasks, we increment_ cid of 1 and we send the task back.

Some of you, who would have fully understood how Python works, you might want to tell me that there is a much easier way to achieve the same results. To which I will answer that you are absolutely right. The previous example sets up a solution that is not the simplest just for the purpose of showing you the complete operation of iterators. The simplest solution consists in returning in the method __iter__ the iterator associated with the list _tasks rather than theTaskManager. So it is not even necessary to implement the _ _next_ _ method.

[26]:
class TaskManager:
    def __init__(self, name="My Task Group"):
        self._name = name
        self._tasks = []

    def createTask(self, **kwargs):
        self._tasks.append( Task(**kwargs) )

    def addTask(self, task):
        self._tasks.append( task )

    def __iter__(self):
        return self._tasks.__iter__()
[27]:
manager = TaskManager()
manager.createTask(tid=0, desc="Finir le cours Python à temps", priority="urgente")
manager.createTask(tid=1, desc="Trouver des idées pour les projets du cours")
[28]:
for t in manager:
    print(t)
0 [0 %] : Finir le cours Python à temps
1 [0 %] : Trouver des idées pour les projets du cours

8.3.4. Operators +, -,*,/

To finish our overview (not exhaustive) of the special Python methods we will see how to define the +, -,*and/operators. The goal is always, I remind you, to have objects that are as integrated as possible into the Python ecosystem. In other words, we want to be able to use our objects in the most Pythonic way possible.

For example, let’s define the operator + between two Task such thatt1 + t2 returns a new instance of Task having for description the concatenation of the descriptions oft1 and t2 and for priority the the higher of the two.

[29]:
class Task:
    priority_level = {'courante': 0, "prioritaire": 1, "urgente": 2}

    def __init__(self, tid, desc, priority="courante"):
        self._tid = tid
        self._description = desc
        self._status = "todo"
        self._stats = 0
        self._priority = priority if priority in self.priority_level.keys() else None
        if self._priority is None:
            raise Exception(f"Not available priority level {priority}")

    def __repr__(self):
        return f"{self._tid} [{self._stats} %] : {self._description}"

    def __lt__(self, other):
        if self.priority_level[self._priority] < self.priority_level[other._priority]:
            return True
        else:
            return False

    def __gt__(self, other):
        return other < self

    def __le__(self, other):
        if self.priority_level[self._priority] <= self.priority_level[other._priority]:
            return True
        else:
            return False

    def __ge__(self, other):
        return other <= self

    def __add__(self, other):
        desc = self._description + " ; " + other._description
        level= self._priority if self._priority >= other._priority else other._priority
        tid = self._tid + other._tid
        return Task(tid, desc, level)
[30]:
t1 = Task(0, "Finir le cours Python à temps", "urgente")
t2 = Task(0, "Trouver des idées de projet")

t3 = t1+t2
print(t3)
print(t3._priority)
0 [0 %] : Finir le cours Python à temps ; Trouver des idées de projet
urgente

Thus by implementing the __add__ method we were able to define exactly the behavior we wanted for the + operator between two tasks.

In the same way, it would be possible to implement the methods, __sub__, __div__, __mul__ to associate a behavior with the operators +,/,-.

8.3.5. Remarks

We have listed here only a small part of the set of special methods that can be implemented in a Python class. There are many other special methods. For more details on this subject, do not hesitate to consult the official documentation.

8.4. Heritage

8.4.1. Mother class, daughter class, a whole family

To finish this first overview of object programming, we will see the concept of inheritance. Inheritance is a concept of object oriented programming introducing the possibility of defining a class B as a daughter of a class A. How interesting will you tell me! By declaring B as a daughter of A, B has access to all the methods and attributes defined in A. And that’s not all !!! Indeed in addition to having access to all the methods and all the attributes of A, class B will be able to redefine certain methods, in addition to new ones.

Another way of seeing heritage and talking about specialization. The idea is that the Mother class is a generic class while the daughter class is a more specialized class.

8.4.2. An example to understand

For example, let’s take our todo list program and in particular the Task object. We could very well make a derived class dedicated to `` urgent ‘’ tasks. This would give for example:

[31]:
class UrgenteTask(Task):
    def __init__(self, tid, desc):
        super(UrgenteTask, self).__init__(tid, desc, priority="urgente")
[32]:
t = UrgenteTask(1, "Finir le cours Python à temps")
print(t)

print(t._priority)
1 [0 %] : Finir le cours Python à temps
urgente

We can see that the print does indeed display the UrgenteTask instance using the formatting defined in the__repr__method of the parent classTask. And we also see that the constructor of the UrgenteTask class no longer takes any optionalpriority argument. The latter is passed directly to the constructor of the parent class with the value " urgent ".

The interest is quite limited, you will tell me. And I can only agree with you except that … well that’s not all. If for example now we want our urgent tasks to be displayed in red grs and underlined, well it’s simple, just redefine in UrgenteTask the__repr__method. For example :

[33]:
class UrgenteTask(Task):
    def __init__(self, tid, desc):
        super(UrgenteTask, self).__init__(tid, desc, priority="urgente")

    def __repr__(self):
        return f"\033[1;4;91m {self._tid} [{self._stats} %] : {self._description} \033[0m"

t = UrgenteTask(1, "Finir le cours Python à temps")
print(t)

 1 [0 %] : Finir le cours Python à temps 

8.4.3. In fact all your objects are objects

I must confess something to you, I have hidden from you from the start the fact that you are inheriting without even knowing it !!! And yes. Because from the moment you define a class, even if you do not derive it from anything, well in fact Python makes it naturally derived from the object class. In Python, there is the object class which, as the help message for this class says, is the base of the database.

[34]:
help(object)
Help on class object in module builtins:

class object
 |  The most base type

In order to verify that I am not telling you anything, let’s do an example.

[35]:
class EmptyClass:
    def __init__(self):
        pass
[36]:
instance = EmptyClass()
help(instance)
Help on EmptyClass in module __main__ object:

class EmptyClass(builtins.object)
 |  Methods defined here:
 |
 |  __init__(self)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)

An even simpler way to verify that our EmptyClass class is indeed a child class ofobject is to ask Python explicitly using the issubclass function.

[37]:
issubclass(EmptyClass, object)
[37]:
True

8.4.4. Public or private attributes and methods

If you have already developed in another programming language using the OOP paradigm you are probably thinking that our classes are missing a little thing. I obviously want notions of public and private attributes and methods. For those of you who, on the contrary, have never done an OOP, you may be saying to yourself, what is this one still telling us?

As a reminder in OOP, an attribute or a method is said to be public when it can be called from outside the class. While on the contrary a private attribute / method can only be used internally of the class. What is the point of “hiding” things by keeping them private? The main interest is to strictly partition what is accessible to the user from what is reserved for the developer of the class. This helps clarify the API (Application Programming Interface) of a program.

And so to come back to object programming in Python. It turns out that in Python this mechanism of private public is not very present but it is not for all that absent. In fact, to define an attribute or a method as private, it suffices to precede the name of the attribute or the name of the method by a double underscore __.

[38]:
class WithoutInterest:
    def __init__(self, x):
        self._x = x
        self.__i_am_protected = True

    def __iAmProtected(self, x):
        self.__i_am_protected = x
[39]:
instance = WithoutInterest( 10 )
print( instance._x )
10
[40]:
try:
    instance.__i_am_protected
except Exception as e:
    print(e)
'WithoutInterest' object has no attribute '__i_am_protected'
[41]:
try:
    instance.__iAmProtected(100)
except Exception as e:
    print(e)
'WithoutInterest' object has no attribute '__iAmProtected'

8.5. Quelques concepts avancés

8.5.1. L’attribut __dict__

*TODO*

[ ]:

[ ]:

[ ]:

8.5.2. Les décorateurs

*TODO*

[ ]: