18. Héritage▲
La fonctionnalité la plus emblématique de la programmation orientée objet est l'héritage. L'héritage est la possibilité de définir une nouvelle classe, qui est une version modifiée d'une classe existante. Dans ce chapitre, j'illustre l'héritage en utilisant des classes qui représentent des cartes à jouer, des paquets de cartes et des mains de poker.
Si vous ne jouez pas au poker, vous pouvez lire à ce sujet sur https://fr.wikipedia.org/wiki/Poker, mais ce n'est pas obligatoire ; je vous dirai tout ce que vous devez savoir pour les exercices.
Les exemples de code de ce chapitre sont disponibles à l'adresse Card.py.
18-1. Objets carte de jeu▲
Il y a cinquante-deux cartes dans un paquet, dont chacune appartient à une des quatre couleurs (ou enseignes) et à l'une des treize valeurs (ou rangs). Les couleurs sont pique, cœur, carreau, et trèfle (dans l'ordre décroissant au jeu de bridge). Les valeurs sont as, 2, 3, 4, 5, 6, 7, 8, 9, 10, valet, dame (ou reine) et roi. Selon le jeu auquel vous jouez, un as peut être plus fort que le roi ou plus faible que le 2.
Si nous voulons définir un nouvel objet pour représenter une carte à jouer, il est évident que les attributs doivent être la couleur et la valeur. Le type des attributs n'est pas si évident. Une possibilité est d'utiliser des chaînes contenant des mots comme 'pique' pour les couleurs et 'dame' pour les valeurs. Un problème avec cette modélisation est qu'il ne serait pas facile de comparer les cartes pour voir laquelle a une valeur ou une couleur supérieure.
Une autre possibilité est d'utiliser des entiers pour encoder les valeurs et les couleurs. Dans ce contexte, « encoder » signifie que nous allons définir une correspondance entre nombres et couleurs, ou entre nombres et valeurs. Ce type d'encodage n'est pas censé être secret (ce serait du « cryptage » ou du chiffrement).
Par exemple, ce tableau montre les couleurs et les valeurs entières correspondantes :
Pique | ↦ | 3 |
Cœur | ↦ | 2 |
Carreau | ↦ | 1 |
Trèfle | ↦ | 0 |
Ce code facilite la comparaison des cartes ; parce que les couleurs les plus élevées correspondent aux nombres plus élevés, nous pouvons comparer les couleurs en comparant leurs codes.
Le codage des valeurs est assez évident ; chacune des valeurs numériques des cartes correspond à l'entier correspondant, et pour les honneurs :
valet | ↦ | 11 |
dame | ↦ | 12 |
roi | ↦ | 13 |
J'utilise le symbole ↦ pour qu'il soit clair que ces correspondances ne font pas partie du programme Python. Elles font partie de la conception du programme, mais elles n'apparaissent pas explicitement dans le code.
La définition de la classe Carte ressemble à ceci :
class
Carte:
"""Représente une carte à jouer standard."""
def
__init__
(
self, couleur =
0
, valeur =
2
):
self.couleur =
couleur
self.valeur =
valeur
Comme d'habitude, la méthode init prend un paramètre optionnel pour chaque attribut. La carte par défaut est le 2 de trèfle.
Pour créer une carte, vous appelez Carte avec la couleur et la valeur de la carte souhaitée.
dame_de_carreau =
Carte
(
1
, 12
)
18-2. Attributs de classe▲
Pour afficher des objets de type Carte d'une manière lisible facilement pour les humains, nous avons besoin d'une correspondance entre les codes nombres entiers et les couleurs et les valeurs correspondantes. Une façon naturelle de le faire est d'utiliser des listes de chaînes de caractères. Nous attribuons ces listes aux attributs de classe :
# à l'intérieur de la classe Carte :
noms_couleurs =
['trèfle'
, 'carreau'
, 'cœur'
, 'pique'
]
noms_valeurs =
[None
, 'as'
, '2'
, '3'
, '4'
, '5'
, '6'
, '7'
,
'8'
, '9'
, '10'
, 'valet'
, 'dame'
, 'roi'
]
def
__str__
(
self):
return
'
%s
de
%s
'
%
(
Carte.noms_valeurs[self.valeur],
Carte.noms_couleurs[self.couleur])
Les variables comme noms_couleurs et noms_valeurs, qui sont définies dans une classe, mais en dehors de toute méthode, s'appellent attributs de classe parce qu'elles sont associées à l'objet classe Carte.
Ce terme les distingue des variables telles que couleur et valeur, qui s'appellent attributs d'instance parce qu'elles sont associées à une instance particulière.
Les deux types d'attributs sont accessibles en utilisant la notation pointée. Par exemple, à l'intérieur de __str__, self est un objet Carte et self.couleur est sa couleur. De même, Carte est un objet classe, et Carte.noms_valeurs est une liste de chaînes de caractères associée à la classe.
Chaque carte a sa propre couleur et sa propre valeur, mais il n'y a qu'une seule copie de noms_couleurs et noms_valeurs.
En mettant le tout ensemble, l'expression Carte.noms_valeurs[self.valeur] signifie « utilise l'attribut valeur de l'objet self comme un index de la liste noms_valeurs de la classe Carte, et sélectionne la chaîne de caractères appropriée. »
Le premier élément de noms_valeurs est None, car il n'y existe aucune carte de rang zéro. En incluant None comme un espace réservé, nous obtenons une correspondance ayant comme belle propriété le fait que l'indice 2 corresponde à la chaîne de caractères '2', et ainsi de suite. Pour éviter de devoir faire cet ajustement, nous aurions pu utiliser un dictionnaire à la place d'une liste.
Avec les méthodes que nous avons jusqu'ici, nous pouvons créer et afficher des cartes :
>>>
carte1 =
Carte
(
2
, 11
)
>>>
print
(
carte1)
valet de cœur
La figure 18.1 est un diagramme de l'objet classe Carte et d'une instance de Carte. Carte est un objet classe ; son type est type. L'objet carte1 est une instance de Carte, donc son type est Carte. Pour économiser l'espace, je n'ai pas dessiné le contenu de noms_couleurs et noms_valeurs.
18-3. Comparer des cartes▲
Pour les types internes, il existe des opérateurs relationnels (<, >, ==, etc.) qui comparent des valeurs et déterminent si l'un est supérieur, inférieur ou égal à un autre. Pour les types définis par le programmeur, nous pouvons remplacer le comportement des opérateurs internes en fournissant une méthode nommée __lt__, qui signifie less than, « inférieur à ».
__lt__ prend deux paramètres, self et other, et renvoie True si self est strictement inférieur à other.
L'ordre correct des cartes n'est pas évident. Par exemple, qu'est-ce qui est mieux, le 3 de trèfle ou le 2 de carreau ? L'une a une valeur plus élevée, mais l'autre a une couleur plus élevée. Afin de comparer les cartes, vous devez décider si la valeur ou la couleur est plus importante.
La réponse pourrait dépendre du jeu auquel vous jouez, mais pour ne pas compliquer les choses, nous faisons le choix arbitraire que c'est la couleur qui primera, donc tous les piques surclassent tous les carreaux, et ainsi de suite.
Une fois cette décision prise, nous pouvons écrire __lt__ :
# à l'intérieur de la classe Carte :
def
__lt__
(
self, other):
# vérifier les couleurs
if
self.couleur <
other.couleur: return
True
if
self.couleur >
other.couleur: return
False
# les couleurs sont identiques... vérifier les valeurs
return
self.valeur <
other.valeur
Vous pouvez réécrire cela d'une façon plus concise, en utilisant la comparaison de tuple :
# à l'intérieur de la classe Carte :
def
__lt__
(
self, other):
t1 =
self.couleur, self.valeur
t2 =
other.couleur, other.valeur
return
t1 <
t2
À titre d'exercice, écrivez une méthode __lt__ pour des objets de type Temps. Vous pouvez utiliser la comparaison de tuple, mais vous pourriez aussi envisager la comparaison des entiers.
18-4. Paquets de cartes▲
Maintenant que nous avons les cartes, la prochaine étape est de définir les Paquets de cartes. Comme un paquet est composé de cartes, il est naturel que chaque Paquet contienne comme attribut une liste de cartes.
Ce qui suit est une définition de classe pour Paquet. La méthode init crée l'attribut cartes et génère l'ensemble standard de cinquante-deux cartes :
class
Paquet:
def
__init__
(
self):
self.cartes =
[]
for
couleur in
range(
4
):
for
valeur in
range(
1
, 14
):
carte =
Card
(
couleur, valeur)
self.cartes.append
(
carte)
La meilleure façon de constituer le paquet est avec une boucle imbriquée. La boucle externe énumère les couleurs de 0 à 3. La boucle interne énumère les valeurs de 1 à 13. Chaque itération crée une nouvelle carte ayant la couleur et la valeur courantes, et l'ajoute à self.cartes.
18-5. Afficher le paquet▲
Voici une méthode __str__ pour Paquet :
# à l'intérieur de la classe Paquet :
def
__str__
(
self):
res =
[]
for
carte in
self.cartes:
res.append
(
str(
carte))
return
'
\n
'
.join
(
res)
Cette méthode montre un moyen efficace d'accumuler une longue chaîne de caractères : en construisant une liste de chaînes de caractères, puis en utilisant la méthode de chaîne de caractères join. La fonction interne str invoque la méthode __str__ sur chaque carte et renvoie sa représentation sous forme de chaîne de caractères.
Comme nous invoquons join sur un caractère de fin de ligne, les cartes sont séparées par des caractères de fin de ligne. Voici à quoi ressemble le résultat :
>>>
paquet =
Paquet
(
)
>>>
print
(
paquet)
as
de trèfle
2
de trèfle
3
de trèfle
...
10
de pique
valet de pique
dame de pique
roi de pique
Même si le résultat apparaît sur 52 lignes, c'est une longue chaîne qui contient des caractères de fin de ligne.
18-6. Ajouter, enlever, mélanger et trier▲
Pour distribuer des cartes, nous voudrions une méthode qui enlève une carte du paquet et la renvoie. La méthode de liste pop offre un moyen pratique de le faire :
# à l'intérieur de la classe Paquet :
def
pop_carte
(
self):
return
self.cartes.pop
(
)
Comme pop retire la dernière carte dans la liste, nous distribuons les cartes à partir de la fin du paquet.
Pour ajouter une carte, nous pouvons utiliser la méthode de liste append :
# à l'intérieur de la classe Paquet :
def
ajouter_carte
(
self, carte):
self.cartes.append
(
carte)
Une méthode comme celle-ci, qui utilise une autre méthode sans faire beaucoup de travail s'appelle parfois un placage. La métaphore vient du travail en bois, où un placage est une mince couche de bois d'essence noble collée à la surface d'une pièce en bois moins cher, pour améliorer l'apparence.
Dans ce cas, ajouter_carte est une méthode « mince » qui exprime une opération de liste en termes appropriés pour les paquets. Elle améliore l'apparence, ou l'interface, de la mise en œuvre.
Nous pouvons également écrire une méthode de Paquet nommée battre en utilisant la fonction shuffle du module random :
# à l'intérieur de la classe Paquet :
def
battre
(
self):
random.shuffle
(
self.cartes)
N'oubliez pas d'importer random.
À titre d'exercice, écrivez une méthode de Paquet appelée trier, qui utilise la méthode de liste sort pour trier les cartes d'un Paquet. La méthode trier utilise la méthode __lt__ que nous avons définie pour déterminer l'ordre.
18-7. Héritage▲
L'héritage est la capacité de définir une nouvelle classe qui est une version modifiée d'une classe existante. À titre d'exemple, disons que nous voulons une classe pour représenter une « main », c'est-à-dire les cartes détenues par un seul joueur. Une main est semblable à un paquet : les deux sont constitués d'une collection de cartes, et les deux nécessitent des opérations comme l'ajout et le retrait de cartes.
En même temps, une main est différente d'un paquet ; il existe des opérations que nous voulons pour les « mains » qui n'ont pas de sens pour un paquet. Par exemple, au poker, nous pourrions comparer deux mains pour voir qui gagne. Au bridge, nous pourrions calculer le nombre de points d'une main afin de faire une enchère.
Cette relation entre classes - similaires, mais différentes - se prête bien à l'héritage. Pour définir une nouvelle classe qui hérite d'une classe existante en Python, vous mettez le nom de la classe existante entre parenthèses :
class
Main
(
Paquet):
"""Représente une main au jeu de cartes."""
Cette définition indique que Main hérite de Paquet ; cela signifie que nous pouvons utiliser des méthodes comme pop_carte et ajouter_carte tant pour les Mains que pour les Paquets.
Lorsqu'une nouvelle classe hérite d'une classe existante, la classe existante est appelée classe mère ou classe parente et la nouvelle classe est appelée classe fille ou classe enfant.
Dans cet exemple, Main hérite __init__ de Paquet, mais celle-ci ne fait pas vraiment ce que nous voulons : au lieu d'alimenter la main avec 52 nouvelles cartes, la méthode init pour Mains doit initialiser cartes à une liste vide.
Si nous fournissons une méthode d'initialisation à la classe Main, elle remplace celle de la classe Paquet :
# à l'intérieur de la classe Main :
def
__init__
(
self, etiquette =
''
):
self.cartes =
[]
self.etiquette =
etiquette
Lorsque vous créez une Main, Python appelle cette méthode init, pas celle de Paquet.
>>>
main =
Main
(
'nouvelle main'
)
>>>
main.cartes
[]
>>>
main.etiquette
'nouvelle main'
Les autres méthodes sont héritées de Paquet, donc nous pouvons utiliser pop_carte et ajouter_carte pour distribuer une carte :
>>>
paquet =
Paquet
(
)
>>>
carte =
paquet.pop_carte
(
)
>>>
main.add_carte
(
carte)
>>>
print
(
main)
roi de pique
Une prochaine étape naturelle consiste à encapsuler ce code dans une méthode appelée deplacer_cartes :
# à l'intérieur de la classe Paquet :
def
deplacer_cartes
(
self, main, nombre):
for
i in
range(
nombre):
main.ajouter_carte
(
self.pop_carte
(
))
deplacer_cartes prend deux arguments, un objet Main et le nombre de cartes à distribuer. Elle modifie tant self (le paquet) que main, et renvoie None.
Dans certains jeux, les cartes sont déplacées d'une main à l'autre, ou remises d'une main vers le paquet. Vous pouvez utiliser deplacer_cartes pour les deux opérations : self peut être soit un Paquet, soit une Main, et main, malgré le nom, peut aussi être un Paquet.
L'héritage est une fonctionnalité utile. Certains programmes qui seraient répétitifs sans héritage peuvent être écrits plus élégamment en l'utilisant. L'héritage peut faciliter la réutilisation du code, puisque vous pouvez personnaliser le comportement des classes parentes sans devoir les modifier. Dans certains cas, la structure de l'héritage reflète la structure naturelle du problème, ce qui rend la conception plus facile à comprendre.
D'un autre côté, l'héritage peut rendre les programmes difficiles à lire. Quand une méthode est invoquée, parfois on ne sait pas trop où trouver sa définition. Le code en question peut être réparti sur plusieurs modules. De plus, beaucoup de choses qui peuvent être faites en utilisant l'héritage peuvent être faites aussi bien ou mieux sans lui.
18-8. Diagrammes de classes▲
Jusqu'à présent, nous avons vu des diagrammes de pile, qui montrent l'état d'un programme, et les diagrammes d'objets, qui montrent les attributs d'un objet et leurs valeurs. Ces diagrammes représentent un instantané dans l'exécution d'un programme, donc ils changent pendant l'exécution du programme.
Ils sont aussi très détaillés ; à certaines fins, trop détaillés. Un diagramme de classe est une représentation plus abstraite de la structure d'un programme. Au lieu de montrer des objets individuels, il montre les classes et les relations entre elles.
Il existe plusieurs types de relations entre les classes :
- les objets d'une classe peuvent contenir des références vers des objets d'une autre classe. Par exemple, chaque Rectangle contient une référence vers un Point, et chaque Paquet contient des références vers plusieurs Cartes. Ce type de relation est appelé HAS-A, « a-un(e) », comme dans « un Rectangle a un Point » ;
- une classe peut hériter d'une autre. Cette relation est appelée IS-A, « est-un(e) », comme dans « une Main est une sorte de Paquet. » ;
- une classe peut dépendre d'une autre dans le sens où les objets d'une classe prennent comme paramètres des objets de la seconde classe, ou utilisent des objets de la seconde classe dans le cadre d'un calcul. Ce type de relation est appelée une dépendance.
Un diagramme de classes est une représentation graphique de ces relations. Par exemple, la figure 18.2 montre les relations entre Carte, Paquet et Main.
La flèche à pointe triangulaire creuse représente une relation IS-A ; dans ce cas, elle indique que Main hérite de Paquet.
La flèche à pointe normale représente une relation HAS-A ; dans ce cas, un Paquet a des références vers des objets Carte.
L'astérisque (*) près de la pointe de la flèche est une multiplicité ou cardinalité ; il indique combien de Cartes a un Paquet. Une multiplicité peut être un simple nombre, comme 52, une plage de valeurs, comme 5..7 ou une étoile, qui indique qu'un Paquet peut avoir un nombre quelconque de Cartes.
Il n'y a aucune dépendance dans ce schéma. Elles seraient normalement représentées par une flèche en pointillé. Ou s'il y a beaucoup de dépendances, elles sont parfois omises.
Un schéma plus détaillé pourrait montrer qu'un Paquet contient en fait une liste de Cartes, mais les types internes comme les listes et les dictionnaires ne sont généralement pas inclus dans les diagrammes de classes.
18-9. Débogage▲
L'héritage peut rendre le débogage difficile parce que lorsque vous appelez une méthode sur un objet, il peut être difficile de comprendre quelle méthode sera invoquée.
Supposons que vous écriviez une fonction qui travaille avec des objets Main. Vous souhaiterez qu'elle fonctionne avec toutes sortes de Mains, comme MainsAuPoker, MainsAuBridge, etc. Si vous invoquez une méthode comme battre, vous pourriez obtenir celle définie dans Paquet, mais si l'une des sous-classes remplace cette méthode, vous obtiendrez la nouvelle version. Ce comportement est généralement une bonne chose, mais il peut devenir déroutant.
Chaque fois que vous n'êtes pas sûr du flux d'exécution de votre programme, la solution la plus simple consiste à ajouter des instructions d'affichage au début des méthodes pertinentes. Si Paquet.battre affiche un message qui dit quelque chose comme méthode Paquet.battre en cours d'exécution, alors le programme retrace au fur et à mesure le flux de son exécution.
Une autre possibilité serait d'utiliser cette fonction, qui prend un objet et un nom de méthode (sous forme de chaîne de caractères) et renvoie la classe qui fournit la définition de la méthode :
def
trouver_classe_qui_definit
(
objet, nom_methode):
for
ty in
type(
objet).mro
(
):
if
nom_methode in
ty.__dict__
:
return
ty
Voici un exemple :
>>>
main =
Main
(
)
>>>
trouver_classe_qui_definit
(
main, 'battre'
)
<
class
'__main__.Paquet'
>
Donc, la méthode battre pour cette main est celle définie dans Paquet.
trouver_classe_qui_definit utilise la méthode mro pour obtenir la liste des objets classe (types) où rechercher des méthodes. « MRO » signifie method resolution order « ordre de résolution des méthodes », qui est la séquence de classes que Python recherche pour « résoudre » un nom de méthode.
Voici une suggestion de conception : lorsque vous substituez une méthode, l'interface de la nouvelle méthode devrait être identique à l'ancienne. Elle devrait prendre les mêmes paramètres, retourner le même type et obéir aux mêmes préconditions et postconditions. Si vous suivez cette règle, vous découvrirez que toute fonction conçue pour travailler avec une instance d'une classe mère, comme un Paquet, va fonctionner également avec des instances des classes filles, comme Main et MainAuPoker.
Si vous ne respectez pas cette règle, qui s'appelle le « principe de substitution de Liskov », votre code va s'effondrer comme (sans jeu de mots) un château de cartes.
18-10. Encapsulation de données▲
Les chapitres précédents montrent un modèle de développement que nous pourrions appeler « conception orientée objet ». Nous avons identifié les objets dont nous avions besoin - comme Point, Rectangle et Temps - et défini des classes pour les représenter. Dans chaque cas, il y a une correspondance évidente entre l'objet et une entité dans le monde réel (ou du moins un monde mathématique).
Mais, parfois, il est moins évident de déterminer quels sont les objets dont vous avez besoin et comment ils doivent interagir. Dans ce cas, vous avez besoin d'un modèle de développement différent. De la même manière que nous avons découvert des interfaces de fonction par encapsulation et généralisation, nous pouvons découvrir des interfaces de classe par encapsulation de données.
L'analyse de Markov, de la section 13.8Analyse de Markov, fournit un bon exemple. Si vous téléchargez mon code à partir de l'adresse markov.py, vous verrez qu'il utilise deux variables globales - suffix_map et prefix - qui sont lues et écrites par plusieurs fonctions.
suffix_map =
{}
prefix =
(
)
Comme ces variables sont globales, nous ne pouvons exécuter qu'une seule analyse à la fois. Si nous lisons deux textes, leurs préfixes et suffixes seront ajoutés aux mêmes structures de données (ce qui peut conduire à la génération de textes quelque peu éclectiques).
Pour exécuter plusieurs analyses et les garder séparées, nous pouvons encapsuler l'état de chaque analyse dans un objet. Voilà à quoi cela ressemble :
class
Markov:
def
__init__
(
self):
self.suffix_map =
{}
self.prefix =
(
)
Ensuite, nous transformons les fonctions en méthodes. Par exemple, voici la méthode process_word :
def
process_word
(
self, word, order =
2
):
if
len(
self.prefix) <
order:
self.prefix +=
(
word,)
return
try
:
self.suffix_map[self.prefix].append
(
word)
except
KeyError
:
# s'il n'existe aucune entrée pour ce préfixe, en créer une
self.suffix_map[self.prefix] =
[word]
self.prefix =
shift
(
self.prefix, word)
Transformer un programme de cette façon - modifier la conception sans modifier le comportement - est un autre exemple de réusinage (voir la section 4.7Réusinage).
Cet exemple suggère un modèle de développement pour la conception des objets et méthodes :
- Commencez par écrire des fonctions qui lisent et écrivent des variables globales (si nécessaire) ;
- Une fois que votre programme fonctionne, recherchez des associations entre les variables globales et les fonctions qui les utilisent ;
- Encapsulez les variables apparentées dans des attributs d'un objet ;
- Transformez les fonctions associées en méthodes de la nouvelle classe.
À titre d'exercice, téléchargez mon code Markov à l'adresse markov.py et suivez les étapes décrites ci-dessus pour encapsuler les variables globales comme attributs d'une nouvelle classe appelée Markov. Solution : Markov.py (remarquez la M capitale).
18-11. Glossaire▲
- encoder : représenter un ensemble de valeurs en utilisant un autre ensemble de valeurs en construisant une correspondance entre eux.
- attribut de classe : un attribut associé à un objet classe. Les attributs de classe sont définis dans une définition de classe, mais en dehors de toute méthode.
- attribut d'instance : un attribut associé à une instance d'une classe.
- placage : un procédé ou une fonction qui fournit une interface à une autre fonction sans faire beaucoup de calculs.
- héritage : la possibilité de définir une nouvelle classe qui est une version modifiée d'une classe définie préalablement.
- classe mère : la classe dont hérite une classe fille ou enfant.
- classe fille : une nouvelle classe créée en héritant d'une classe existante ; également appelée » classe enfant » ou « sous-classe ».
- relation IS-A : une relation entre une classe fille et sa classe mère.
- relation HAS-A : une relation entre deux classes dans laquelle les instances d'une classe contiennent des références aux instances de l'autre.
- dépendance : une relation entre deux classes où les instances d'une classe utilisent les instances de l'autre classe, mais ne les stockent pas comme attributs.
- diagramme de classes : un diagramme qui montre les classes d'un programme et les relations entre elles.
- multiplicité ou cardinalité : une notation dans un diagramme de classes qui montre, pour une relation HAS-A, combien il y a de références à des instances d'une autre classe.
- encapsulation de données : un modèle de développement d'un programme qui implique au départ un prototype utilisant des variables globales et une version finale qui transforme les variables globales en attributs d'instance.
18-12. Exercices▲
Exercice 1
Pour le programme suivant, dessinez un diagramme de classes UML qui montre ces classes et les relations entre elles.
class
PingPongParent:
pass
class
Ping
(
PingPongParent):
def
__init__
(
self, pong):
self.pong =
pong
class
Pong
(
PingPongParent):
def
__init__
(
self, pings=
None
):
if
pings is
None
:
self.pings =
[]
else
:
self.pings =
pings
def
add_ping
(
self, ping):
self.pings.append
(
ping)
pong =
Pong
(
)
ping =
Ping
(
pong)
pong.add_ping
(
ping)
Exercice 2
Écrivez une méthode de Paquet appelée distribue_mains qui prend deux paramètres, le nombre de mains à distribuer et le nombre de cartes par main. Elle doit créer le nombre voulu d'objets Main, distribuer le nombre approprié de cartes par main et renvoyer une liste de Mains.
La liste suivante reprend les combinaisons possibles au poker, par ordre croissant de la valeur et ordre décroissant de la probabilité :
- Paire : deux cartes ayant la même valeur ;
- Double paire : deux paires de cartes ayant la même valeur ;
- Brelan : trois cartes ayant la même valeur ;
- Suite ou quinte : cinq cartes ayant les valeurs dans l'ordre (l'as peut être en première ou en dernière position, donc as-2-3-4-5 ou 10-valet-dame-roi-as sont des suites valides, mais dame-roi-as-2-3 ne l'est pas.) ;
- Couleur : cinq cartes ayant la même couleur ;
- Full ou main pleine : trois cartes d'une valeur, deux cartes d'une autre ;
- Carré : quatre cartes de même valeur ;
- Quinte flush ou suite couleur : cinq cartes dans l'ordre (tel que défini ci-dessus) ayant la même couleur.
Le but de ces exercices est d'estimer la probabilité de tirer ces différentes combinaisons.
-
Téléchargez les fichiers suivants à l'adresse https://allen-downey.developpez.com/livres/python/pensez-python/fichiers/ :
- Card.py : une version complète des classes Carte, Paquet et Main de ce chapitre.
- PokerHand.py : une implémentation incomplète d'une classe qui représente une main au poker, et un code qui la teste.
- Si vous exécutez PokerHand.py , elle distribue sept mains de 7 cartes chacune et les contrôle pour voir si l'une d'elles contient une quinte flush. Lisez attentivement ce code avant de poursuivre.
- Ajoutez à PokerHand.py des méthodes nommées has_pair , has_twopair , etc. qui renvoient True ou False selon que la main satisfait ou non aux critères pertinents. Votre code devrait fonctionner correctement pour des « mains » qui contiennent un nombre quelconque de cartes (bien que 5 et 7 soient les formats les plus courants).
- Écrivez une méthode nommée classify , qui calcule la combinaison de la plus haute valeur pour une main, et définit l'attribut label en conséquence. Par exemple, une main de 7 cartes peut contenir une quinte flush et une paire ; elle doit être étiquetée « quinte flush ».
- Lorsque vous êtes convaincu que vos méthodes de classification fonctionnent, l'étape suivante consiste à estimer les probabilités des différentes mains. Écrivez une fonction dans PokerHand.py qui bat un paquet de cartes, le divise en mains, classifie les mains et compte le nombre de fois où les différentes combinaisons apparaissent.
- Affichez une table des combinaisons et leurs probabilités. Exécutez votre programme avec des nombres de plus en plus grands de mains jusqu'à ce que les valeurs de sortie convergent vers un degré raisonnable de précision. Comparez vos résultats aux valeurs théoriques du tableau ci-après :
Solution : PokerHandSoln.py.
Probabilités en % des différentes combinaisons du poker (mains de 5 et 7 cartes) |
||
---|---|---|
Combinaison |
Main de 5 cartes |
Main de 7 cartes |
Quinte flush ou suite couleur |
0,00154 % |
0.0311 % |
Carré |
0,0240 % |
0,168 % |
Full ou main pleine |
0,144 % |
2,60 % |
Couleur |
0,196 % |
3,03 % |
Suite ou quinte |
0,392 % |
4,62 % |
Brelan |
2,11 % |
4,83 % |
Double paire |
4,75 % |
23,5 % |
Paire |
42,3 % |
43,8 % |
Source : https://en.wikipedia.org/wiki/List_of_poker_hands .