Py: objets et classes
Py: objets et classes

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:

  1. Un type qui identifie le rôle de l’objet.
  2. Des attributs qui sont les propriétés de l’objet.
  3. 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 avec id.

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é).

  1. 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.