Programmation orienté objet
Directory
serie
- partie 1: Introduction
- partie 2: En pratique$ \Leftarrow $
- partie 3: Attributs
- partie 4: Héritage
- partie 5: Héritage multiple et composition
- partie 6: Méthodes magiques
sources
Recapitulatif
- Un attribut de classe est accessible sans avoir à créer d’instance (
NomClasse.attribut_de_classe = "poule"
) et est partagé entre tous les objets de cette classe. - Un attribut d’objet est uniquement accessible à travers une instance et est propre à l’instance.
- Une instance peut accéder aux attributs de classe et d’objet.
- Une méthode de classe est accessible sans avoir à créer d’instance. On utilise le décorateur
@classmethod
avant la définition de la méthode. Le premier argument n’est plusself
maiscls
- Les méthodes sans autres paramètres que
self
peuvent être décorées par@property
pour ne plus avoir à les flanquer de parenthèses lors de l’appel.
TP-1
Façon procédurale
Imaginons que nous voulons proposer une bibliothèque permettant de récupérer les 100 dernières questions concernant Python sur Stackoverflow.
Pour lire les données et les récupérer, pas besoin de POO, un scripte procédural fait l’affaire:
import csv
import urllib.request as urllib
from io import StringIO
# URL du CSV
URL = "http://data.stackexchange.com/StackOverflow/csv/109782"
# On télécharge les données, on les décode et on les enrobe dans
# StringIO pour qu'elles soient lisible de la même façon qu'un
# fichier malgré le fait qu'elles soient juste en mémoire.
csv_data = StringIO(urllib.urlopen(URL).read(100000).decode('utf8'))
# On utilise le module CSV pour lire notre "fichier" CSV.
# DictReader retourne une liste de dictionnaires, un par
# entrée du "fichier".
for question in csv.DictReader(csv_data):
print(question['CreationDate']) # date de création
print(question['Post Link']) # id question
Extrais de l’output:
...
2017-07-22 22:17:54
45259736
2017-07-22 22:16:27
45259726
...
Le fichier CSV issu de data.stackexchange.com n’est pas facile à parser. À la place de bourriner à coup de split()
, nous utilisons le module csv
qui comme expliqué en commentaire du code, récupère chaque entrée du fichier comme un dictionnaire qui aura la structure:
{'nom_decolonne': 'valeur_pour_cette_ligne', ...}
Cette façon de faire n’est vraiment pas pratique (réutilisable). On peut en faire une fonction réutilisable qui sera utilisable de l’extérieur.
import csv
import json
import urllib.request as urllib
from datetime import datetime
from io import StringIO
DATA_SOURCE_URL = "http://data.stackexchange.com/StackOverflow/csv/109782"
QUESTION_URL = "http://stackoverflow.com/question/{id}"
def download_question(url=DATA_SOURCE_URL):
csv_data = StringIO(urllib.urlopen(url).read(100000).decode('utf8'))
for question in csv.DictReader(csv_data):
# On transforme la string date en objet datetime
question['CreationDate'] = datetime.strptime(
question['CreationDate'],
'%Y-%m-%d %H:%M:%S'
)
# Le second champ est au format JSON. On le
# transforme donc en objet Python
question['Post Link'] = QUESTION_URL.format(id=question['Post Link'])
yield question
On pourrait l’importer et faire:
for question in download_question():
# affiche le titre et l'url
print("{CreationDate} : {Post Link}".format(**question))
Extrais de l’output:
...
2017-07-22 22:09:35 : http://stackoverflow.com/question/45259674
2017-07-22 22:07:42 : http://stackoverflow.com/question/45259663
2017-07-22 21:58:30 : http://stackoverflow.com/question/45259595
2017-07-22 21:56:16 : http://stackoverflow.com/question/45259582
...
Ce n’est pas un mauvaise façon de faire, On peut égallement faire des interfaces en programmation fonctionnelle.
Façon POO
import csv
import json
import urllib.request as urllib
from io import StringIO
from datetime import datetime
DATA_SOURCE_URL = "http://data.stackexchange.com/StackOverflow/csv/109782"
QUESTION_URL = "http://stackoverflow.com/question/{id}"
class Question(object):
def __init__(self, id, creation_date):
self.id = id
self.creation_date = creation_date
# On génère ces valeurs à la lecture:
def get_creation_date(self):
return datetime.strptime(self.creation_date,
'%Y-%m-%d %H:%M:%S')
def get_url(self):
return QUESTION_URL.format(id=self.id)
# Maintenant, notre fonction retourne des objets Question
def download_question(url=DATA_SOURCE_URL):
csv_data = StringIO(urllib.urlopen(url).read(100000).decode('utf8'))
for question in csv.DictReader(csv_data):
question['Post Link'] = json.loads(question['Post Link'])
# à la place de retourner des dictionnaires, on
# retourne des objets Question
yield Question(creation_date=question['CreationDate'],
id=question['Post Link'])
A première vue, ça ne semble pas très interessant, Ça fait la même chose mais c’est plus long.
Par contre on a un petit changement côté utilisation et c’est ça qu’on vise:
for question in download_questions():
# affiche le titre et l'url
print("{}: {}".format(question.creation_date,
question.get_url()))
À la place d’avoir un dictionnaire qui pourrait contenir n’importe quoi, on a un objet question
, avec un titre et la possibilité de construire l’URL.
Attributs et méthodes de classe
La POO, ce n’est pas juste faire des objets, c’est aussi les habiller. Un attribut de classe est un attribut qui appartient, non pas à l’objet mais à l classe.
class RandomObjet(object):
attribut_de_classe = "meme val pour tous"
def __init__(self):
self.attribut_d_objet = "valable pour objet en cours"
attribut_de_classe
est accessible depuis RandomObjet
sans créer d’instance, donc sans avoir à faire RandomObjet()
. Parcontre attribut_d_objet
n’est pas accessible si on a pas d’instance.
>>> print(RandomObjet.attribut_de_classe)
meme val pour tous
>>> print(RandomObjet.attribut_d_objet)
AttributeError: type object 'RandomObjet' has no attribute 'attribut_d_objet'
Une instance a donc accès aux deux:
>>> instance = RandomObjet()
>>> print(instance.attribut_de_classe)
meme val pour tous
>>> print(instance.attribut_d_objet)
valable pour objet en cours
Les attributs de classe sont donc partagés par toutes les instances d’une classe. Cela permet par exemple de faire des compteurs d’objets…
De la même façon on peut faire des méthodes de classe:
class RandomObjet(object):
@classmethod
def methode_de_classe(cls):
print("poule")
>>> RandomObjet.methode_de_classe()
poule
On utilise ici un décorateur qui dit que la méthode est une éthode de classe $ \Rightarrow $ accessible sans créer aucune instance
A noter que la convention de nommage change. Le premier argument n’est plus nommé self
mais cls
. Le premier argument sera “la classe en cours” et no plus l’objet en cours.
Encapsulation
Maintenant nous pouvons regrouper tout ce qui a un rapport avec notre objet Question
dans la classe:
import csv
import json
import urllib.request as urllib
from io import StringIO
from datetime import datetime
class Question(object):
#Les constantes sont maintenant des attributs de classe
DATA_SOURCE_URL = "http://data.stackexchange.com/StackOverflow/csv/109782"
QUESTION_URL = "http://stackoverflow.com/question/{id}"
def __init__(self, id, creation_date):
self.id = id
self.creation_date = creation_date
def get_creation_date(self):
return datetime.strptime(self.creation_date,
'%Y-%m-%d %H:%M:%S')
def get_url(self):
return self.QUESTION_URL.format(id=self.id)
# la fonction qui fabrique tous les objets Question est maintenant
# dans la classe Question, en tant que méthode de classe
@classmethod
def query(cls, url=DATA_SOURCE_URL):
csv_data = StringIO(urllib.urlopen(url).read(100000).decode('utf8'))
for quetion in csv.DictReader(csv_data):
question['Post Link'] = json.loads(question['Post Link'])
yield Question(creation_date=question['CreationDate'],
id=question['Post Link'])
C’est ce qu’on appelle l’encapsulation. On met tout les trucs qui ont un rapport entre eux dans la même boîte et on laisse la boîte s’occuper de comment ça fonctionne en interne.
Et là, on commence à avoir une API très chou:
print("Questions from: {}".format(Question.DATA_SOURCE_URL))
for question in Question.query():
# affiche titre et url
print("{creation_date}: {url}".format(
creation_date=question.creation_date,
url=question.get_url))
Tout part de l’objet Question
. Si on cherche quelque chose liée à l’objet Question
, on doit faire Question.<un_truc>
. On peut expérimenter dans le shell avec la complétion du code. En regardant ce boutde code pas besoin de savoir comment Question
marche pour savoir ce que ça fait. C’est assez explicite.
On peut encore faire un peu mieux…
Les propriétés
Ce sont des outils qui déguisent des méthodes pour les faire passer pour des attributs. Voici un simple exemple:
class RandomObjet(object):
def __init__(self, valeur):
self.valeur = valeur
def get_sq_val(self):
return self.valeur ** 2
>>> objet = RandomObjet(2)
>>> print(objet.get_sq_val())
4
La méthode get_sq_val()
ne prend aucun paramètre (autre que self
), pour éviter de devoir la flanquer de parenthèses à chaque appel, on peut la convertir en propery en ajoutant @property
avant sa définition. Avec une property, on dit à Python “fait comme si cette méthode était un banal attribut”:
class RandomObjet(object):
def __init__(self, valeur):
self.valeur = valeur
@property
def sq_val(self):
return self.valeur ** 2
>>> objet = RandomObjet(2)
>>> print(objet.get_sq_val)
4
Si nous appliquons ça à notre classe Question, ça donne:
import csv
import json
import urllib.request as urllib
from io import StringIO
from datetime import datetime
class Question(object):
#Les constantes sont maintenant des attributs de classe
DATA_SOURCE_URL = "http://data.stackexchange.com/StackOverflow/csv/109782"
QUESTION_URL = "http://stackoverflow.com/question/{id}"
def __init__(self, id, creation_date):
self.id = id
self.creation_date = creation_date
@property
def created(self):
return datetime.strptime(self.creation_date,
'%Y-%m-%d %H:%M:%S')
@property
def url(self):
return self.QUESTION_URL.format(id=self.id)
# la fonction qui fabrique tous les objets Question est maintenant
# dans la classe Question, en tant que méthode de classe
@classmethod
def query(cls, url=DATA_SOURCE_URL):
csv_data = StringIO(urllib.urlopen(url).read(100000).decode('utf8'))
for question in csv.DictReader(csv_data):
question['Post Link'] = json.loads(question['Post Link'])
yield Question(creation_date=question['CreationDate'],
id=question['Post Link'])
Ce qui clean un peu le code et l’interface également:
print("Questions from: {}".format(Question.DATA_SOURCE_URL))
for question in Question.query():
# affiche titre et url
print("{creation_date}: {url}".format(
creation_date=question.creation_date,
url=question.url))
Conclusion (Pour le moment)
Ceci était un premier aperçu de l’usage de la POO dans le monde réel. Le code s’est complexifié côté bibliothèque mais du côté utilisateur, on pass de ça:
from question_lib import download_questions, DATA_SOURCE_URL
print "Questions from : {}".format(DATA_SOURCE_URL)
for question in download_questions():
print "{creationDate} : {url}".format(**question['Post Link'])
À ça:
from question_lib import Question
print("Questions from: {}".format(Question.DATA_SOURCE_URL))
for question in Question.query():
print("{creation_date}: {url}".format(
creation_date=question.creation_date,
url=question.url))
Le style change, la façon dont sont exposées les données n’est pas la même non plus. Sur cette exemple simple, la complexité ajoutée pour le resultat fait que le jeu peut ne pas en valoir la chandelle mais sur des plus gros codes, c’est un game changer.
Un autre gros avantage de l’encapsulation, c’est que même si Stackoverflow change son format de données (de CSV à XML, par exemple), on a juste à adapter la classe. Le code qui utilise la classe, lui, n’aura pas besoin de changer.