1ère Générale NSI

 

Term. Générale NSI

 

Terminale STI2D SIN

Bts Ccst

Technico-commercial 3.0

[[{"title":"Python - Programmation objet","posi":0},{"text":"
Nous avons évoqué dans la séquence précédente l'intérêt de définir et identifier certaines structures de données composées de plusieurs éléments, par  exemple avec la table de hachage pour laquelle nous avions besoin de mémoriser à la fois un tableau de paquets et un entier représentant le nombre
total d'éléments.

Revenons sur les différents moyens de représenter une telle structure composite en Python.

 -Le couple, cas particulier de n-uplet pour n valant 2, permet effectvement de regrouper deux éléments de types distincts mais n’autorise pas la modification des éléments. En l'occurrence on pourrait modifier
le contenu du tableau de paquets (cela ne change pas l'identité de ce tableau) mais pas modifier l’entier représentant la taille.

Le tableau permet de regrouper une séquence d’éléments et autorise la modification, mais nous avons invariablement recommandé de n’en faire qu’une utilisation homogène (un même type pour tous les éléments contenus). Le regroupement d’un tableau et d’un entier est incompatible avec cette discipline.

Les n-uplets nommés sont une approche tout à fait adaptée. Cependant, réaliser un n-uplet nommé à l’aide d’un dictionnaire n’est pas l'approche la plus idiomatique, ni en Python ni dans d’autres langages majeurs, chacun offrant pour cela des mécanismes
plus intégrés.

Le paradigme de la programmation objet, qui est intégré à Python et que nous allons présenter dans cette séquence, fournit une notion de classe, qui
permet à la fois de définir (et nommer) des structures de données composites, et de structurer le code d’un programme.

"},{"text":""}],[{"text":"
Une classe définit et nomme une structure de données qui vient s’ajouter aux structures de base du langage. La structure définie par une classe peut regrouper plusieurs composantes de natures variées. Chacune de ces composantes est appelée un attribut (on dit aussi un champ ou une propriété) et est dotée d’un nom.


","title":"Classes et attributs : structurer les données"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
******
Supposons que l’on souhaite manipuler des triplets d’entiers représentant des temps mesurés en heures, minutes et secondes. 
On appellera la structure correspondante Chrono. Les trois nombres pourront être appelés, dans l’ordre, heures, minutes et secondes, et nous pourrions nous figurer le temps «21 heures, 34 minutes et 55 secondes» comme un triplet nommé correspondant à la représentation graphique suivante.

 *********

Chrono
heures21
minutes34
secondes55

 Un triplet donné associe ainsi chacun des noms heures, minutes et secondes à un nombre entier. La structure Chrono elle-même peut alors être pensée comme le cas particulier des n-uplets nommés possédant exactement trois composantes nommées respectivement heures, minutes et secondes.

Python permet la définition de cette structure Chrono sous la forme d’une classe avec le code suivant.

class Chrono:
\"\"\"Une classe pour représenter un temps mesuré en
heures, minutes et secondes\"\"\"
def __init__(self, h, m, s):
self.heures = h
self.minutes = m
self.secondes = s

La définition d'une nouvelle classe est introduite par le mot-clé class, suivi du nom choisi pour la classe et du symbole : (deux-points). 
Le nom de la classe commence par une  lettre majuscule. Tout le reste de la définition est alors placé en retrait.
Nous avons dans cette exemple une chaîne de documentation décrivant la classe et la définition d’une fonction __init__ dont nous détaillerons la construction à la section suivante.
Notons pour l'instant simplement sa forme, à savoir qu’elle possède un premier paramètre appelé self, trois paramètres correspondant aux trois composantes
de notre triplet, ainsi que trois instructions de la forme self.a =  ... correspondant de même aux trois composantes (et en l’occurrence, affectant à
chaque attribut sa valeur).


","title":"Description d'une classe"},{"edit":""}],[{"text":"
Une fois une telle classe définie, un élément correspondant à la structure Chrono peut être construit avec une expression de la forme Chrono(h,m,s). On appelle un tel élément un objet où une instance de la classe Chrono.

On peut ainsi définir et affecter à la variable t un objet représentant notre temps « 21 heures, 34 minutes et 55 secondes» avec la ligne suivante.

>>> t = Chrono(21, 34, 55)

Tout s'écrit comme si le nom de la classe était également une fonction attendant trois paramètres et construisant l’élément correspondant.

Notez que, comme c'était le cas pour les tableaux, la variable t ne contient pas à strictement parler l’objet qui vient d’être construit, mais un pointeur vers le bloc de mémoire qui à été alloué à cet objet. La situation  correspond donc au schéma suivant.

t[.] +———> Chrono
         heures! 21
      minutes | 34
     secondes] 55

En outre, comme le suggère ce schéma, l’objet mémorise d’une manière ou l’autre son appartenance à la classe Chrono.


","title":"Création d'un objet"},{"edit":""}],[{"text":"

On peut accéder aux attributs d’un objet +de la classe Chrono avec la notation t.a où a désigne le nom de l’attribut visé. 

Les attributs, comme les cases d’un tableau, sont mutables en Python : on peut non seulement
consulter leur valeur mais aussi la modifier.

>>> t.secondes
55
>>> t.secondes = t.secondes + 1

>>> t.secondes

Notez que l’on a parlé dans le paragraphe précédent «d’attribut d’un objet». En effet, bien que les noms des attributs soient attachés à une classe, chaque objet possède pour ses attributs des valeurs qui lui sont propres. C’est pourquoi on parle parfois aussi d’attributs d'instance. Ainsi, chaque objet de la
classe Chrono possède bien trois attributs heures, minutes et secondes, dont les valeurs sont indépendantes des valeurs des attributs de même
nom des autres instances. Les deux définitions 
t = Chrono(21, 34, 55)
et u = Chrono(5, 8, 13) 
conduisent donc à la situation suivante.

t[e}——{Chrono          u[e}——{Chrono
heures  | 21                    heures 5
minutes  34                  minutes 8
secondes 55             secondes 13

Les valeurs des attributs d’un objet pouvant varier, on les comprend parfois comme décrivant l’état de cet objet. Avec ce point de vue, un changement des valeurs des attributs d’un objet correspond alors à l’évolution de cet objet. Une avancée de cinq secondes du chronomètre t mènerait ainsi à la situation suivante.

t{e———\\Chrono Uu|e$+———) Chrono
heures 21 heures 5
minutes 35 minutes 8
secondes 0 secondes 13

 
Erreurs. Il n’est évidemment pas possible d'obtenir la valeur d’un attribut inexistant.

>>> t.x
Traceback (most recent call last):
File \"<stdin>\", line 1, in <module>
AttributeError: Chrono’ object has no attribute ?x’

De façon plus surprenante, rien n'empêche en Python d’affecter par mégarde une valeur à un attribut n’appartenant pas à la classe de l’objet.

>>> t.x = 89
>>> (t.heures, t.minutes, t.secondes, t.x)
(21, 34, 55, 89)

Voir à ce propos l’encadré « spécificités des attributs en Python » ci-dessous.


","title":"Manipulation des attributs"},{"edit":""}],[{"text":"
La structure des objets en Python ne correspond pas tout à fait à la pratique usuelle de la programmation objet. Dans le paradigme objet habituel, une classe introduit un ensemble d’attributs, définissant la totalité des attributs que possédera chaque instance de cette classe. C’est cette situation que nous décrivons dans cette séquence et que nous utiliserons dans le reste de l’ouvrage.

En Python, cependant, les attributs ne sont pas réellement introduits au niveau de la classe. Plutôt, chaque affectation d'un attribut à un objet crée cet attribut pour cet objet particulier. Dans la terminologie
de Python, ces «attributs» s'appellent ainsi des variables d'instance.
Python permet donc techniquement que deux objets d’une même classe possèdent des attributs n’ayant aucun rapport les uns avec les autres.
Utilisée sans discernement, cette possibilité est évidemment une source inépuisable d'erreurs. Pour rester dans le cadre habituel de la programmation objet, on s'imposera donc la discipline suivante en Python : que chaque objet au moment de sa création se voie doté des attributs prévus pour sa classe, et qu'aucun autre attribut ne lui soit ajouté par la suite.


","title":"Spécificités des attributs en Python"},{"edit":""}],[{"text":"
Une classe peut également définir des attributs de
classe, dont la valeur est attachée à la classe elle-même.

class Chrono:
heure_max = 24


On peut consulter de tels attributs depuis n'importe quelle instance, ou depuis la classe elle-même.

>>> t = Chrono(21, 34, 55)
>>> (t.heure_max, Chrono.heure_max)
(24, 24)

On peut également modifier cet attribut en y accédant via la classe elle-même pour que la modification soit perceptible par toutes les instances présentes ou futures. 

>>> Chrono.heure max = 12
>>> t.heure max
12

En revanche, un tel attribut n’est pas destiné à être modifié depuis une instance (techniquement cela ne ferait que créer une variable d’instance du même nom, pour cette seule instance, qui serait donc décorrélée de
lattribut de classe).


","title":"Attributs de classe"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"title":"Retour sur les tables de hachage"},{"text":"

Le programme 10 utilisait un n-uplet nommé pour regrouper la table des paquets et la taille d’une table de hachage. On utilisera plus couramment à la place une classe munie de deux attributs, dont les valeurs initiales sont fixes et n’ont donc pas besoin d’être passées à la fonction __init__.

class Ensemble:
def __init__(self):
self.taille = 0
self.paquets = [[] for _ in range(32)]



La fonction de création d’une table de hachage vide se contente alors de créer une nouvelle instance de la classe Ensemble.

  def cree():
    return Ensemble()

On pourrait également adapter chacune des fonctions écrites pour les n-uplets nommés à cette nouvelle organisation de la structure de données. La fonction contient par exemple s’écrirait ainsi.

def contient(s, x):
p = x % len(s.paquets)
return x in s.paquets[p]

Cette manière d'écrire des fonctions manipulant des objets fonctionne parfaitement dans le cadre du programme de terminale. Elle n’est cependant
pas l’usage idiomatique de la programmation orientée objet, que nous allons présenter dans la section suivante.

"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"title":"Méthodes : manipuler les données"},{"text":"
Dans le paradigme de la programmation objet, la notion de classe est souvent associée à la notion d’encapsulation : un programme manipulant
un objet n’est pas censé accéder librement à la totalité de son contenu, une partie de ce contenu pouvant relever du «détail d’implémentation». La manipulation de l’objet passe donc de préférence par une interface constituée de fonctions dédiées, qui font partie de la définition de la classe et sont appelées les méthodes de cette classe.


"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"

Les méthodes d’une classe servent à manipuler les objets de cette classe.
Chaque appel de méthode peut recevoir des paramètres mais s'applique donc avant tout à un objet de la classe concernée. L'appel à une méthode
texte s'appliquant au chronomètre t et renvoyant une chaîne de caractères.

***********

>>> t.texte()
?21h 34m 555?

Cette notation pour l’appel de méthode utilise la même notation pointée que l'accès aux attributs de t, mais fait apparaître en plus une paire de parenthèses, comme pour l’appel d’une fonction sans paramètres.

Lorsqu'une méthode dépend d’autres paramètres que cet objet principal t, ces autres paramètres paraissent de la manière habituelle, entre les parenthèses et séparés par des virgules. L'appel à une méthode avance faisant avancer le chronomètre t d’un certain nombre de secondes passé en paramètre s'écrit donc comme suit.

>>> t.avance(5)
>>> t.texte()
?21h 35m 0s’

On a déjà rencontré cette notation, par exemple avec tab.append(e) pour ajouter l'élément e à la fin d’un tableau tab. Remarquez également dans cet
exemple qu’une méthode appliquée à l’objet t a la possibilité de modifier les attributs de cet objet.

Lors d’un appel i.m(e1, ..., en) à une méthode m, l'objet à est appelé le paramètre implicite et les paramètres e1 à en, les paramètres explicites.
Toutes les méthodes d’une classe attendent comme paramètre implicite un objet de cette classe. Les paramètres explicites, en revanche, de même que
l’éventuel résultat de la méthode, peuvent être des valeurs Python arbitraires : on y trouvera aussi bien des valeurs de base (nombres, chaînes de caractères, etc.) que des objets.

On peut ainsi imaginer dans notre classe Chrono une méthode egale s'appliquant à deux chronomètres (le paramètre implicite et un paramètre explicite) et testant l'égalité des temps représentés, et une méthode clone
s'appliquant à un chronomètre t et renvoyant un nouveau chronomètre initialisé au même temps que t.

>>> u = t.clone()
>>> t.egale(u)
True

>>> t.avance(3)
>>> t.egale(u)
False


","title":"Utilisation d'une méthode"},{"edit":"

Tester et écrire ici le résultat.

"}],[{"text":"

Comme nous venons de le voir, une méthode d’une classe peut être vue comme une fonction ordinaire, pouvant dépendre d’un nombre arbitraire de paramètres  à ceci près qu'elle dait nécessairement avoir pour premier paramètre un objet de cette classe (le paramètre implicite). Une méthode ne peut donc pas avoir zéro paramètre.




La définition d’une méthode d’une classe se fait exactement avec la même notation que la définition d’une fonction. En Python, le paramètre implicite apparaît comme un paramètre ordinaire et prend la première position, les paramètres explicites 1 à n prenant alors les positions 2 à n + 1. Par convention, ce premier paramètre est systématiquement appelé self !. 

Ce paramètre étant un objet, notez que l’on va pouvoir accéder à ses attributs avec la notation self.a . Ainsi, les méthodes texte et avance de la classe Chrono peuvent être définies de la manière suivante :

  def texte(self):
     return (str(self.heures) + 'h '
     + str(self.minutes) + 'm '
     + str(self.secondes) + 's')

  def avance(self, s):
     self.secondes += s
     # dépassement secondes
     self.minutes += self.secondes // 60
     self.secondes = self.secondes % 60
     # dépassement minutes
     self.heures += self.minutes // 60
     self.minutes = self.minutes % 60


Remarque :  Dans d’autres langages de programmation orientée objet, il s'appelle parfois this. 



","title":"Définition d’une méthode"},{"edit":"

Tester et écrire ici le résultat.

"}],[{"text":"
Erreurs. Ne pas faire apparaître les parenthèses après un appel de méthode ne déclenche pas l'appel, même si la méthode n’attendait aucun paramètre explicite.
Cet oubli ne provoque pas pour autant une erreur
en Python, l'interprète construisant à la place un élément particulier représentant «la méthode associée à son paramètre implicite». On pourra ainsi observer la réponse suivante si l’on tente un appel incomplet dans
la boucle interactive.

>>> t.texte

<bound method Chrono.texte of <__main__.Chrono object at

0x10d8ac198>>

L’appel n'ayant pas lieu, cet oubli se manifestera en revanche certaine-
ment plus tard, soit du fait que cette valeur spéciale produite n’est pas
le résultat de la méthode sur lequel on comptait, soit plus sournoisement
car l’objet n’a pas été mis à jour comme il aurait dû l’être.

En revanche, utiliser un attribut numérique comme une méthode déclenche cette fois bien une erreur immédiate.

>>> t.heures()
Traceback (most recent call last):

File \"<stdin>\", line 1, in <module>
TypeËrror: ’int’ object is not callable

","title":" "},{"edit":"

Tester et écrire ici le résultat.

"}],[{"text":"
La construction d’un nouvel objet avec une expression comme :

Chrono(21, 34, 55) 

déclenche deux choses :

1. la création de l’objet lui-même, gérée directement par l'interprète ou le compilateur du langage, 
2. l'appel à une méthode spéciale chargée d’initialiser les valeurs des attributs. Cette méthode, appelée constructeur, est définie par le programmeur. 

En Python, il s’agit de la méthode  __init__ que nous
avons pu observer dans les exemples.

La définition de la méthode spéciale __init__ ne se distingue en rien de la définition d'une méthode ordinaire : son premier attribut est self et représente l’objet auquel elle s’applique, et ses autres paramètres sont les paramètres donnés explicitement lors de la construction. La particularité de cette méthode est la manière dont elle est appelée, directement par l’interprète Python en réponse à une opération particulière.



","title":"Constructeur"},{"edit":"

Tester et écrire ici le résultat.

"}],[{"text":"
Il existe en Python un certain nombre d’autres méthodes particulières, chacune avec un nom fixé et entouré comme pour __init__ de deux paires
de symboles _. Ces méthodes sont appelées par certaines opérations prédéfinies de Python, permettant parfois d’alléger ou d’uniformiser la syntaxe. Il existe de telles méthodes d'usage général, comme les exemples donnés dans le tableau suivant.

 Méthode  Appel   Effet  
__str__(self)str(t)
renvoie une chaîne de caractères
__lt__(self,u)t<urenvoie True si t est strictement plus petit que u
__hash__(self)

hash(t)

donne un code de hachage pour t, par exemple pour l'utiliser comme clé d'un dictionnaire d 
 __len__(self)len(t)  | renvoie un nombre entier définissant la taille de t 
 __contains__(self, x)  x in trenvoie True si et seulement si x est dans la collection
 __getitem__(self, i) t[i]renvoie le i-ième élément de t 
  

Remarque : La méthode texte de notre classe Chrono correspond exactement au rôle de la méthode __str__, mais ne bénéficie pas de la syntaxe allégée.

 

 

puisque nous n’avons pas utilisé le nom dédié à cela. On pourrait éventuel
lement ajouter la définition suivante.

def __str__(self):
return self.texte()



","title":"Autres méthodes particulières en Python"},{"edit":"

Tester et mettre le résultat ici (code et figure).

"}],[{"text":"
 Par défaut, la comparaison entre deux objets avec
== ne considère pas comme égaux deux objets avec les mêmes valeurs pour chaque attribut : elle ne renvoie True que lorsqu'elle est appliquée deux fois au même objet, identifié par son adresse en mémoire.

Pour que cette comparaison caractérise les objets qui, sans être physiquement les mêmes, représentent la même valeur, il faut définir la méthode spéciale 

__eq__(self, other) 

On peut à cette occasion soit simplement comparer les valeurs de chaque attribut, soit appliquer un critère
plus fin adapté à la classe représentée.



","title":"Égalité entre objets."},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"title":"Classes et espaces de noms","text":"
Deux classes, même définies dans un même fichier, peuvent tout à fait avoir des attributs ou des méthodes
de même nom sans que cela prête à confusion. En effet, on accède toujours aux méthodes et attributs d’une classe via un objet de cette classe (voire dans certains cas via le nom de la classe elle-même) et l’identité de cet objet permet de résoudre toutes les ambiguïtés potentielles.

Ainsi, des noms d’attributs courants comme x ou y pour des coordonnées dans le plan ou des noms de méthodes généraux comme ajoute ou __init__ peuvent être utilisés dans plusieurs classes différentes sans
risque de confusion. On dit qu’une classe définit un espace de noms, c’est-à-dire une zone séparée des autres en ce qui concerne le nommage des variables et des autres éléments. 
Attention en revanche, une classe donnée ne peut contenir un attribut et une méthode de même nom.
"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
Dans un style de programmation objet ordinaire, l’appel de méthode se fait exclusivement avec la notation pointée t.m(e1, ... , en),

En Python, il reste toutefois possible d'accéder directement à une méthode m d’une classe C et de l’appeler comme une fonction ordinaire. Il faut dans ce cas bien passer le paramètre implicite comme les autres : C.m(t, e1, ..., en).



","title":"Accès direct aux méthodes"},{"htm":"","css":"","js":""}],[{"text":"
On à vu que pouvaient exister des attributs de
classe, dont la valeur ne dépend pas des instances mais est partagée au niveau de la classe entière. De même, la programmation objet connaît une notion de méthode de classe, aussi appelée méthode statique, qui ne s'applique pas à un objet en particulier. Ces méthodes sont parfois pertinentes pour réaliser des fonctions auxiliaires ne travaillant pas directement sur les objets de la classe ou des opérations s'appliquant à plusieurs instances aux rôles symétriques et dont aucune n’est modifiée.


  def est_seconde_valide(s):
   return 0 <= s and s < 60


  def max(t1, t2):
    if t1.heures > t2.heures:
      return t1
    elif t2.heures > t1.heures:
      return t2
    elif t1.minutes > t2.minutes:
     .....

Pour appeler de telles méthodes, on peut utiliser la notation d'accès direct avec le nom de la classe.

>>> Chrono.est_seconde_valide(64) :
False

>>> Chrono.max(t, u)

<__main__.Chrono object at Ox10d8ac198>

Notez que de telles méthodes sont équivalentes à des fonctions qui seraient définies à l’extérieur de la classe. Cette notion n'est donc pas cruciale en Python. Sachez simplement que définir comme ici une méthode sans paramètre self suffit à créer l’effet simple que nous venons de décrire, sans correspondre exactement à la notion de méthode statique de Python.


","title":"Méthodes de classe"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
Programme 12 — Une classe pour les ensembles 

class Ensemble:
def __init__(self):
self.taille = 0
self.paquets = [[] for _ in range(23)]

def contient(self, x):
return x in self.paquets[x % 23]

def ajoute(self, x):
if not self.contient(x):
self.taille += 1
self.paquets[x % 23].append(x)

def contient_doublon(t):
s = Ensemble()
for x in t:
if s.contient (x):
return True
s.ajoute(x)
return False


Le programme 12 donne une adaptation sous la forme d’une classe du programme 9. Nous profitons de la facilité avec laquelle une classe peut regrouper plusieurs données pour mémoriser la taille de l'ensemble à côté de la table des paquets. Cela permettrait par exemple une définition simple
d’une méthode __len__.

  def __len__(self):
    return self.taille

Notez que la réalisation de notre structure d'ensemble sous la forme d’une classe demande quelques modifications superficielles de notre fonction 
contient_doublon, également incluses dans ce programme :

  -  l'appel à la fonction cree() est remplacé par un appel au constructeur
Ensemble() ;
  - les appels de fonctions contient(s, x) et ajoute(s, x) sont transformés en les appels de méthodes s.contient(x) et s.ajoute(x). 

S'il n’est pas envisageable de faire passer ainsi le code client en style objet on peut cependant toujours ajouter, en dehors de la définition de la classe, des fonctions encapsulant ce détail.

  def cree():
    return Ensemble()

  def contient(s, x):
    return s.contient(x)


","title":"Une classe pour les ensembles"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
***
Dans la philosophie objet, l'interaction avec les objets d’une classe se fait essentiellement avec les méthodes, et les attributs sont considérés par défaut comme relevant du détail d’implémentation. 

Ainsi, concernant la classe Chrono, il est fondamental de savoir que l’on peut afficher et faire évoluer
les temps, mais l'existence des trois attributs heures, minutes et secondes est anecdotique.

En l'occurrence, il serait certainement bienvenu de modifier la structure de cette classe pour simplifier toutes les opérations arithmétiques sur les temps. On pourrait ainsi se contenter d’un unique attribut _temps mesurant le temps en secondes.

class Chrono:
  def __init__(self, h, m, s):
    self. _temps = 3600*h + 60%xm + s

Les opérations arithmétiques modifieraient alors cet attribut sans besoin de se soucier des dépassements comme c'était le cas dans notre première version de la méthode avance.

  def avance(self, s):
     self. _temps += s

En contrepartie, nous devons adapter le code de certaines méthodes pour qu’elles assurent la conversion entre les secondes et les triplets «heures,minutes,secondes».

def texte(self):
return (str(self._ temps // 3600) + ’h ?
+ str((self._temps // 60) % 60) + ’m ?
+ str(self. temps % 60) + ?’s’)

Dans certains cas, on pourra introduire également des méthodes qui ne sont pas destinées à faire partie de l’interface. Par exemple ici, on peut ajouter une méthode _conversion qui extrait d’un temps le triplet (h,m,s) correspondant, destinée à être utilisé par les méthodes principale.

Programme 13 — Chronomètre compté en secondes

 

class Chrono:
def __init__(self, h, m, s):
self._temps = 3600*h + 60*m + s

def texte(self):
h, m, s = self._conversion()
return str(h) + 'h ' + str(m) + 'm ' + str(s) + 's'

def avance(self, s):
self. _temps += s

def egale(self, u):
return self._temps == u._temps

def clone(self):
h, m, s = self._conversion()
return Chrono(h, m, s)

def _conversion(self):
s = self._temps
return (s // 3600, (s // 60) % 60, s % 60)


comme texte et clone. 
Le programme 13 donne une version complète de la
classe Chrono qui inclut cette méthode auxiliaire.

Rappelons que la présence de ce symbole _ n’est qu’une déclaration d’intention en Python : il rappelle à un utilisateur extérieur que celui-ci n’est pas censé utiliser la méthode _conversion, sans que rien dans le langage ne l'empêche réellement de le faire.

Notez que, cette remarque étant faite sur la philosophie de la programmation objet, il aurait mieux valu appeler nos trois attributs _heures, _minutes et _secondes dans la première version de la classe Chrono, c’est-à-dire préfixer leur nom d’un symbole _ soulignant leur caractère interne. 

En revanche, les méthodes conservent bien, sauf exception, leur nom tel quel : elles forment 
******

","title":"Retour sur l'encapsulation"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
``````
La notion d’héritage présentée dans cette section est intégralement hors programme. Cependant, cette notion étant l’un des principaux éléments distinctifs de la programmation orientée objet, il nous faut l’aborder brièvement pour donner une présentation équilibrée de ce paradigme de programmation.

Il est possible de définir une nouvelle classe comme une extension d’une classe existante. Dans ce contexte, la classe d’origine est appelée classe de
base ou classe mère et la nouvelle classe fille. Dans une telle situation d’extension, la classe fille possède automatiquement tous les attributs et méthodes de la classe de base (on dit qu’elle en hérite), et peut en outre ajouter à sa définition de nouveaux attributs et de nouvelles méthodes qui lui sont spécifiques. Il faut comprendre la classe fille comme définissant un cas
particulier de la structure générale décrite par la classe mère. On dit donc aussi que la classe fille est une spécialisation de la classe mère et que la classe
mère est une généralisation de la classe fille.

Ainsi, une structure CompteARebours peut être définie comme un Chrono qui posséderait, en plus de ce qui caractérise un Chrono ordinaire, la capacité de faire évoluer son temps à reculons. La définition à écrire pour cela est la suivante.

class CompteARebours(Chrono) :
def tac(self):
self._temps -= 1


À la première ligne de la définition, le nom de la nouvelle classe suit directement le mot-clé class et le nom de la classe de base est fourni entre parenthèses. 

La définition du contenu de la classe ne mentionne ensuite que ce qui est spécifique à la classe fille, ici la nouvelle méthode tac. Toutes les méthodes déjà présentes dans Chrono, comme __init__ et texte, sont
héritées : elles sont présentes sans qu’on les ait mentionnées à nouveau.

>>> c = CompteARebours(0,1,1)
>>> c.tac()

>>> c.tac()

>>> c.texte()

?0h 0m 59s?

Dans une situation d’héritage, une instance de la classe fille possède tous les attributs et méthodes de la classe mère. Il s'ensuit que tout programme destiné à manipuler une instance de la classe mère arrivera tout aussi bien à manipuler une instance de la classe fille : un objet de la classe fille peut être vu comme un objet de la classe mère.

Il se trouve que la spécialisation que représente une classe fille va plus loin que le seul ajout de nouveaux attributs ou de nouvelles méthodes 

£££££

être mieux adaptées au cas particulier défini par la classe fille. Pour cela, il suffit de définir à nouveau une méthode du même nom qu’une méthode déjà  existante.

On peut par exemple définir dans la classe CompteARebours étendant Chrono une nouvelle version de la méthode texte.

class CompteARebours (Chrono) :
   def texte(self):
        h, m, s = self._conversion()
        return ’plus que ' + str(h) + 'h ' \\
             + str(m) + 'm' + str(s) + 's'

Notez que ce code réalise au passage un appel self._conversion(), qui fait référence à la méthode _conversion héritée de la classe Chrono.


","title":"Héritage"},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
. De telles extensions permettent plus de modularité et de réutilisation de code, grâce aux deux effets cumulatifs
suivants :

  - D'une part, grâce à l'héritage, toute classe fille bénéficie de l’intégralité du code qui a été écrit pour sa classe mère sans qu'il soit besoin de le réécrire. Cet effet peut être démultiplié lorsque l’on a besoin de créer plusieurs classes qui sont des variantes les unes des
autres : une classe mère peut regrouper tout ce qui est commun aux classes que l’on souhaite définir, puis chacune peut hériter de cette même classe mère et n’ajouter que ce qui lui est spécifique.

   - D'autre part, grâce au principe de substitution selon lequel tout objet d’une classe fille peut être utilisé à la place d’un objet de la classe mère, il est possible d'écrire des fonctions polymorphes, c’est-à-dire des fonctions qui s’appliquent à plusieurs types d’objets. En l'occurrence, une fonction attendant en paramètre un objet d’une classe mère pourra également s'appliquer à tout objet de toute classe fille.



","title":"Héritage et réutilisation de code"},{"edit":"


"}],[{"text":"
Il est possible en Python de définir une nouvelle
classe étendant plusieurs autres classes à la fois. La classe fille hérite ainsi des méthodes de tous ses parents, ce qui donne un pouvoir supplémentaire au mécanisme d’héritage. 

Notez cependant que tous les langage de programmation objet n'autorisent pas cet héritage multiple, qui pose par ailleurs d’autres questions, notamment pour éviter les ambiguïtés lorsque plusieurs parents définissent des méthodes de même nom.


","title":"Héritage multiple"},{"edit":"


"}],[{"text":"
Lorsqu'une classe fille redéfinit une des méthodes de sa classe mère, il reste toujours possible de faire référence à la version d’origine. On utilise pour cela la fonction spéciale super(), qui donne accès aux méthodes telles que définies dans la classe mère. On peut ainsi faciliter la redéfinition de texte en réutilisant la
version définie dans la classe mère Chrono.

  def texte(self):
   return ’plus que ' + super().texte()

Techniquement, en Python, cette fonction super renvoie un objet particulier, chargé d’aller chercher dans la classe mère le code des méthodes appelées (en cas d’héritage multiple c’est évidemment un peu plus fin).

Du point de vue des valeurs des attributs, tout se passe en revanche bien comme si le paramètre implicite était self. 

En Python, il est également possible d'utiliser un accès direct et de remplacer l’expression super().texte() par Chrono.texte(self).


","title":"Accès aux méthodes de la classe mère."},{"edit":"


"}],[{"text":"
Une exception est une structure de données contenant plusieurs informations, dont généralement un message et un résumé de l’état de la pile d'appels au moment où l’exception a été levée. 

Ce sont notamment ces données qui sont affichées lorsqu'un programme est interrompu par une exception.

En Python, cette structure est un objet. Les noms des exceptions sont en réalité des noms de classes, et la ligne 

      raise ValueError(’indice négatif’)

contient en réalité deux actions : d’abord l'appel du constructeur ValueËrror, créant une instance de la classe éponyme; puis l’interruption du programme avec raise, qui transmet l'instance qui vient d'être créée et qui pourra être manipulée comme l’objet ordinaire qu’elle est dans les blocs alternatifs des constructions 

      try /except 

englobantes.

Pour récupérer et nommer cette instance, on utilise la forme 

     except ValueError as err:

où le nom fourni après le mot-clé as est un identifiant, de notre choix, définissant une variable locale utilisable uniquement dans ce bloc alternatif.

Ainsi, il est également possible de définir soi-même de nouvelles exceptions avec des noms et attributs sur mesure : il suffit de définir de nouvelles classes étendant la classe Exception de Python.


","title":"La vraie nature des exceptions."},{"edit":"

Mettre le résultat ici (code et figure).

"}],[{"text":"
Définir une classe Fraction pour représenter un nombre rationnel. Cette classe possède deux attributs, num et denom, qui sont des entiers et désignent respectivement le numérateur et le dénominateur. 

On demande que le dénominateur soit plus particulièrement un entier strictement positif.

  - Écrire le constructeur de cette classe. Le constructeur doit lever une ValueError si le dénominateur fourni n’est pas strictement positif.

  - Ajouter une méthode __str__ qui renvoie une chaîne de caractères de la forme \"12 / 35\", ou simplement de la forme \"12\" lorsque le dénominateur vaut un.

   -  Ajouter des méthodes __eq__ et  __it__ qui reçoivent une deuxième fraction en argument et renvoient True si la première fraction représente respectivement un nombre égal ou un nombre strictement inférieur à la deuxième fraction.

  -  Ajouter des méthodes __add__ et __mul__ qui reçoivent une deuxième fraction en argument et renvoient une nouvelle fraction représentant respectivement la somme et le produit des deux fractions.

   - Tester ces opérations.

Bonus : s'assurer que les fractions sont toujours sous forme réduite.


","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
#On teste la valeur du dénominateur dans le construc-
#teur. Pour le reste, on applique les règles de calcul et on construit une
#nouvelle fraction dans chaque opération arithmétique.

class Fraction:
def __init__(self, n, d):
if d <= 0:
raise ValueError(str(d) + \"n’est pas strictement positif\")
self.num = n
self.denom = d

def __str__(self):
if self.denom == 1 :
return str(self.num)
else :
return str(self.num) + ' / ' + str(self.denom)

def __eq__(self, f):
return self.num * f.denom == f.num * self.denom

def __it__(self, f):
return self.num * f.denom < f.num * self.denom

def __add__(self, f):
return Fraction(self.num*f.denom + f.num*self.denom, self.denom * f.denom)

def __mul__(self, f):
return Fraction(self.num*f.num, self.denom*f.denom)


"},{"solution":"Bonus :
class Fraction:
def __init__(self, n, d):
if d <= 0:
raise ValueError(str(d) + \"n’est pas strictement positif\")
p = pgcd(n, d)
self.num = n //p
self.denom = d // p

#et la fonction auxiliaire pour calculer le PGCD (voir exercice 5 page 16).
def pgcd(a, b):
if b == 0:
return a
return pgcd(b, a % b)

def __str__(self):
if self.denom == 1 :
return str(self.num)
else :
return str(self.num) + ' / ' + str(self.denom)

def __eq__(self, f):
return self.num * f.denom == f.num * self.denom

def __it__(self, f):
return self.num * f.denom < f.num * self.denom

def __add__(self, f):
return Fraction(self.num*f.denom + f.num*self.denom, self.denom * f.denom)

def __mul__(self, f):
return Fraction(self.num*f.num, self.denom*f.denom)


"}],[{"text":"
Définir une classe Intervalle représentant des intervalles de nombres. Cette classe possède deux attributs a et b représentant respectivement l’extrémité inférieure et l'extrémité supérieure de l’intervalle. 

Les deux extrémités sont considérées comme incluses dans l'intervalle. 

Tout intervalle avec b < a représente l'intervalle vide.

  - Écrire le constructeur de la classe Intervalle et une méthode est_vide renvoyant True si l’objet représente l’intervalle vide et False sinon.

  - Ajouter des méthodes __len__ renvoyant la longueur de l'intervalle (l'intervalle vide à une longueur 0) et __contains__ testant l’appartenance à l'intervalle.

 - Ajouter une méthode __eq__ permettant de tester l'égalité de deux intervalles avec == et une méthode __le__  permettant de tester l'inclusion d’un intervalle dans un autre avec <=.
 
Attention : toutes les représentations de l'intervalle vide doivent être considérées égales, et incluses dans tout intervalle.

  - Ajouter des méthodes intersection et union calculant respectivement l'intersection de deux intervalles et le plus petit intervalle contenant l’union de deux intervalles (l'intersection est bien toujours un intervalle, alors que l’union ne l’est pas forcément). Ces deux fonctions doivent renvoyer un nouvel intervalle sans modifier leurs paramètres.

Tester ces méthodes.


","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
class Intervalle:
\"\"\"classe pour représenter un intervalle fermé
d’extrémités a et b, considéré vide si b < a\"\"\"
def __init__(self, a, b):
self.a = a
self.b = b
def est_vide(self):
return self.b < self.a

#La longueur d’un intervalle est donnée par la différence entre les bornes,
#avec cas particulier pour l'intervalle vide.

def __len__(self):
return max(0, self.b-self.a)
def __contains__(self, x):
return self.a <= x <= self.b

#Deux intervalles sont égaux s’ils ont les mêmes bornes, ou s’ils sont tous deux
#vides. L’inclusion est déterminée par une comparaison des indices lorsque les
#intervalles sont non vides, mais le cas self vide doit être traité à part.

def __eq__(self, i):
return self.est_vide() and i.est_vide() \\
or self.a == i.a and self.b == i.b

def __le__(self, i):
return self.est_vide() \\
or i.a <= self.a and self.b <= i.b

def intersection(i, j):
return Intervalle(max(i.a, j.a), min(i.b, j.b))

def union(i, j):
return Intervalle(min(i.a, j.a), max(i.b, j.b))

"}],[{"text":"
Définir une classe Angle pour représenter un angle en degrés.

Cette classe contient un unique attribut, angle, qui est un entier. On demande que, quoiqu'il arrive, l'égalité 
0<angle<360 reste vérifiée.

Écrire le constructeur de cette classe.

  - Ajouter une méthode __str__ qui renvoie une chaîne de caractères de la forme \"60 degrés\". 
Observer son effet en construisant un objet de la classe Angle puis en l’affichant avec print.

 - Ajouter une méthode ajoute qui reçoit un autre angle en argument (un objet de la classe Angle) et l’ajoute au champ angle de l’objet.
Attention à ce que la valeur d'angle reste bien dans le bon intervalle.

  - Ajouter deux méthodes cos et sin pour calculer respectivement le cosinus et le sinus de l’angle. 
On utilisera pour cela les fonctions cos et sin de la bibliothèque math. 
Attention : il faut convertir l’angle en radians (en le multipliant par π/180) avant d’appeler les fonctions cos
et sin.

Tester les méthodes ajoute, cos et sin.

","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
#On fait bien attention de maintenir l’angle entre 0 et
#360, dans le constructeur __init__ ainsi que dans la méthode ajoute.

from math import cos, sin, pi

class Angle:
\"\"\"une classe pour représenter un angle en degrés\"\"\"
def __init__(self, a):
self.ansle = a % 360

def __str__(self):
return str(self.angle) + \" degrés\"

def ajoute(self, a):
self.angle = (self.angle + a.angle) % 360

def radians(self):
return self.angle * pi / 180

def cos(self):
return cos(self.radians())

def sin(self):
return sin(self.radians())

#Pour ce qui est de la conversion en radians, on a choisi ici d'ajouter une
#méthode radians, qui a un intérêt en soi. Mais une simple fonction conver-
#tissant des degrés vers des radians conviendrait tout aussi bien.

#Pour tester, on peut utiliser des angles remarquables (0, 30, 45, 60, 90,
#etc.) pour lesquels on connaît la valeur du cosinus et du sinus.

"}],[{"text":"
Définir une classe Date pour représenter une date, avec trois attributs jour, mois et annee.

Ecrire son constructeur.

  - Ajouter une méthode __str__ qui renvoie une chaîne de caractères de la forme \"8 mai 1945\". On pourra se servir d’un attribut de classe qui est un tableau donnant les noms des douze mois de l’année. 

Tester en construisant des objets de la classe Date puis en les affichant avec print .

  - Ajouter une méthode __it__ qui permet de déterminer si une date d1 est antérieure à une date d2 en écrivant d1 < d2. La tester.



","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
class Date:
\"\"\"lune classe pour représenter une date\"\"\"

def __init__(self, j, m, a):
self.jour = j
self.mois = m
self.annee = a

nom_mois = [\"janvier\", \"février\", \"mars\", \"avril\", \\
\"mai\", \"juin\", \"juillet\", \"août\", \"septembre\", \\
\"octobre\", \"novembre\", \"décembre\"]

def __str__(self):
return str(self.jour) + \" \" + \\
Date.nom_mois[self.mois - 1] +\" \" + \\
str(self.annee)

def __it__(self, d):
return self.annee < d.annee or \\
self.annee == d.annee and (self.mois < d.mois or \\
self.mois == d.mois and self.jour < d.jour)

#Pour cette dernière méthode, il est important de savoir que l’opération and
#a une priorité plus forte que l'opération or.
"}],[{"text":"
Dans certains langages de programmation, comme Pascal ou Ada, les tableaux ne sont pas nécessairement indexés à partir de 0. C’est le
programmeur qui choisit sa plage d’indices. 
Par exemple, on peut déclarer un tableau dont les indices vont de  -10 à 9 si on le souhaite. 

Dans cet exercice, on se propose de construire une classe Tableau pour réaliser de tels tableaux.
Un objet de cette classe aura deux attributs, un attribut premier qui est la valeur de premier indice et un attribut contenu qui est un tableau Python contenant les éléments. Ce dernier est un vrai tableau Python, indexé à partir de 0.

  - Écrire un constructeur __init__(self, tmin, tmax, v) où tmin est le premier indice, tmax le dernier indice et v la valeur utilisée pour initialiser toutes les cases du tableau.  

Ainsi, on peut écrire
   t = Tableau(-10, 9, 42) 
pour construire un tableau de vingt cases, indexées de -10 à 9 et toutes initialisées avec la valeur 42.

  - Écrire une méthode __len__(self) qui renvoie la taille du tableau.

   - Écrire une méthode __getitem__(self, i) qui renvoie l'élément du tableau self d'indice i. De même, écrire une méthode __setitem__(self, i, v) qui modifie l’élément du tableau self d'indice i pour lui donner la valeur v. 
Ces deux méthodes doivent vérifier que l’indice i est bien valide et, dans le cas contraire, lever l'exception IndexError avec la valeur de i en argument (c’est-à-dire
raise IndexError(i)).

  -  Enfin, écrire une méthode __str__(self) qui renvoie une chaîne de caractères décrivant le contenu du tableau.
","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
class Tableau:
\"\"\"tableau avec une plage d’indices quelconqgue\"\"\"
def __init__(self, imin, imax, v):
self.premier = imin
self.contenu = [v] * (imax - imin + 1)

def __len__(self):
return len(self.contenu)

def __indice_valide(self, i):
if i < self.premier or \\
i >= self.premier + len(self.contenu):
raise IndexError(i)

def __getitem__(self, i):
self._indice_valide(i)
return self.contenu[i - self.premier]

def __setitem__(self, i, v):
self._indice_valide(i)
self.contenu[i - self.premier] = v

def __str__(self):
return str(self.contenu)


"}],[{"text":"
On veut définir une classe TaBiDir pour des tableaux 
bidirectionnels, dont une partie des éléments ont des indices positifs et une partie des éléments ont des indices négatifs, et qui sont extensibles aussi bien par
la gauche que par la droite. 

Plus précisément, les indices d’un tel tableau bidirectionnel vont aller d’un indice imin à un indice Îmax, tous deux inclus, et tels que imin ≤ 0 et -1≤imax. 

Le tableau bidirectionnel vide correspond
AU CAS OÙ imin vaut 0 et imax vaut -1.

La classe TaBiDir a pour attributs deux tableaux Python: 
   un tableau droite contenant l'élément d'indice 0 et les autres éléments d'indices positifs, 
  et un tableau gauche tel que gauche[0] contient l'élément d'indice -1 du tableau bidirectionnel, et gauche[1], gauche [2], etc. contiennent les éléments d'indices négatifs suivants, en progressant vers la gauche.

  - Écrire un constructeur __init__(self, g, d) construisant un tableau bidirectionnel contenant, dans l’ordre, les éléments des tableaux g et d. Le dernier élément de g (si g n’est pas vide), devra être calé sur l'indice -1 du tableau bidirectionnel, et le premier élément de d (si d n'est pas vide) sur l'indice 0. 
Ecrire également des méthodes imin(self) et imax(self) renvoyant respectivement l’indice minimum et l'indice maximum.

  - Ajouter une méthode append(self, v), qui comme son homonyme des tableaux Python ajoute l'élément v à droite du tableau bidirectionnel self, et une méthode prepend(self, v) ajoutant cette fois l'élément v à gauche du tableau bidirectionnel self.
 Utiliser append sur un tableau bidirectionnel vide place l'élément à l’indice 0. Utiliser prepend sur un tableau bidirectionnel vide place l'élément à l'indice -1.

  - Ajouter une méthode __getitem__(self, i) qui renvoie l’élément du tableau bidirectionnel self à l'indice i, et une méthode __setitem__(self, i, v) qui modifie l'élément du tableau self d'indice i pour lui donner la valeur v.

  - Ajouter une méthode __str__(self) qui renvoie une chaîne de caractères décrivant le contenu du tableau.

","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
#Pour obtenir les éléments dans le bon ordre, il faut
#à l’initialisation former l’attribut gauche avec les éléments du paramètre
#g pris dans l’ordre inverse. Dans un cas comme dans l’autre, on travaille
#avec un nouveau tableau et non avec l'original pour éviter les problèmes de
#partage.

class TaBiDir:

def __init__(self, g, d):
self.gauche = g[::-1]
self.droite = d[:]

def imin(self):
return - len(self.gauche)

def imax(se1f) :
return len(self.droite) - 1

#Ceci étant fixé, étendre à gauche ou à droite correspond directement à
#étendre le tableau de gauche ou le tableau de droite.

def append(self, v):
self.droite.append(v)

def prepend(self, v):
self.gauche.append(v)

#Les accès aux éléments sont répartis à gauche ou à droite selon que i est
#positif ou négatif. Attention au décalage des indices dans la partie gauche
#(l'indice -1 du tableau bidirectionnel correspond à l'indice O0 du tableau
#gauche).

def __getitem__(self, i):
if i >= 0:
return self.droite[i]
else:
return self.gauche[-i-1]

def __setitem__(self, i, v):
if i >= 0:
self.droite[i] = v
else:
self.gauche[-i-1] = v

#Pour __str__, on réutilise ici la méthode __str__ des tableaux de Py-
#thon pour avoir directement la présentation avec éléments séparés par des
#virgules, mais il faut ajouter un peu de découpage/collage pour retirer un
#crochet de chaque côté et séparer les deux parties par une virgule.

def __str__(self):
return str(self.gauche[::-1])[0:-1] + ', ' \\
+ str(self.droite)[1:]

"}],[{"text":"
Nous allons illustrer la programmation orientée objet sur un mini-projet inspiré du jeu des lemmings. Dans ce jeu, les lemmings marchent dans une grotte représentée par une grille à deux dimensions dont
chaque case est soit un mur soit un espace vide, un espace vide pouvant contenir au maximum un lemming à un instant donné. 

Les lemmings apparaissent l’un après l’autre à une position de départ, et disparaissent lorsqu'ils atteignent une case de sortie. Chaque lemming a une coordonnée verticale et une coordonnée horizontale désignant la case dans laquelle il se trouve, ainsi qu’une direction (gauche ou droite). Les lemmings se déplacent à tour
de rôle, toujours dans l’ordre correspondant à leur introduction dans le jeu, de la manière suivante :

  - si l’espace immédiatement en-dessous est libre, le lemming tombe d’une case ;

  - sinon, si l’espace immédiatement devant est libre (dans la direction du lemming concerné), le lemming avance d’une case;

  - enfin, si aucune de ces deux conditions n’est vérifiée, le lemming se retourne.

On propose pour réaliser un petit programme permettant de voir évoluer une colonie de lemmings une structure avec une classe Lemming pour les
lemmings, une classe Case pour les cases de la grotte, et une classe principale Jeu pour les données globales.

La classe principale Jeu contient un attribut grotte contenant un tableau à deux dimensions de cases, et un attribut lemmings contenant un tableau des lemmings actuellement en jeu. 

Son constructeur initialise la grotte, par exemple à partir d’une carte donnée par un fichier texte d’un format inspiré du suivant, où # représente un mur, où les lemmings apparaissent au niveau de la case vide de la première ligne, et D représente la sortie.


# ################
#                #              #
#####  ###########
#            #   #               #
#  ###########.  #
#.               D
########  ########
       #. #.      
       ####

Cette classe fournit notamment les méthodes suivantes:  
  - affiche(self) affiche la carte avec les positions et directions de tous les lemmings en jeu;

  - tour(self) fait agir chaque lemming une fois et affiche le nouvel état du jeu;

  - demarre(self) lance une boucle infinie attendant des commandes de l'utilisateur. 
Exemples de commandes : 1 pour ajouter
un lemming,  q pour quitter, et Entrée pour jouer un tour.

  - Une classe Lemming avec des attributs entiers positifs 1 et c indiquant la ligne et la colonne auxquelles se trouve le lemming, et un attribut valant 1 si le lemming se dirige vers la droite et -1 si le lemming se
dirige vers la gauche. Il sera aussi utile d’avoir un attribut j pointant sur l'instance de la classe Jeu pour laquelle le lemming a été créé, pour accéder au terrain et à la liste des lemmings. 

Cette classe fournit en outre les méthodes suivantes :

  -  __str__(self) renvoie '>' ou '<' selon la direction du lemming ;
  - action(self) déplace ou retourne le lemming ;
  -  sort(self) retire le lemming du jeu.
  - La classe Case contient un attribut terrain contenant le caractère représentant la caractéristique de la case (mur, vide, sortie), et un attribut lemming contenant l’éventuel lemming présent dans cette case
et None si la case est libre. Cette classe fournit notamment les méthodes
  - __str__(self) renvoie le caractère à afficher pour représenter cette case ou son éventuel occupant ;

  -  libre(self) renvoie True si la case est peut recevoir un lemming (elle n’est ni un mur, ni occupée) ;

  -  depart(self) retire le lemming présent ;

  - arrivee(self, lem) place le lemming lem sur la case, ou le fait sortir du jeu si la case était une sortie.

Cette base peut ensuite évidemment être étendue avec des terrains plus variés, de nouvelles possibilités d’interaction pour le joueur, des objectifs en termes de nombre de lemmings sauvés, etc. 
","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
#Dans les cases, on utilise comme dans le plan le
#caractère ? ? pour représenter une case vide, *#’ un mur et ?0? une sortie.
#Dans la méthode __str__ on renvoie, le cas échéant, la représentation du
#lemming présent plutôt que la représentation du terrain lui-même.

class Case:
def __init__(self, char):
self.terrain = char
self.lem = None

def __str__(self):
if self.lem is not None:
return str(self.lem)
else:
return self.terrain

def libre(self):
return (self.terrain == ' ' or self.terrain == '0') \\
and self.lem == None

def depart(self):
self.lem = None

def arrivee(self, lem):
if self.terrain == '0':
lem.sort()
else:
self.lem = lem

#Dans la classe Lemming on introduit une méthode additionnelle
#deplacement (self, 1, c) qui tente de déplacer le lemming vers la case
#de coordonnées (1, c) : si la case est libre le lemming est effectivement dé-
#placé et l’appel renvoie True ; si la case n’est pas libre le lemming ne bouge
#pas et la méthode renvoie False. La méthode action tente alors à l’aide
#de cette méthode de faire tomber le lemming, puis de le faire avancer, et
#enfin le retourne seulement si aucun des deux mouvements n’a eu liou.
#La méthode deplace peut modifier le contenu des cases du jeu, et la méthode
#sort va modifier le tableau lemmings du jeu.

class Lemming:
def __init__(self, j, l, c):
self.j = j
self.l = l
self.c = c
self.d = 1

def __str__(self):
if self.d > 0:
return '>'
else:
return '<'

def deplacement (self, l, c):
if not self.j[l][c].libre():
return False
self.j[self.l][self.c].depart()
self.j[l][c].arrivee(self)
return True

def action(self):
if self.deplacement(self.l+1, self.c):
self.l += 1
elif self.deplacement(self.l, self.c + self.d):
self.c += self.d
else:
self.d = -self.d

def sort(self):
self.j.lemmings.remove(self)

#Pour simplifier l'accès aux cases de la grotte dans les autres classes, on inclut
#une méthode __getitem__ permettant d'accéder à la ligne d'indice i de la
#grotte directement à partir d’une instance j avec la notation j[i]. On a
#également isolé le code responsable de l’introduction d’un nouveau lemming
#dans une méthode à part. Notez dans la méthode tour que la boucle n’est
#pas faite directement sur le tableau lemmings mais sur une copie de ce
#tableau. En effet, des lemmings sont susceptibles d’être retirés du tableau
#lemmings au cours de l’itération sur ce même tableau, ce qui causerait des
#problèmes.

class Jeu:
def __init__(self, nom_de_carte):
carte = open(nom_de_carte)
self.grotte = [[Case(char) for char in ligne
if char != '\\n']
for ligne in carte.readlines()]
carte.close()
self.lemmings = []

def __getitem__(self, i):
return self.grotte[i]

def affiche(self):
for ligne in self.grotte:
for case in ligne:
print(case, end='')
print()
print ('\\n')

def ajoute_lemming(self) :
if self.grotte[0][1].libre():
lem = Lemming(self, 0, 1)
self.lemmings.append(lem)
self.grotte[0][1].arrivee(lem)

def tour(self):
for lem in self.lemmings[:]:
lem.action()

def demarre(self):
while True:
cmd = input()
if cmd == 'q':
break
elif cmd == 'l':
self.ajoute_lemming()
else:
self.tour()
self.affiche()

#Enfin, il suffit pour jouer de créer une instance du jeu en lui fournissant le
#nom d’un fichier texte contenant la carte d’une grotte et appeler sa méthode
#de démarrage.

Jeu('carte.txt').demarre()

"}],[{"text":"
Nous allons illustrer la programmation orientée objet sur un mini-projet de jeu de course. Des tortues font la course en se déplaçant à tour de rôle sur un circuit en deux dimensions matérialisé par une grille dont certaines cases sont passables et d’autres non. 

Les tortues ne sont pas très maniables et leurs capacités d'accélération, de décélération et de changement de direction sont limitées : pour déterminer les mouvements possibles à un tour on reproduit depuis la position courante de la tortue le mouvement du tour précédent, et on a ensuite le droit de décaler le point d’arrivée d’au plus une case.

Pour l'affichage, on propose d’utiliser le module turtle de Python. Sachez que ce module définit une classe Turtle, dont chaque instance donne une tortue indépendante. Ces tortues sont ensuite manipulées
avec les opérations habituelles, qui sont en réalité des méthodes.  

from turtle import Turtle

t1 = Turtle()

t2 = Turtle()

t1.left(84)

t1.forward(60)

t2.left(18)

t2.forward(90)


Ainsi les instructions créent donc deux tortues t1 et t2 animées indépendamment. En l’occurrence, chacune opère une rotation puis avance, pour représenter ensemble les deux aiguilles d’une horloge qui afficherait midi douze. 
On propose de réaliser un tel jeu en utilisant trois classes : 

  - une classe principale Jeu contenant les tortues et le terrain et animant les tortues à tour de rôle, une classe Tortubolide représentant une tortue de course, et
une classe Vecteur permettant de manipuler des couples de coordonnées.
On peut réaliser le jeu en deux étapes :
 1. Dans une première étape, permettre aux tortues de se déplacer librement sur un terrain vierge.

  - La classe Vecteur est réalisée intégralement à cette étape, avec deux attributs x et y représentant des coordonnées entières dans le plan cartésien à deux dimensions et des méthodes __eq__,
_-add__ et __sub__ permettant des comparaisons et manipuations arithmétiques de base.

  - La classe Tortubolide comporte à cette étape trois attributs : un vecteur donnant les coordonnées de la tortue, un vecteur donnant sa vitesse (le vecteur allant de la position précédente à la position actuelle), et une instance de la classe Turtle destinée à afficher le parcours de cette tortue de course. 
Les tortues ont une vitesse initiale nulle, c’est-à-dire égale à Vecteur(0, 0). 
Cette classe fournit une méthode action(self, acc) qui modifie la vitesse actuelle de la tortue en lui ajoutant le vecteur acc puis déplace la tortue en ajoutant son vecteur vitesse à son vecteur position.
Lors du déplacement, la méthode trace à l'écran le trajet suivi et à position d'arrivée.

  - La classe Jeu possède en attribut une ou plusieurs tortues de course, et définit une méthode demarre lançant la boucle de jeu attendant les commandes du ou des joueurs. Par exemple : 
’s' pour conserver le mouvement tel quel, 
’z', 'x', 'q', ’d’ pour ajuster le déplacement d’une case, respectivement vers le haut, le bas, la gauche, la droite, et ’fin’ pour quitter le jeu.

 

2. Dans une deuxième étape on ajoute les murs entourant le circuit, que les tortues de course ne peuvent pas franchir. On apporte alors les modifications suivantes.

  - La classe Jeu contient comme nouvel attribut un tableau à deux dimensions indiquant quelles cases sont ou non franchissables.
Cet attribut peut être initialisé à partir d’un dessin du circuit donné dans un fichier texte. Au démarrage du jeu, on dessine l'intégralité du circuit dans la fenêtre turtle avant d'y inclure les tortues de course (dont les coordonnées de départ peuvent de même être données dans le fichier décrivant le circuit, par exemple sur les premières lignes).

 - La classe Tortubolide a besoin d’une méthode action enrichie, dans laquelle le déplacement d'une tortue est arrêté par un mur qu’elle tenterait de franchir.
On cherchera à obtenir le comportement suivant :
  - si la trajectoire de la tortue doit traverser un mur, alors la tortue est arrêtée sur la case précédant immédiatement ce mur et sa vitesse est réduite à zéro. Pour pouvoir réaliser cette méthode, il faudra ajouter aux attributs de Tortubolide l'instance du jeu en cours.

Cette base peut ensuite être étendue, par exemple en faisant en sorte que les tortues concurrentes soient également des obstacles ou que les collisions imposent des pénalités plus sévères au redémarrage, en enrichissant le circuit, ou en améliorant l'interface graphique. Dans certaines de ces extensions,
il pourra être pertinent d'ajouter des classes, par exemple une classe Case comme dans l'exercice précédent. 

Ci-dessous, un exemple de fichier décrivant un circuit et les coordonnées de départ de deux tortues de course.

   ########################
###                                          ###
#                                                  ##
#                                                    ##
#                 ############             ##
#              ##                     ##            #
#              ##                     ##            #
#                #############             #
#                                                       #
#                                                       #
##                                                   ##
############################## 

","title":"Exercice"},{"edit":"

Mettre le résultat ici (code et figure).

"},{"solution":"
#On utilise le module turtle pour l'affichage, le module
#sys pour sa fonction exit et le module math pour décomposer le mouvement
#des tortues. On définit deux constantes globales : ECHELLE pour la longueur
#du côté d’une case en pixels et TANPI8 (tangente de ?) qui servira pour
#calculer les déplacements des tortues.

import turtle
import sys
import math

ECHELLE = 12
TANPI8 = math.tan(math.pi / 8)

#La classe vecteur applique les opérations de base coordonnée par coordonnée.

class Vecteur:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, v):
return Vecteur(self.x + v.x, self.y + v.y)

def __sub__(self, v):
return Vecteur(self.x - v.x, self.y - v.y)

def __eq__(self, other):
return self.x == other.x and self.y == other.y

#Classe Tortubolide : on utilise une méthode auxiliaire pour le déplacement,
#qui décompose le mouvement total en une suite de mouvements d’une case
#horizontaux, verticaux ou diagonaux. Cette décomposition permet d’arrêter
#la tortue sur la case précédent un mur, le cas échéant.

class Tortubolide:
def __init__(self, pos, couleur, jeu):
self.pos = pos
self.vit = Vecteur(0, 0)
self.jeu = jeu
self.t = turtle.Turtle()
self.t.up()
self.t.goto(pos.x*ECHELLE, pos.y*ECHELLE)
self.t.down()
self.t.hideturtle()
self.t.color(couleur)

def dessine(self) :
self.t.goto(self.pos.x*ECHELLE,
(self.pos.y - 0.5)*ECHELLE)
self.t.begin_fill()
self.t.circle(ECHELLE/2)
self.t.end_fill()
self.t.goto(self.pos.x*ECHELLE, self.pos.y*ECHELLE)

def action(self, acc):
self.vit += acc
self.avance_aux(self.vit)
self.t.goto(self.pos.x*ECHELLE, self.pos.y*ECHELLE)
self.dessine()

def avance_aux(self, v):
if v == Vecteur(0, 0):
return
dx = 0 if abs(v.x) < TANPI8*abs(v.y) \\
else 1 if v.x > 0 else -1
dy = 0 if abs(v.y) < TANPI8*abs(v.x) \\
else 1 if v.y > 0 else -1
dv = Vecteur(dx, dy)
if self.jeu[self.pos + dv]:
self.vit = Vecteur(0, 0)
else:
self.pos += dv
self.avance_aux(v - dv)

#Outre les méthodes de dessin et la boucle principale du jeu, on à inclut dans
#la classe Jeu une méthode __getitem__ recevant en paramètre un vecteur
#v et indiquant si la case dont les coordonnées correspondent à ce vecteur est
#un mur.

class Jeu:
def __init__(self, nom_de_carte):
carte = open(nom_de_carte)
self.x1 = int(carte.readline())
self.y1 = int(carte.readline())
self.x2 = int(carte.readline())
self.y2 = int(carte.readline())
self.murs = [[char == '#' for char in ligne]
for ligne in carte.readlines()]
carte.close()
self.tortubolides = []

def __getitem__(self, v):
return self.murs[-v.y-1][v.x]

def dessine_circuit(self):
hauteur = len(self.murs)
largeur = len(self.murs[0])
turtle.setup(largeur*ECHELLE, hauteur*ECHELLE)
turtle.setworldcoordinates(-ECHELLE, -ECHELLE,
(largeur+1)*ECHELLE,
(hauteur+1)*ECHELLE)
turtle.hideturtle()
turtle.speed(0)
for x in range(largeur) :
for y in range(hauteur):
if self.murs[-y-1][x]:
Jeu.carre(x*ECHELLE, y*ECHELLE)

def ajoute_tortubolide(self, x, y, couleur):
b = Tortubolide(Vecteur(x, y), couleur, self)
self.tortubolides.append(b)
b.dessine()

def demarre(self):
self.dessine_circuit()
self.ajoute_tortubolide(self.x1, self.y1, 'orange')
self.ajoute_tortubolide(self.x2, self.y2, 'purple')
while True:
for tortubolide in self.tortubolides:
cmd = input()
if cmd == 'fin':
sys.exit(0)
elif cmd == 's':
tortubolide.action(Vecteur(0, 0))
elif cmd == 'q':
tortubolide.action(Vecteur(-1, 0))
elif cmd == 'z':
tortubolide.action(Vecteur(0, 1))
elif cmd == 'd':
tortubolide.action(Vecteur(1, 0))
elif cmd == 'x':
tortubolide.action(Vecteur(0, -1))
else:
tortubolide.action(Vecteur(0, 0))
def carre(x, y):
d = ECHELLE/2
turtle.up()
turtle.goto(x-d, y-d)
turtle.down()
turtle.begin_fill()
turtle.goto(x+d, y-d)
turtle.goto(x+d, y+d)
turtle.goto(x-d, y+d)
turtle.goto(x-d, y-d)
turtle.end_fill()

#Il n’y a plus qu’à démarrer un jeu créé avec un circuit au choix.

Jeu('carte2.txt').demarre()

"}]]

En poursuivant votre navigation sur mon site, vous acceptez l’utilisation des Cookies et autres traceurs  pour réaliser des statistiques de visites et enregistrer sur votre machine vos activités pédagogiques. En savoir plus.