© Your Copyright
Les logiciels produits par l’ingénierie informatique sont par essence complexes. Ils doivent composer avec les contraintes techniques imposées par le matériel, manipuler des données volumineuses ou hétérogènes, mettre en œuvre un ensemble de fonctionnalités qui évoluent au cours du temps, interagir avec des utilisateurs humains ou avec tout un écosystème d’autres logiciels eux mêmes complexes... Le plus souvent ces applications sont développées pas plusieurs personnes. Elles sont étendues, corrigées, versionnées!
La taille des codes et notre capacité limité à prendre en compte leur complexité a conduit à les découper en unités plus petites qui présentent une apparente simplicité : les classes. Une classe est responsable d’une partie du logiciel (single responsability principle) et collabore avec les autres classes. Les mécanismes d’encapsulation permettent de “cacher” la mécanique interne d’une classe pour ne présenter via sont interface que les mécanismes utiles à son utilisation.
Le contrôle de l’accès aux variables et fonctions membres permet de choisir ce qui est visible ou non depuis l’extérieur d’un objet :
- visibilité publique : Si on possède une référence vers un objet, on peut accéder à toutes ses propriétés et services publics définis par sa classe. En python, c’est la visibilité par défaut si on ne précise rien.
- visibilité privée : Les propriétés et services privés ne sont visibles qu’à l’intérieur de l’espace de nommage de la classe. Seul le code de la classe peut les manipuler. En python, pour déclarer un élément privé, on fait précéder son nom de 2 tirets.
Prenons le code d’une classe qui possède un attribut publique:
class MyClass: def __init__(self): self.public_prop = 42 obj=MyClass() #code valid x=obj.public_prop
Nous pouvons rendre cet attribut privé en ajoutant “__
” devant sont nom. L’accès à l’attribut depuis un code non membre de la classe provoquera une erreur.
class MyClass: def __init__(self): self.__private_prop = 42 obj=MyClass() #ERROR!! x=obj.__private_prop
On pourra toujours accéder à l’attribut depuis une méthode de sa classe.
class MyClass: def __init__(self): self.__private_prop = 42 def public_method(self): #can acces to private property x = self.__private_prop return x obj=MyClass() #code valid x=obj.public_method()
On peut également rendre une méthode privée. Elle ne sera plus accessible depuis l’extérieur de la classe.
class MyClass: def __init__(self): self.__private_prop = 42 def __private_method(self): #can acces to private property x = self.__private_prop return x obj=MyClass() #ERROR!! x=obj.__private_method()
Mais on pourra toujours y accéder depuis une autre méthode.
class MyClass: def __init__(self): self.__private_prop = 42 def __private_method(self): return self.__private_prop def public_method(self): #can acces to private methode x= self.__private_method() return x obj=MyClass() #code valid x=obj.public_method()python : code_run80.pySorties
Soit un disque qui peut être posé sur une surface rectangulaire. On souhaite éviter que le disque ne sorte de cette surface. Si non voulons coder ces informations :
class Disc:
def __init__(self,x,y,r,x_max,y_max):
self.x=x
self.y=y
self.r=r
self.x_max=x_max
self.y_max=y_max
d = Disc(50.,50.,10.,800.,600.)
#wrong value!
d.x=5.
d.y=60.
x > r/2
x < xmax - r/2
y > r/2
y < ymax - r/2
L’ensemble des ces contraintes forme ce qu’on appel un invariant. On souhaite s’assurer que cet invariant soit respecté au cours de l’exécution du programme.
Malheureusement l’instruction d.x=5.
a pour effet de briser cet invariant.
En rendant les attributs privés et en n’y permettant l’accès qu’à travers des méthodes, on peut s’assurer du maintient de l’invariant.
class Disc:
def __init__(self,x,y,r,x_max,y_max):
self.__x_max=x_max
self.__y_max=y_max
self.__r=r
x,y=self.__adjust_position(x,y)
self.__x=x
self.__y=y
def __adjust_position(self,x,y):
if x < self.__r/2. :
x = self.__r/2.
elif x > (self.__x_max - self.__r / 2.) :
x = self.__x_max - self.__r /2.
if y < self.__r/2. :
y = self.__r/2.
elif y > (self.__y_max - self.__r / 2.) :
y = self.__y_max - self.__r /2.
return x,y
def move(self,x,y):
x,y=self.__adjust_position(x,y)
self.__x = x
self.__y = y
d = Disc(50.,50.,10.,800.,600.)
d.move(5.,60.)
Pour réduire les risques et simplifier l’utilisation d’une classe, nous nous fixons la règle de tout rendre privé par défaut et de ne rendre visible que ce qui doit nécessairement l’être.
Pour permettre le maintient des invariants qui portent sur les attributs, nous utiliserons des accesseurs et des mutateurs.
class MyClass:
def __init__(self,value):
self.__private_value=value
self.__public_value=value
#accessor
def get_public_value(self):
return self.__public_value
#mutator
def set_public_value(self,value):
self.__public_value=value
Ainsi pour rendre un attribut public, nous le doterons d’accesseur et mutateur publics, mais nous le coderons quand même comme un attribut privé.
Par défaut tous les attributs sont privés. Seulement lorsque c’est nécessaire, nous les rendons publics.
Même si tous les attributs publics ne sont pas soumis à des invariants, nous choisissons tout de même de systématiser l’utilisation d’accesseur/mutateur. Ce choix est discutable car il alourdit l’écriture du code et rajoute des indirections pour accéder aux attributs, ce qui ralentit l’exécution du code.
Nous choisissons tout de même ce compromis car :
- la perte de performance (très minime) n’est pas gênante dans la mesure où python n’est pas un langage qu’on utilise lorsqu’on recherche à optimiser les temps d’exécution.
- le code produit sera homogène. L’utilisateur de la classe n’aura pas à distinguer les attributs soumis à invariant des autres. Pas besoin de se demander si on accède comme ça :
obj.x
ou comme ça :obj.get_x()
.- l’écriture de messages par les accesseurs/mutateurs en phase de débogage est très intéressante
- on n’a pas toujours conscience à priori des invariants à respecter quand on commence à coder. Il peut être coûteux de changer l’interface d’une classe en cours de développement pour passer d’un accès direct à un accès par accesseur (le descripteur
property
, ci-après, annule cet argument en python).- vous êtes à l’école, et il est intéressant d’apprendre à respecter une règle de codage! pourquoi pas celle-ci?
porperty
.class MyClass:
def __init__(self,value):
self.__public_value=value
#accessor
def get_public_value(self):
print("access value")
return self.__public_value
#mutator
def set_public_value(self,value):
print("set vallue")
self.__public_value=value
public_value = property(get_public_value,set_public_value)
mc=MyClass(12)
mc.public_value = 12
print(mc.public_value)
Grâce à property
, lorsqu’on manipule l’attribut public_value
on appelle automatiquement les accesseurs et mutateurs de __public_value
. Malheureusement l’ENIBook ne supporte pas cette fonctionnalité.
Cette autre écriture est possible :
class MyClass: def __init__(self,value): self.__public_value=value @property def public_value(self): print("access vallue") return self.value @public_value.setter def public_value(self,value): print("set vallue") self.__value=value mc=MyClass(12) mc.public_value = 12 print(mc.public_value)
L’utilisation de property
a le grand intérêt de pouvoir introduire un invariant sans modifier le code client. En effet, l’instruction mc.public_value = 12
peut soit manipuler un attribut public soit appeler sans le savoir un accesseur.
Un attribut est une donnée définie par son type (entier, réel, chaîne de caractères...) et par une multiplicité.
x
est 1 entier”position
se défini par deux réels”Lorsque la multiplicité vaut plus de 1, il convient d’utiliser un conteneur.
Pour ce faire en python, deux conteneurs semblent appropriés : list
et tupple
.
Le tupple
étant non mutable, on utilisera plutôt ce conteneur pour les attributs avec multiplicité qui ne sont pas modifiés.
On utilisera plutöt une liste si la valeur et la multiplicité de l’attribut sont amenés à évoluer au cours de la vie de l’objet.
Ensuite, si on souhaite accéder à la valeur des attributs, il faut ajouter un accesseur :
Pour respecter l’encapsulation, afin de garantir la possibilité de maintenir un invariant sur un attribut avec multiplicité, l’accesseur doit renvoyer une copie du conteneur de l’attribut et non le conteneur lui même.
Ici, nous avons choisit de recopier la liste dans un tuple
:
return tuple(self.__students)
Si nous avions renvoyé directement la liste des noms d’étudiants :
return self.__students
Il aurait été possible de modifier n’importe comment la liste des noms d’étudiants du groupe depuis l’extérieur de l’objet. Le maintient de l’invariant : “aucun doublon dans la liste”, deviendrait impossible à vérifier.
On aurait également pu renvoyer une copie de la liste avec l’instruction :
return self.__students[:]
La définition d’une classe fournit un espace de nommage dans lequel il est possible de déclarer des fonctions membres, des
fonctions qui prennent comme premier paramètre self
. Il est également possible de déclarer dans cet espace des variables de classe :
class MyClass:
public_class_var =12
__private_class_var =12
def doit(self):
print MyClass.__private_class_var
print MyClass.public_class_var
#print MyClass.__private_class_var : not allowed here
Ces variables de classe peuvent être de visibilité privée comme les méthodes en les faisant précéder de __
.
Notons également qu’il n’est pas nécessaire d’instancier un objet pour manipuler une variable de classe.
Il est également possible de déclarer dans la classe une fonction qui ne sera pas une méthode d’instance.
C’est à dire une fonction qui ne prend pas comme premier paramètre self
.
Par exemple, nous pouvons écrire un accesseur pour nos variables de classe.
class ColoredPoint :
__cpt_red_instances = 0
__color_name_table={'red':0,'green':1,'blue':2,'rouge':0,'vert':1,'bleu':2,0:0,1:1,2:2}
def __init__(self,x,y,color):
self.__x,self.__y = x,y
self.__color = ColoredPoint.__color_name_table[color]
if self.__color == 0:
ColoredPoint.__cpt_red_instances += 1
print ColoredPoint.__cpt_red_instances, ' red created'
def get_cpt_red_instances():
return ColoredPoint.__cpt_red_instances
cp1 = ColoredPoint(0,0,"rouge")
cp2 = ColoredPoint(0,0,"blue")
cp3 = ColoredPoint(0,0,"red")
print ColoredPoint.get_cpt_red_instances()
Cependant, il faut avoir conscience que l’écriture du code : cp1.get_cpt_red_instances()
provoquera une erreur, puisqu’il n’y a pas de paramètre self``disponible dans la fonction ``get_cpt_red_instances
.
Pour permettre l’appel d’une telle fonction depuis la référence d’un objet, python a prévu une solution.
Il s’agit d’ajouter le décorateur : @staticmethod
devant la fonction.
class ColoredPoint :
...
@staticmethod
def get_cpt_red_instances():
return ColoredPoint.__cpt_red_instances
cp1 = ColoredPoint(0,0,"rouge")
print cp1.get_cpt_red_instances()
On parle alors de méthode statique.
Attention l’ENIBook ne sait pas gérer les décorateurs.
Pour information, il existe un autre décorateur classmethod
.
Il permet de remplacer la référence sur l’objet par une référence sur la classe elle même :
class ColoredPoint :
...
@staticmethod
def get_cpt_red_instances():
return ColoredPoint.__cpt_red_instances
@classmethod
def print_cpt_red_instances(cls):
print cls.get_cpt_red_instances()
cp1 = ColoredPoint(0,0,"rouge")
print cp1.get_cpt_red_instances()
Ainsi, nous avons remplacé self``par ``cls
car ce premier paramètre prendra pour valeur une référence vers la classe au lieu d’une référence vers l’instance.
On parle alors de méthodes de classes plutôt que de méthodes d’instance.
Bien que le décorateur classmethod
présentera sont réel intérêt lorsque nous aurons abordé l’héritage nous pouvons donner un exemple d’utilisation.
Si nous voulons créer une factory nommée red()
pour créer uniquement des points rouges.
class ColoredPoint :
__cpt_red_instances = 0
__color_name_table={'red':0,'green':1,'blue':2,'rouge':0,'vert':1,'bleu':2,0:0,1:1,2:2}
def __init__(self,x,y,color):
self.__x,self.__y = x,y
self.__color = ColoredPoint.__color_name_table[color]
if self.__color == 0:
ColoredPoint.__cpt_red_instances += 1
print ColoredPoint.__cpt_red_instances, ' red created'
@staticmethod
def get_cpt_red_instances():
return ColoredPoint.__cpt_red_instances
@classmethod
def print_cpt_red_instances(cls):
print cls.get_cpt_red_instances()
@classmethod
def red(cls,x,y):
return cls(x,y,'red')
#using factory to instanciate
red_point = ColoredPoint.red(10.,10.)
red_point.print_cpt_red_instances()
La Python software fundation fournit un certain nombre de préconisations pour améliorer les codes écrits en python : Python Enhancement Proposals (PEP). La PEP8 est la plus connue. Elle propose un style de programmation. Vous êtes invités à consulter ce document. Autant que possible, ce cours essaie de respecter la PEP8.
La PEP256, quant à elle, propose de commenter le code en utilisant les docstrings. Pour nous, il s’agira de commencer les modules, classes et méthodes par une chaîne de caractères entre triple guillemets : """ comment """
. Ce commentaire sera accessible en consultant la propriété .__doc__
pour chaque élément commenté.
Python est un langage très souple qui permet de programmer de plein de manières différentes. De plus, il n’y a pas de fichier entête pour connaître le contenu des classes. Pour y voir clair il est essentiel de produire un code structuré, documenté et homogène. Pour atteindre cet objectif, ce cours impose quelques les règles d’écriture de code :
- Nommage :
- Le nom des classes est en
CamelCase
- Le nom des modules, des variables, méthodes et attributs est en minuscule séparé par de
_
- Le nom des constantes est en majuscule
- Les noms de variables et les commentaires sont en anglais
- S’il n’y a qu’une seule classe dans un module, le module porte le nom de la classe.
- Les modules et les classes sont commentées comme dans l’exemple proposé.
- Les imports de modules se font dans l’ordre suivant :
- Modules internes à python
- Modules externes, logiciels tiers
- Modules internes à l’application
- A l’intérieur de la classe on trouvera dans l’ordre :
- Les variables statiques
- L’initialisateur
- Le finaliseur
- Les accesseurs et mutateurs
- Les méthodes publiques
- Les méthodes privées
- Enfin après l’instruction suivante, on place les tests unitaires du module :
if __name__ == '__main__':
Exemple de fichier : my_class.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Provides MyClass class
illustrates the way python script should be structured
"""
# Built-In Imports
import os
import sys
# Third part Libs
import pygame
# Own modules
import my_other_class
#we may add properties to module
__author__ = "Gireg Desmeulles"
__copyright__ = "Copyright 2019, ENIB"
__credits__ = ["Gireg Desmeulles", "Elisabetta Brevacqua"]
__license__ = "GPL"
__version__ = "1.0.1"
__maintainer__ = "Gireg Desmeulles"
__email__ = "desmeulles@enib.fr"
__status__ = "Production"
MY_CONSTANT = 42
class MyClass(list):
"""here comment the MyClass purpose"""
#----------class attributs----------
__MY_CLASS_CONSTANT = 42
__my_class_attribute = "value"
#----------special methods----------
def __init__(self, first_parameter=0.0):
self.__public_attr = first_parameter
self.__private_attr = True
def __del__(self):
print "MyClass instance destruction"
#----------public operations----------
def get_public_attr(self):
return self.__public__attr
def set_public_attr(self, value):
self.__public_attr = value
public_attr = property(fget=get_public_attr, fset=set_public_attr)
def public_method(self):
""" doctring comment for public_method"""
pass
@staticmethod
def my_class_constant():
return __MY_CLASS_CONSTANT
#----------private operations----------
def __private_method(self):
pass
if __name__ == '__main__': #code to execute if called from command-line
#unit testing
my_class = MyClass('toto')
print __doc__
print MyClass.__doc__
Ce chapitre nous donne quelques règles pour coder les classes :
- rendre toutes les propriété des classes privées par défaut et ne rendre public que ce qui doit nécessairement l’être
- pour rendre un attribut public, le doter d’accesseurs et mutateurs publics, mais conserver une visibilité privée pour cet attributs
- respecter les règles de codage, de commentaires et de structuration du code.
Par ailleurs nous avons appris qu’il est possible de définir des méthodes et attributs statiques qui sont liés à la classe et non aux objets.
1- Définir une classe
Vehicle
avec les attributs privés suivants :type
,brand
,color
etprice
2- Créer un constructeur qui accepte les 4 paramètres
3- Créer les accesseurs
4- Créer une méthode
show
permettant d’afficher les caractéristiques d’un véhicule5- Instancier deux véhicules de votre choix et afficher leurs caractéristiques
1- Définir une classe
Point
avec les attributs privés suivants :a
(abscisse) eto
(ordonnée) qui représentent l’abscisse et l’ordonnée d’un point dans un plan cartésien.2- Créer un constructeur qui accepte les deux paramètres
3- Nous souhaitons modifier la valeur par défaut de ce point. Créer les accesseurs et les mutateurs convenables pour effectuer cette tâche.
4- Créer une méthode
move
pour déplacer ce point sur le plan.5- Créer une méthode
show
permettant d’afficher les valeurs de l’abscisse et de l’ordonnée après déplacement.6- Créer une instance
p
7- Afficher les valeurs de
a
eto
avant et après modifications.8- Afficher les valeurs du point
p
après déplacement
Définissez une classe Domino
qui permet d’instancier des objets simulant les pièces d’un jeu de dominos.
1. Le constructeur de cette classe initialisera les valeurs des points présents sur les deux faces A et B du domino (valeurs par défaut = 0).
2. Deux autres méthodes seront définies :
- une méthode
show_points()
qui affiche les points présents sur les deux faces- une méthode
value()
qui renvoie la somme des points présents sur les 2 faces.
Exemples d’utilisation de cette classe :
>>> d1 = Domino ( 2 , 6 )
>>> d2 = Domino ( 4 , 3 )
>>> d1.show_points( )
face A : 2 face B : 6
>>> d2.show_points( )
face A : 4 face B : 3
>>> print "total des points : " , d1.value() + d2.value()
15
>>> dominos_list = [ ]
>>> for i in range(7) :
dominos_list.append(Domino(6,i))
>>> for j in dominos_list :
j.show_points()
class Domino:
""" Class Domino represent domino pawn"""
def __init__(self,a=0,b=0):
self.__a=a
self.__b=b
def __del__(self):
print "destroy domino"
def show_points(self):
msg = "face A : "+ str(self.__a)+" face B : "+ str(self.__b)
print(msg)
def value(self):
return self.__a+self.__b
if __name__ == '__main__':
domino = Domino(1,4)
domino.show_points()
print domino.value()
Définissez une classe BankAccount
, qui permette d’instancier des objets tels que account_1
,``account_2``, etc...
1. L’initialisateur de cette classe déclarera deux attributs name
et balance
, avec les valeurs
par défaut 'Dupont'
et 1000
.
2. Trois autres méthodes seront définies :
- ``deposit(amount)` permettra d’ajouter une certaine somme au solde
withdraw(amount)
permettra de retirer une certaine somme du soldeshow()
permettra d’afficher le nom du titulaire et le solde de son compte.
Exemples d'utilisation de cette classe :
>>> account_1 = BankAccount('Duchmol ', 800)
>>> account_1.deposit(350)
>>> account_1.withdraw(200)
>>> account_1.show()
Le solde du compte bancaire de Duchmol est de 950 euros.
>>> account_2 = BankAccount()
>>> account_2.deposit(25)
>>> account_2.show()
Le solde du compte bancaire de Dupont est de 1025 euros.
class BankAccount :
""" define a bank account """
def __init__(self, name = "Dupont", balance = 1000. ):
self.__name = name
self.__balance = balance
def __del__(self):
print("destroy account ")
#public method
def deposit(self, amount):
self.__balance += amount
def whithdraw(self, amount):
self.__balance -= amount
def show(self):
print("Le solde du compte bancaire de", self.__name, "est de", self.__balance, "euros.")
if __name__ == '__main__':
account_1 = BankAccount('Duchmol', 800.)
account_1.deposit(350)
account_1.whithdraw(200)
account_1.show()
account_2 = BankAccount()
account_2.deposit(25)
account_2.show()
Définissez une ``Car``qui permette d’instancier des objets reproduisant le comportement de voitures automobiles.
1. Le constructeur de cette classe initialisera les attributs d’instance suivants, avec les valeurs
par défaut indiquées :
mark = 'Ford', color = 'red', driver = 'no_one', speed = 0.
Lorsque l’on instanciera un nouvel objet Car()
, on pourra choisir sa marque et sa couleur, mais pas sa vitesse, ni le nom de son conducteur.
2. Les méthodes suivantes seront définies :
*
choose_driver(name)
permettra de désigner (ou de changer) le nom du conducteur *speed_up(acceleration, duration)
permettra de faire varier la vitesse de la voiture.La variation de vitesse obtenue sera égale au produit : acceleration * duration. Par exemple, si la voiture accélère
1.3m.s^-2
pendant 20 secondes, son gain de vitesse doit être égal à26 m/s
. Des accélérations négatives seront acceptées (ce qui permettra de décélérer). La variation de vitesse ne sera pas autorisée si le conducteur est ‘no_one’.
- show() permettra de faire apparaître les propriétés présentes de la voiture, c’est-à dire sa marque, sa couleur, le nom de son conducteur, sa vitesse.
Exemples d'utilisation de cette classe :
>>> a1= Car('Peugeot', 'bleue')
>>> a2= Car(color='verte')
>>> a3= Car('Mercedes')
>>> a1.choose_driver('Romeo')
>>> a2.choose_driver('Juliette')
>>> a2.speed_up(1.8,12)
>>> a3.speed_up(1.9,11)
Cette voiture n'a pas de conducteur !
>>> a2.show()
Ford verte pilotee par Juliette, vitesse=21.6m/s.
>>> a3.show()
Mercedes rouge pilotee par personne, vitesse=0m/s.