Une classe est en fait la définition d’un type. int
, str
ou encore list
sont des exemples de classes. User
en est une autre.
Une classe décrit la structure des objets du type qu’elle définit : quelles méthodes vont être applicables sur ces objets.
- La classe à Dallas
- Argumentons pour construire
- Comment veux-tu que je t'encapsule ?
- Tu aimes les glaces, canard ?
- TP : Forum, utilisateurs et messages
La classe à Dallas
On définit une classe à l’aide du mot-clef class
survolé plus tôt :
1 2 | class User: pass |
(l’instruction pass
sert ici à indiquer à Python que le corps de notre classe est vide)
Il est conseillé en Python de nommer sa classe en CamelCase, c’est à dire qu’un nom est composé d’une suite de mots dont la première lettre est une capitale. On préférera par exemple une classe MonNomDeClasse
que mon_nom_de_classe
.
Exception faite des types builtins qui sont couramment écrits en lettres minuscules.
On instancie une classe de la même manière qu’on appelle une fonction, en suffixant son nom d’une paire de parenthèses.
Cela est valable pour notre classe User
, mais aussi pour les autres classes évoquées plus haut.
1 2 3 4 5 6 7 8 | >>> User() <__main__.User object at 0x7fc28e538198> >>> int() 0 >>> str() '' >>> list() [] |
La classe User
est identique à celle du chapitre précédent, elle ne comporte aucune méthode.
Pour définir une méthode dans une classe, il suffit de procéder comme pour une définition de fonction, mais dans le corps de la classe en question.
1 2 3 | class User: def check_pwd(self, password): return self.password == password |
Notre nouvelle classe User
possède maintenant une méthode check_pwd
applicable sur tous ses objets.
1 2 3 4 5 6 7 8 | >>> john = User() >>> john.id = 1 >>> john.name = 'john' >>> john.password = '12345' >>> john.check_pwd('toto') False >>> john.check_pwd('12345') True |
Quel est ce self
reçu en premier paramètre par check_pwd
? Il s’agit simplement de l’objet sur lequel on applique la méthode, comme expliqué dans le chapitre précédent. Les autres paramètres de la méthode arrivent après.
La méthode étant définie au niveau de la classe, elle n’a que ce moyen pour savoir quel objet est utilisé.
C’est un comportement particulier de Python, mais retenez simplement qu’appeler john.check_pwd('12345')
équivaut à l’appel User.check_pwd(john, '12345')
. C’est pourquoi john
correspondra ici au paramère self
de notre méthode.
self
n’est pas un mot-clef du langage Python, le paramètre pourrait donc prendre n’importe quel autre nom. Mais il conservera toujours ce nom par convention.
Notez aussi, dans le corps de la méthode check_pwd
, que password
et self.password
sont bien deux valeurs distinctes : la première est le paramètre reçu par la méthode, tandis que la seconde est l’attribut de notre objet.
Argumentons pour construire
Nous avons vu qu’instancier une classe était semblable à un appel de fonction. Dans ce cas, comment passer des arguments à une classe, comme on le ferait pour une fonction ?
Il faut pour cela comprendre les bases du mécanisme d’instanciation de Python. Quand on appelle une classe, un nouvel objet de ce type est construit en mémoire, puis initialisé. Cette initialisation permet d’assigner des valeurs à ses attributs.
L’objet est initialisé à l’aide d’une méthode spéciale de sa classe, la méthode __init__
. Cette dernière recevra les arguments passés lors de l’instanciation.
1 2 3 4 5 6 7 8 | class User: def __init__(self, id, name, password): self.id = id self.name = name self.password = password def check_pwd(self, password): return self.password == password |
Nous retrouvons dans cette méthode le paramètre self
, qui est donc utilisé pour modifier les attributs de l’objet.
1 2 3 4 5 | >>> john = User(1, 'john', '12345') >>> john.check_pwd('toto') False >>> john.check_pwd('12345') True |
Comment veux-tu que je t'encapsule ?
Au commencement étaient les invariants
Les différents attributs de notre objet forment un état de cet objet, normalement stable. Ils sont en effet liés les uns aux autres, la modification d’un attribut pouvant avoir des conséquences sur un autre. Les invariants correspondent aux relations qui lient ces différents attributs.
Imaginons que nos objets User
soient dotés d’un attribut contenant une évaluation du mot de passe (savoir si ce mot de passe est assez sécurisé ou non), il doit alors être mis à jour chaque fois que nous modifions l’attribut password
d’un objet User
.
Dans le cas contraire, le mot de passe et l’évaluation ne seraient plus corrélés, et notre objet User
ne serait alors plus dans un état stable.
Il est donc important de veiller à ces invariants pour assurer la stabilité de nos objets.
Protège-moi
Au sein d’un objet, les attributs peuvent avoir des sémantiques différentes.
Certains attributs vont représenter des propriétés de l’objet et faire partie de son interface (tels que le prénom et le nom de nos objets User
). Ils pourront alors être lus et modifiés depuis l’extérieur de l’objet, on parle dans ce cas d’attributs publics.
D’autres vont contenir des données internes à l’objet, n’ayant pas vocation à être accessibles depuis l’extérieur.
Nous allons sécuriser notre stockage du mot de passe en ajoutant une méthode pour le hasher1 (à l’aide du module crypt
), afin de ne pas stocker d’informations sensibles dans l’objet.
Ce condensat du mot de passe ne devrait pas être accessible de l’extérieur, et encore moins modifié (ce qui en altérerait la sécurité).
De la même manière que pour les attributs, certaines méthodes vont avoir une portée publique et d’autres privée (on peut imaginer une méthode interne de la classe pour générer notre identifiant unique). On nomme encapsulation cette notion de protection des attributs et méthodes d’un objet, dans le respect de ses invariants.
Certains langages2 implémentent dans leur syntaxe des outils pour gérer la visibilité des attributs et méthodes, mais il n’y a rien de tel en Python.
Il existe à la place des conventions, qui indiquent aux développeurs quels attributs/méthodes sont publics ou privés.
Quand vous voyez un nom d’attribut ou méthode débuter par un _
au sein d’un objet, il indique quelque chose d’interne à l’objet (privé), dont la modification peut avoir des conséquences graves sur la stabilité.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import crypt class User: def __init__(self, id, name, password): self.id = id self.name = name self._salt = crypt.mksalt() # sel utilisé pour le hash du mot de passe self._password = self._crypt_pwd(password) def _crypt_pwd(self, password): return crypt.crypt(password, self._salt) def check_pwd(self, password): return self._password == self._crypt_pwd(password) |
1 2 3 | >>> john = User(1, 'john', '12345') >>> john.check_pwd('12345') True |
On note toutefois qu’il ne s’agit que d’une convention, l’attribut _password
étant parfaitement visible depuis l’extérieur.
1 2 | >>> john._password '$6$DwdvE5H8sT71Huf/$9a.H/VIK4fdwIFdLJYL34yml/QC3KZ7' |
Il reste possible de masquer un peu plus l’attribut à l’aide du préfixe __
. Ce préfixe a pour effet de renommer l’attribut en y insérant le nom de la classe courante.
1 2 3 4 5 6 7 8 9 10 11 12 | class User: def __init__(self, id, name, password): self.id = id self.name = name self.__salt = crypt.mksalt() self.__password = self.__crypt_pwd(password) def __crypt_pwd(self, password): return crypt.crypt(password, self.__salt) def check_pwd(self, password): return self.__password == self.__crypt_pwd(password) |
1 2 3 4 5 6 7 | >>> john = User(1, 'john', '12345') >>> john.__password Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'User' object has no attribute '__password' >>> john._User__password '$6$kjwoqPPHRQAamRHT$591frrNfNNb3.RdLXYiB/bgdCC4Z0p.B' |
Ce comportement pourra surtout être utile pour éviter des conflits de noms entre attributs internes de plusieurs classes sur un même objet, que nous verrons lors de l’héritage.
Tu aimes les glaces, canard ?
Un objet en Python est défini par sa structure (les attributs qu’il contient et les méthodes qui lui sont applicables) plutôt que par son type.
Ainsi, pour faire simple, un fichier sera un objet possédant des méthodes read
, write
et close
. Tout objet respectant cette définition sera considéré par Python comme un fichier.
1 2 3 4 5 6 7 8 9 10 | class FakeFile: def read(self, size=0): return '' def write(self, s): return 0 def close(self): pass f = FakeFile() print('foo', file=f) |
Python est entièrement construit autour de cette idée, appelée duck-typing : « Si je vois un animal qui vole comme un canard, cancane comme un canard, et nage comme un canard, alors j’appelle cet oiseau un canard » (James Whitcomb Riley)
TP : Forum, utilisateurs et messages
Pour ce premier TP, nous allons nous intéresser aux classes d’un forum.
Forts de notre type User
pour représenter un utilisateur, nous souhaitons ajouter une classe Post
, correspondant à un quelconque message.
Cette classe sera inititalisée avec un auteur (un objet User
) et un contenu textuel (le corps du message). Une date sera de plus générée lors de la création.
Un Post
possèdera une méthode format
pour retourner le message formaté, correspondant au HTML suivant :
1 2 3 4 5 6 | <div> <span>Par NOM_DE_L_AUTEUR le DATE_AU_FORMAT_JJ_MM_YYYY à HEURE_AU_FORMAT_HH_MM_SS</span> <p> CORPS_DU_MESSAGE </p> </div> |
De plus, nous ajouterons une méthode post
à notre classe User
, recevant un corps de message en paramètre et retournant un nouvel objet Post
.
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 | import crypt import datetime class User: def __init__(self, id, name, password): self.id = id self.name = name self._salt = crypt.mksalt() self._password = self._crypt_pwd(password) def _crypt_pwd(self, password): return crypt.crypt(password, self._salt) def check_pwd(self, password): return self._password == self._crypt_pwd(password) def post(self, message): return Post(self, message) class Post: def __init__(self, author, message): self.author = author self.message = message self.date = datetime.datetime.now() def format(self): date = self.date.strftime('le %d/%m/%Y à %H:%M:%S') return '<div><span>Par {} {}</span><p>{}</p></div>'.format(self.author.name, date, self.message) if __name__ == '__main__': user = User(1, 'john', '12345') p = user.post('Salut à tous') print(p.format()) |
Nous savons maintenant définir une classe et ses méthodes, initialiser nos objets, et protéger les noms d’attributs/méthodes.
Mais jusqu’ici, quand nous voulons étendre le comportement d’une classe, nous la redéfinissons entièrement en ajoutant de nouveaux attributs/méthodes. Le chapitre suivant présente l’héritage, un concept qui permet d’étendre une ou plusieurs classes sans toucher au code initial.