Python: Programmation orientée objet
Directory
Introduction
Un objet en Python est défini par sa structure (les attributs qu’il contient et les méthodes qu’il lui sont applicables) plutôt que par son type.
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 cannard” (James Whitcomb Riley)
La programmation orientée objet est le paradigme qui nous permet de définir nos propres types d’objets, avec leurs propriétés et opérations.
Objets
Cette introduction omet volontairement le terme “class” et se concentre sur la notion d’objet.
Objet et caractéristiques
Un objet est constitué de 3 caractéristiques:
- Un type qui identifie le rôle de l’objet.
- Des attributs qui sont les propriétés de l’objet.
- Des méthodes, les opérations (actions) qui s’appliquent sur l’objet.
Exemples
- On instancie un variable number de type
int
>>> number = 5
numerator
est un attribut de number
>>> number.numerator
5
- Variable values de type
list
>>> values = []
append
est une méthode de values
>>> values.append(number)
>>> values
[5]
- Toute valeur en Python est donc un objet
- Tout objet est associé à un type
Un type définit la sémantique d’un objet. On sait par exemple que les objets de type int
sont des nombres entiers, que l’on peut additionner, soustraire, etc…
Pour créer un nouveau type en Python nous utilisons le mot clé class
:
class User:
pass
Nous avons maintenant à notre disposition un type User
. Pour créer un objet de ce type, il nous suffit de procéder comme suit:
john = User()
On dit alors que john est une instance de User
.
Attributs
Ils représentent des valeurs propres à l’objet. Nos objets de type User
pourraient par exemple contenir un identifiant id
, un nom name
et un mot de passe passeword
.
En python, nous pouvons facilement associer des valeurs à nos objets:
class User:
pass
john = User()
john.id = 1
john.name = 'john'
john.password = '12345'
print(john.id)
print(john.name)
print(john.password)
output:
1
john
12345
Nous avons instancié un objet nommé john, de type User
, auquel nous avons attribué trois attributs puis nous avons affiché les valeurs de ces attributs.
À noter que l’on peut redéfinir la valeur d’un attribut, et qu’un attribut peut aussi être supprimé à l’aide de l’opérateur del
del john.password
print(john.password)
output:
Traceback (most recent call last):
File "/home/sol/Code/Python/Tuto/Class/00tests/00.py", line 14, in <module>
print(john.password)
AttributeError: 'User' object has no attribute 'password'
Il est déconseillé de nommer une valeur de la même manière qu’une fonction built-in. Dans le cas d’un attribut ce n’est pas gênant car il ne fait pas partie du même namespace.
john.id
ne rentre pas en conflit avecid
.
Méthodes
Opérations applicables sur les objets. Ce sont des fonctions qui reçoivent notre objet en premier paramètre.
class User:
pass
john = User()
john.id = 1
john.name = 'john'
john.password = '12345'
def user_check_pwd(user, password):
return user.password == password
output:
False
True
Les méthodes recevant l’objet en paramètre, elles peuvent en lire et modifier les attributs. Par exemple, la méthode append
des listes permet d’insérer un nouvel élément $ \Rightarrow $ elle modifie bien la liste en question.
Classes
Une classe est la définition d’un type. int
, str
ou 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 $ \Rightarrow $ quelles méthodes vont être applicatbles sur ces objets.
Introduction
On définit une classe à l’aide du mot-clé class
:
class User:
pass
Le mot-clé
pass
indique à Python que le corp de la classe est vide.
Convention: nom en CamelCase
On instancie une classe de la même façon qu’on appelle une fonction: On suffixe son nom d’une paire de parenthèses. C’est valable pour les classes perso mais aussi pour les types primitifs:
>>> User()
<__main__.User object at 0x7f89fd767588>
>>> int()
0
>>> str()
''
>>> list()
[]
Pour définir une méthode dans une classe, on procède comme pour la définition d’une fonction mais dans le corp de la classe en question:
class User:
def check_pwd(self, password):
return self.password == password
La classe User
possède maintenant une méthode check_pwd
applicable sur tous ses objets.
>>> john = User()
>>> john.id = 1
>>> john.name = 'john'
>>> john.password = '12345'
>>> john.check_pwd('toto')
False
>>> john.check_pwd('12345')
True
Le self
reçu en premier paramètre par check_pwd
désigne l’objet sur lequel on applique la méthode. Les autres parmètres 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. Appeler john.check_pwd('12345')
équivaut à l’appel User.check_pwd('john', '12345')
. C’est pourquoi john
correspond ici au paramètre self
de notre méthode.
self
n’est pas un mot clé mais une convention qu’il faut respecter !
Dans le corps de la méthode, check_pwd
, password
et self.password
sont deux valeurs distinctes. La première est le paramètre reçu par la méthode et la seconde est l’attribut de l’objet.
Initialisation et attributs
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 recevera les arguments passés lors de l’instanciation.
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
Dans cette méthode se trouve le paramètre self
qui est donc utilisé pour modifier les attributs de l’objet.
>>> john = User(1, 'john' '12345')
>>> john.check_pwd('toto')
False
>>> john.check_pwd('12345')
True
Encapsulation
Invariants
Les différents attributs de nos objets forment un état de cet objet, normalement stable. Les attributs sont liés les uns aux autre, la modification d’un d’eux peut avoir des conséquences sur les autres. Les invariants correspondent aux relations qui lient ces différents attributs.
Si les objets User
étaient dotés d’un attribut contenant une évaluation du mot de passe (savoir si le mot de pass est sécurisé ou non), il doit devrait alors être mis à jour chaque fois que nous modifions l’attribut password
d’un objet User
.
Dans le cas contraire, le mdp et l’évaluation ne seraient plus corrélés, et l’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.
Visibilité
En Python il n’existe pas de visibilité tel que public ou private en C++. Il existe à la place des conventions qui indiquent aux développeurs quels attributs/méthodes sont publics ou privés. Quand un nom d’attribut ou de méthode débute 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é.
Pour empécher l’acces depuis l’extérieur à l’attribut password nous allons ajouter une méthode pour le hasher¹ (à l’aide du module crypt). 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é).
- Le hashage d’un mot de pass correspond à une opération non-reversible qui permet de calculer un condensat (hash) du mot de passe. Ce condesnat peut-être utilisé pour vérifier la validité d’un mot de passe, mais ne permet pas de retrouver le mot de passe d’origine.
import crypt
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)
john = User(1, 'john', '12345')
print(john.check_pwd('12345'))
output:
True
A remarquer qu’il ne s’agit que d’une convention, l’attribut
_password
étant tout à fait visible depuis l’extétieur.
print(john._password)
output:
$6$fMWbLcsayCnTWdxH$6DTBPKy2.tGfboVLpJX0m7NBxAhtQOnf2.KOBIJl/3Gcll.5dI2qrLa1dB3.eWwZlJQSGWjC5XniYZIBnGraS0
On peut masquer un peu plus l’attribut à l’aide du préfix __
. Ce préfix a pour effet de renommer l’attribut en y insérant le nom de la classe courante:
import crypt
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)
john = User(1, 'john', '12345')
print(john.__password)
output:
Traceback (most recent call last):
File "/home/sol/Code/Python/Tuto/Class/00tests/00.py", line 17, in <module>
print(john.__password)
print(john._User__password)
output:
$6$g1WE7ILuFXf5d5hE$xOIa5QQa5FOH9zqtvgQJmYAgVeeYhhcEH./2r2LwKucztP2Dlo5zrLY/JQ8KBkJ5hNrcyv6EhW06vNX1qDs4h0
Ce comportement est principalement utilisé pour éviter des conflits de noms entre attributs internes de plusieurs classes sur un même objet.