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*
[ ]: