Des objets en Python 3

Et des questions pour les zesteux !

a marqué ce sujet comme résolu.

Bonjour, bonsoir, bon matin, :)

Je suis actuellement en train d'apprendre à programmer en Python, avec python 3.4 et j'en suis rendu à la notion de classes, d'objets, de méthodes et tout ce qui peut aller avec.

Avant de continuer, j'aimerais savoir si ma façon de gérer les classes est considérable comme "juste" par un programmeur expérimenté, pour éviter de finir par écrire du code que moi seul comprendrait. Si vous avez des remarques à propos de quoi que ce soit d'autre (gestions des fonctions, mon niveau plus que moyen en anglais), faites vous plaisir :) , je suis là pour écouter ce que je peux apprendre.

L'exercice
Réaliser le jeu du "Plus Ou Moins" dans une version utilisant les notions de POO que vous venez de découvrir.
Conditions de validation :

  • Les règles doivent être affichées au départ.
  • La saisie doit être sécurisée autant que cela est possible selon vous (si l'utilisateur saisi "\n" ou "3.141592" le jeu lui signale que ce n'est pas un nombre valide), pensez aux instructions try/except/else.
  • Il doit y avoir au moins deux niveaux de difficultés, avec un choix pour l'utilisateur avant de jouer.

Voici ce que j'ai écrit, après bien des déboires avec la POO, que je comprends assez mal je pense.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# !/usr/bin/env python3
# -*-coding:utf-8-*
# LessOrMorePython.py

"""
Less or More game, made by Poliorcetics.

First version has been made with the C programming language, but this version
is made with Python 3.4.3 because I want learn it.

Copyright (c) Poliorcetics, 2015. All rights reserved.
"""

# Import randrange to use it in random initialization
import random as ran

###############################################################################
# 'Infos' class
###############################################################################

class Infos(object):
    """
    'Infos' class
    =============

    Contains some informations about the game and the author

    Variables
    ---------

    [String]
     - author : contains informations about the author
     - version : contains the version number
     - copyright : contains informations about the copyright

    [Tuple]
     - all_infos : contains all the previous informations in a tuple
    """

    author = "Poliorcetics, alias Alexis B."
    version = "0.2.1"
    copyright = "Copyright © Poliorcetics, 2015. All rights reserved."
    all_infos = (author, version, copyright)


###############################################################################
# 'Game' class, main class.
###############################################################################

class Game(object):
    """
    'Game' class
    ============

    Contains all of what is basically needed by the game to run.

    Variables
    ---------

    [String]
     - player_name : deprecated right now (line is commented)

    [Tuple]
     - infos : contains informations about the author, the version and the copyright.
                See more by looking at the documentation of the Infos class.

    Functions
    ---------

    The ‘self,’ is omitted in all functions, but it's the first argument everywhere
    (if you use the function outside the class, it doesn't need to be write)

    - introduceGame() : just here to display the title of the game
    - displayRules() : displays the rules
    - chooseDifficulty() : return the choice of the player for the difficulty level
    - computeSearchedNumber(dif, top): create the mysterious number and return it
    - defineTopOfTheRange(dif) : return the top of the range, corresponding to the selected difficulty
    - isLessOrMore(t_nb, s_nb) : displays if the typed number is higher or lower than the searched number
    - searchNumber(s_nb, top) : let the player searching what is the searched number
    - ending(inf) : displays some informations about author, version and copyright

    All functions have their own documentation, look at it for more informations
    """

    # player_name = "Default"
    infos = Infos.all_infos

    def introduceGame(self):
        """Introduces the game with a simple shell display"""

        print("         \n---------------------------")
        print("         >>> Less Or More Game ! <<<")
        print("         ---------------------------\n")


    def displayRules(self):
        """
        Displays the rules of the Less Or More Game. No arg needed, no return.
        """

        print("-- Rules --");
        print("1. The game will create a mysterious number.")
        print("2. The game will ask you to enter a number.")
        print("3. This number MUST BE included in the range \
corresponding to the selected difficulty.")
        print("4. This number MUST BE an integer, like 42 and unlike 3.1415.")
        print("5. If the number you type is in range and is higher \
than the searched number, the game will say \"That's less !\", else it will \
say \"That's more !\".")
        print("6. You have to find the mysterious number with a minimum \
number of tests.")

    def chooseDifficulty(self):
        """
        Displays the difficulty options and returns the player's choice.

        Return 1, 2 or 3.
        """

        dif = 0

        # Displays possible choices for the difficulty
        print("\n-- Difficulty --");
        print("1 : Mysterious number is in range [1;100]")
        print("2 : Mysterious number is in range [1;1000]")
        print("3 : Mysterious number is in range [1;10000]\n")
        print(">> To choose a difficulty, enter 1, 2 or 3.")

        # Asks the user to know what he want
        while not(0 < dif < 4):
            dif = input("Please choose a difficulty : ")
            try:
                dif = int(dif)
            except ValueError:
                dif = 0
            else:
                dif = int(dif)

        return dif

    def computeSearchedNumber(self, dif, top):
        """
        Return 'searched_number' (INT), that the player will have to find to win

        - 'dif' : INT, represents the choosen difficulty
        - 'top' : INT, represents the maximum possible value for the searched number
        """

        # Game displays that it is computing for 'searched_number'
        print("\nCalculation of the mysterious number ...")

        if dif == 1:
            # +1 : to make the true range
            searched_number = ran.randrange(top)+1
        elif dif == 2:
            searched_number = ran.randrange(top)+1
        else: # dif == 3
            searched_number = ran.randrange(top)+1

        # Game lets the user know that it finished to compute searched number
        print("Finished ! \n")

        # Uncomment to get the answer before playing (use it only for tests please)
        print(searched_number, "\n")

        return searched_number

    def defineTopOfTheRange(self, dif):
        """
        Return 'top_of_the_range' (INT), the maximum possible value for the searched number

        - 'dif' : INT, represents the choosen difficulty
        """

        if dif == 1:
            top_of_the_range = 100
        elif dif == 2:
            top_of_the_range = 1000
        else:
            top_of_the_range = 10000

        return top_of_the_range

    def isLessOrMore(self, t_nb, s_nb):
        """
        Displays if the typed number 't_nb' is lower or higher than the searched number 's_nb'

        Returns False if t_nb != s_nb, else True
        """

        if t_nb > s_nb:
            print("\nThat's less !\n")
            return False
        elif t_nb < s_nb:
            print("\nThat's more !\n")
            return False
        else: # t_nb == s_nb
            print("\nCongratulations ! You found the searched number !\n")
            return True

    def searchNumber(self, s_nb, top):
        """
        Return the number of attempts 'nb_of_tests' the player made to found the searched number 's_nb'

        - 's_nb' : INT, the number the player have to find
        - 'top' : INT, represents the maximum possible value for the searched number (and the typed number)

        While the player does not enter the searched number, this function will keep going, asking the player again and again.
        """

        nb_of_tests = 0
        t_nb = 0

        # Begin the real game : the player will have to enter a number and
        # to find the searched number
        while t_nb != s_nb:

            # Player enters an integer that MUST BE included in the choosen range
            while not(0 < t_nb <= top) or not(position):

                nb_of_tests += 1 # every attempt is counted
                t_nb = input("Please, enter an integer in range [1:{}] ".format(top))

                # Checks if the typed number is in range and an integer
                try:
                    t_nb = int(t_nb)
                except (ValueError, TypeError):
                    pass
                    t_nb = 0
                    print("An integer in the range please.")
                else:
                    t_nb = int(t_nb)
                    if (0 < t_nb <= top):
                        break
                    else:
                        print("An integer in the range please.")

            # Displays to the player if he is lower or higher compare to the searched number
            position = Game.isLessOrMore(self, t_nb, s_nb)

        return nb_of_tests

    def ending(self, inf):
        """
        Displays the informations about the author, the version and the copyright.

        - 'inf' : TUPLE, contains the informations. See the 'Infos' class to learn more about it.
        """

        print("Made with Python 3.4.3 and <3 by {}.".format(inf[0]))
        print("Version {}, {cop}\n".format(inf[1], cop=inf[2]))


# If this file is lauched directly, the game begin
if __name__ == "__main__":

    game = Game()

    game.introduceGame()
    game.displayRules()

    difficultyLevel = game.chooseDifficulty()
    top_of_the_range = game.defineTopOfTheRange(difficultyLevel)
    searched_number = game.computeSearchedNumber(difficultyLevel, top_of_the_range)

    game.searchNumber(searched_number, top_of_the_range)

    game.ending(Game.infos)

Si j'ai oublié de préciser une information, n'hésitez pas à demander :) .

+0 -0
  1. Trop de print tuent le print, mais bon c'est de l'affichage, bref, regarde du côté des triple quotes pour éviter le nombre de print.
  2. Ligne 136, le bloc else est inutile !
  3. Ligne 141, je ne vois pas l'intérêt du paramètre dif, j'ai l'impression que ça fait double emploi par rapport à la fonction ligne 168.
  4. Après ligne 200, j'ai lu en diagonale, quelques petits détails peuvent rendre le code plus propre conceptuellement parlant, mais c'est un détail…

En règle général, pour un jeu comme celui-ci les print devrait être une syntaxe très limitée, pour moi c'est trop ici !

Pour le reste, c'est lisible, tu documentes bien, mais à vrai dire, ça ne m'a servi que pour des variables que j'ai trouvées mal choisies au niveau du nom (je pense à dif et top).

Donc pour quelqu'un qui débute en POO, syntaxiquement parlant c'est bien, voir très bien… Mais conceptuellement parlant, un peu de réflexion supplémentaire aurait pu alléger le nombre de lignes de code.

Bonsoir,

Le code est très propre, mais trop verbeux à mon goût. Quelques remarques en vrac :

  • lignes 152 à 158 : WTF ?
  • ligne 239, ce genre de code est à éviter, on écrit plutôt self.isLessOrMore(t_nb, s_nb)
  • lignes 243 et suivantes : faire dépendre la méthode d'une classe du contenu d'une autre classe est clairement une mauvais pratique OO, la responsabilité de savoir comment s'afficher devrait être déléguée à la classe Infos (typiquement avec la méthode spéciale __repr__. C'est également incorrect pour une autre raison: inf est en réalité un attribut de Game, alors pourquoi le passer en argument ?
  • tes méthodes se trimballent des arguments un peu partout, parce que tu n'utilise quasiment pas d'attributs pour stocker tes variables d'état: c'est clairement la mauvaise manière de procéder. Le souci est particulièrement visible lignes 262-266 ;
  • je ne vois pas pourquoi tu as dédoublé chooseDifficulty et defineTopOfTheRange, sinon à cause du point précédent.
+0 -0

Pour illustrer mon propos, voici une version où j'ai respecté ton organisation tout en corrigeant les points qui me gênaient (inutile de regarder tout de suite, tu devrais essayer de reprendre ton code avant).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# !/usr/bin/env python3
# -*-coding:utf-8-*

import random as ran


class Game:
    __author__ = "Poliorcetics, alias Alexis B."
    __version__ = "0.2.1"
    __copyright__ = "Copyright © Poliorcetics, 2015. All rights reserved."

    _maxis = [100, 1000, 10000]

    def introduceGame(self):
        print("         \n---------------------------")
        print("         >>> Less Or More Game ! <<<")
        print("         ---------------------------\n")

    def displayRules(self):
        print("-- Rules --");
        print("1. The game will create a mysterious number.")
        print("2. The game will ask you to enter a number.")
        print("3. You have to find the mysterious number with a minimum \
number of tests.")

    def chooseDifficulty(self):
        # Displays possible choices for the difficulty
        print("\n-- Difficulty --");
        for i, maxi in enumerate(self._maxis):
            print("%d : Mysterious number is in range [1;%d]" % (i + 1, maxi))
        print()

        level = None
        # Asks the user to know what he want
        while level not in range(len(self._maxis)):
            level = input(">> Please choose a difficulty : ")
            try:
                level = int(level) - 1
            except ValueError:
                pass

        self.top_on_the_range = self._maxis[level]
        self.searched_number = ran.randint(1, self.top_on_the_range)

    def isLessOrMore(self, t_nb):
        if t_nb > self.searched_number:
            print("\tThat's less !")
        elif t_nb < self.searched_number:
            print("\tThat's more !")
        else: # t_nb == self.searched_number
            print("\tCongratulations ! You found the searched number !")

    def searchNumber(self):
        s_nb = self.searched_number
        top = self.top_on_the_range
        nb_of_tests = 0
        t_nb = 0

        # Begin the real game : the player will have to enter a number and
        # to find the searched number
        while t_nb != s_nb:

            # Player enters an integer that MUST BE included in the choosen range
            while True:

                nb_of_tests += 1 # every attempt is counted
                t_nb = input(">> Please, enter an integer in range [1:{}] ".format(top))

                # Checks if the typed number is in range and an integer
                try:
                    t_nb = int(t_nb)
                except (ValueError, TypeError):
                    pass
                else:
                    if 0 < t_nb <= top:
                        break
                    else:
                        print("An integer in the range please.")

            # Displays to the player if he is lower or higher compare to the searched number
            self.isLessOrMore(t_nb)

        print("Finished in %d attempts" % nb_of_tests)

    def ending(self):
        print("Made with Python 3.4.3 and <3 by {}.".format(self.__author__))
        print("Version {}, {cop}\n".format(self.__version__, cop=self.__copyright__))


# If this file is lauched directly, the game begin
if __name__ == "__main__":
    game = Game()
    game.introduceGame()
    game.displayRules()
    game.chooseDifficulty()
    game.searchNumber()
    game.ending()

J'ai fais un code pour montrer comment je pense, si je fais de la POO ou même sans POO, en admettant connaître l'algorithme évidemment !

Tout d'abord le code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from random import randint

class Game:
    def __init__(self, level):
        self.level = level

    def __askNumber(self):
        while True:
            value = input("Entrer un nombre : ")
            if value.isdigit():
                break
        return int(value)
    
    def __askPlay(self):
        result = input("Voulez-vous rejouer ? [yes/no] : ").lower()
        if result != 'yes':
            self.game = False
    
    def __displayResult(self, n):
        if n < self.number:
            res = "Plus"
        elif n > self.number:
            res = "Moins"
        else:
            res = "Gagné !"
            
        print(res)
        
    def play(self):
        self.game = True
        while self.game:
            self.number = randint(0, self.level+1)
            numberUser = None
            while numberUser != self.number:
                numberUser = self.__askNumber()
                self.__displayResult(numberUser)
            self.__askPlay()

game = Game(100)
game.play()

Dans le code ci-dessus, je suis parti de la méthode play, et selon un algorithme simple, j'ai inventé des noms/verbes pour résoudre cet algorithme, ces noms/verbes de mes méthodes sont

  1. __askNumber
  2. __askPlay
  3. __displayresult

Puis je les ai créé tranquillement dans ma classe, sachant que se sont des méthodes internes, et dont le seul intérêt est de faire fonctionner la classe, je les ai mis en private.

Et vous comment pensez-vous un code niveau conception ?

+0 -0

Merci à vous tous, j'ai beaucoup appris :) !

Dans l'ordre de vos réponses :

  • Situphen : je n'avais même pas remarqué que j'avais tout mélangé. Je corrige ça tout de suite.

  • Fred1599, Yoch : j'ai pris notes de tout ce que vous avez dit, je vais essayer de corriger puis je reviendrais avec la nouvelle version. Je n'ai pas regardé précisément vos codes pour essayer de faire par moi-même, mais voir qu'on pouvait faire beaucoup plus court me suffit.

Et aussi merci pour le lien Fred, je ne savais pas que les méthodes pouvaient être privées en Python (faut vraiment que je lise la doc de temps à autre, ça m'éviterait de déranger pour rien).

+0 -0

Oui et non. Ce qui commence par _ est plutôt a considérer comme protected entre autre par ce qu'il n'y a rien qui est fait pour empecher son accès en dehors de la classe.

Par contre quand ça commence par __ là c'est plus subtile car le nom de la classe est injecté en interne sur le nom du paramètre ce qui le rend plus difficile d'accès de l'extérieure (mais pas impossible). Bref un double underscore, c'est une volonté forte du dev de privé l'accès à l'élément tandis qu'un simple c'est un "tu devrais pas" mais sans rien faire pour l'empecher.


edit: pour bien faire comprendre ce que je dis

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class Foo:
...     def __init__(self):
...         self._a = 42
...         self.__b = 1337
...     def print_a(self):
...         print(self._a)
...     def print_b(self):
...         print(self.__b)
... 
>>> f = Foo()
>>> f.print_a()
42
>>> f.print_b()
1337
>>> f._a
42
>>> f._b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute '_b'
>>> f._Foo__b
1337

Je définit donc une class ou le constructeur crée un élément préfixé avec un seul underscore et un autre avec deux. J'ai aussi définit deux méthodes qui utilisent ces valeurs. Les lignes 11 à 14 montrent bien que les methodes dans la classe n'ont aucun problème à accéder à la valeur. Sur les lignes 15 - 20 on voit par contre que l'élément avec un seul underscore est parfaitement accessible par contre celui avec deux ne l'est pas. La ligne 21 vous montre comment on peut y accéder malgré tout.

Donc attention, un double underscore, même si ce n'est pas un vrai privé (au sens Java du terme) c'est tout de même plus contraignant. Pour autant on l'utilise assez peu en général. La convention du simple suffit largement en général.

+0 -0

Par contre une remarque que personne n'a faite encore sur ce thread, c'est que créer une classe pour un jeu du plus ou moins, en Python, c'est le marteau pour écraser un moustique.

C'est bien pour l'exemple, pour s'entraîner à manier des classes, mais pour un programme aussi simple on préférera le faire en procédural pour alléger le code, et donc le rendre à la fois plus lisible et plus rapide à maintenir.

M'enfin ne te fais pas de bile. Visiblement tu débutes avec la POO en Python, tu as encore tout le temps devant toi pour apprendre à déterminer les cas où elle est utile et ceux où il est préférable de s'en passer. ;)

+0 -0

Hello nohar,

En effet, tu as tout à fait raison, mais je pense que le PO est au courant de cela, il voulait avoir une façon de procéder… Il est vrai que sur le net tu peux avoir tout et rien, et qu'il est difficile de voir le code sur lequel on peut prendre exemple.

Exact, je débute en POO (enfin je débute tout court pour être exact).

Je me doute bien que c'est ici assez violent d'utiliser la POO (à peu près autant que le fait que la moitié de ce que j'écris soit de la doc), mais comme tu le dis : c'est pour l'exemple. :)

À ce propos :

Le code suivant ne fonctionne pas chez moi, mais je ne comprends pas du tout pourquoi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# !/usr/bin/env python3
# -*-coding:utf-8-*

class Exemple():

    def __init__(self):

        test = "I'm  a test"

    def print_test(self):

        print(self.test)


exe = Exemple()
exe.print_test()

J'obtiens :

1
2
3
4
Traceback (most recent call last):
  File "<string>", line 29, in <module>
  File "<string>", line 26, in print_test
AttributeError: Exemple instance has no attribute 'test'

À l'inverse, le même code sans la méthode __init__ fonctionne très bien. Quelqu'un saurait-il m'expliquer pourquoi j'obtiens cela ?

+0 -0

Il te dit « Exemple n'a pas d'attribut "test" ». Et pour cause, test est une variable local créée dans __init__. Si tu veux créer un attribut, il faut faire :

1
2
3
class Exemple():
    def __init__(self):
        self.test = "I'm  a test"
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte