Le design dit orienté-objet est une approche de programmation autour des 'objects'.
Nous avons déjà manipulé des objets comme les lists, les tuples et les dictionnaires ainsi que les tableaux de la librairie
NumPy
.
Le sujet de la programmation orientée-objet peut faire l'objet d'un cours entier. Dans ce notebook, nous allons traité :
Nous allons commencer par des exemples. Nous n'allons pas aborder les notions d'héritage et polymorphisme.
Python prend en charge le paradigme de programmation orienté objet; en fait, tout en Python est un objet. Vous avez utilisé des concepts de l'informatique orientée objet tout au long de ce cours.
Commençons par une importation de la librairie NumPy
:
import numpy as np
Considérons un tableau NumPy:
A = np.array([[1, -4, 7], [2, 6, -1]])
print(A)
Nous savons déjà comment vérifier le type d'un objet
print(type(A))
Cela signifie que A
est une instanciation de la classe numpy.ndarray
. On peut dire que A
est un numpy.ndarray
'.
Alors, qu'est-ce qu'un numpy.ndarray
? C'est une classe qui a des attributs et des méthodes (member functions en anglais).
Les attributs sont des données appartenant à un objet. Le tableau A
a un certain nombre d'attributs. Un attribut que nous connaissons est shape
:
s = A.shape
print(s)
Chaque objet de type numpy.ndarray
a l'attribut shape
qui décrit le nombre d'entrées dans le tableau dans chaque direction. Les autres attributs sont size
, qui est le nombre total d'entrées:
s = A.size
print(s)
et ndim
, qui est le nombre de dimensions du tableau (i.e. 1 pour une vecteur, 2 pour une matrice):
d = A.ndim
print(d)
Notez qu'après un nom d'attribut, il n'y a pas de ()
. C'est une caractéristique des attributs - nous avons simplement accédé à des données appartenant à un objet. Nous n'appelons pas une fonction ou ne faisons aucun travail de calcul.
Les méthodes sont des fonctions associées à une classe et exécutant des opérations sur les données associées à une instanciation d'une classe. Un objet numpy.ndarray
a une méthode min
, qui renvoie l'entrée minimum dans le tableau:
print(A.min())
Les méthodes sont des fonctions, et comme les fonctions peuvent prendre des arguments. Par exemple, nous pouvons utiliser la méthode sort
pour trier les lignes d'un tableau:
A.sort(kind='quicksort')
print(A)
où nous avons appelé la méthode sort
qui appartient ànumpy.ndarray
, et nous avons passé un argument qui spécifie qu'elle doit utiliser le tri rapide.
Les méthodes d'un objet peuvent prendre d'autres objets comme arguments. Étant donné un tableau à deux dimensions (matrice) $A$ et un tableau à une dimension (vecteur) $x$
A = np.array([[1, -4, 7], [2, 6, -1]])
x = np.ones(A.shape[1])
print(x)
On peut calculer $b = Ax$ en utilisant la méthode dot
:
b = A.dot(x)
print(b)
Les attributs et méthodes de classe sont généralement répertoriés dans la documentation. Pour numpy.ndarray
, tous les attributs et méthodes sont listés et expliqués à l'adresse https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html.
En utilisant Jupyter (ou IPython), on peut faire appel à 'tab-completion' pour voir les attributs et méthodes disponibles.
Parfois, nous ne pouvons pas trouver une classe (type d'objet) qui convient à notre problème. Dans ce cas, nous pouvons créer le nôtre. À titre d'exemple simple, considérons une classe qui contient le nom et le prénom d'une personne:
class PersonName:
def __init__(self, surname, forename):
self.surname = surname # Attribute
self.forename = forename # Attribute
# This is a method
def full_name(self):
"Return full name (forename surname)"
return self.forename + " " + self.surname
# This is a method
def surname_forename(self, sep=","):
"Return 'surname, forename', with option to specify separator"
return self.surname + sep + " " + self.forename
Avant de détailler la syntaxe de cette classe, nous allons l'utiliser. Nous créons d'abord un objet (une instanciation) de type PersonName
:
name_entry = PersonName("Bloggs", "Joanna")
print(type(name_entry))
Nous testons les attributs :
print(name_entry.surname)
print(name_entry.forename)
Ensuite, nous testons les méthodes de la classe :
name = name_entry.full_name()
print(name)
name = name_entry.surname_forename()
print(name)
name = name_entry.surname_forename(";")
print(name)
Détaillons la classe, est déclarée par
class PersonName:
On a alors ce qu'on appelle le créateur (intialiser en anglais):
def __init__(self, surname, forename):
self.surname = surname
self.forename = forename
C'est la fonction qui est appelée à la création d'un objet, c'est-à-dire lorsque nous utilisons name_entry = PersonName (" Bloggs "," Joanna ")
. Le mot-clé self
fait référence à l'objet lui-même - cela peut prendre du temps pour développer une compréhension de self
. L'initialiseur dans ce cas stocke le nom et le prénom de la personne (attributs).
def full_name(self):
"Return full name (forname surname)"
return self.forename + " " + self.surname
def surname_forename(self, sep=","):
"Return 'surname, forname', with option to specify separator"
return self.surname + sep + " " + self.forename
Ces méthodes sont des fonctions qui font quelque chose avec les données de classe. Dans ce cas, à partir du prénom et du nom, ils renvoient le nom complet de la personne, formaté de différentes manières.
Les opérateurs comme +
, -
,*
et /
sont en fait des fonctions - en Python, ils sont des raccourcis pour les fonctions avec les noms __add__
, __sub__
, __mul__
et __truediv__
, respectivement. En ajoutant ces méthodes à une classe, nous pouvons définir ce que les opérateurs mathématiques doivent faire.
Disons que nous voulons créer nos propres numéros avec leurs propres opérations. À titre d'exemple simple (complètement inutile), nous décidons que nous voulons changer la notation de telle sorte que *
signifie division et /
signifie multiplication.
Pour changer *
et /
pour nos nombres spéciaux, nous créons une classe pour représenter nos nombres spéciaux, et
lui fournir ses propres fonctions __mul__
et __truediv__
.
Nous fournirons également la méthode __str __ (self)
- elle est appelée lorsque nous utilisons la fonction print
.
class crazynumber:
"A crazy number class that switches the mutliplcation and division operations"
# Initialiser
def __init__(self, x):
self.x = x # This is an attribute
# Define multiplication (*) (this is a method)
def __mul__(self, y):
return crazynumber(self.x/y.x)
# Define the division (/) (this is a method)
def __truediv__(self, y):
return crazynumber(self.x*y.x)
# This is called when we use 'print' (this is a method)
def __str__(self):
return str(self.x) # Convert type to a string and return
Remarque: les noms de méthode
__mul__
,__truediv__
,__str__
, etc., ne doivent pas être appelés directement/ Ils sont mappés par Python aux opérateurs (*
et/
dans les deux premiers cas). La méthode__str__
est appelée dans les coulisses lors de l'utilisation de
Nous allons créer maintenant deux objets crazynumber
:
u = crazynumber(10)
v = crazynumber(2)
Puisque nous avons défini *
comme une division, nous nous attendons à ce que u * v soit égal à 5:
a = u*v # This will call '__mul__(self, y)'
print(a) # This will call '__str__(self)'
Test de l'opérateur /
:
b = u/v
print(b)
En fournissant des méthodes, nous avons défini comment les opérateurs mathématiques doivent être interprétés.
Nous avons précédemment utilisé des versions implémentées dans des librairies des fonctions de tri, et nous avons vu qu'elles sont beaucoup plus rapides que nos propres implémentations. Et si nous avons une liste de nos propres objets que nous voulons trier ? Par exemple,
nous pourrions avoir une classe StudentEntry
, puis une liste avec un objet StudentEntry
pour chaque étudiant.
Les fonctions de tri intégrées ne peuvent pas savoir comment nous voulons trier notre liste.
Un autre cas est si nous avons une liste de nombres, et nous voulons les trier selon une règle personnalisée ?
Les fonctions de tri intégrées ne se soucient pas des détails de nos données. Tout ce sur quoi ils comptent
sont des comparaisons, par exemple les opérateurs <
, >
et ==
. Si nous équipons notre classe d'opérateurs de comparaison,
nous pouvons utiliser des fonctions de tri intégrées.
Disons que nous voulons trier une liste de nombres de telle sorte que tous les nombres pairs apparaissent avant les nombres impairs, mais sinon la règle de classement habituelle s'applique. Nous ne voulons pas écrire notre propre fonction de tri. Nous pouvons faire ce tri personnalisé en créant notre propre classe pour contenir un nombre et en l'équipant des opérateurs <
, >
et ==
.
Les fonctions associées à ces opérateurs sont :
__lt__(self, other)
(less than other
, <
)__gt__(self, other)
(greater than other
, >
)__eq__(self, other)
(equal to other
, ==
)Ces fonctions renvoient True
ou False
.
Nous avons ici, une classe pour stocker un numéro qui obéit à nos règles de tri personnalisées:
class MyNumber:
def __init__(self, x):
self.x = x # Stocker la valeur (attribut)
# opérateur '<' personnalisé (method)
def __lt__(self, other):
if self.x % 2 == 0 and other.x % 2 != 0: # Je suis pair et l'autre est impair, donc je suis plus petit
return True
elif self.x % 2 != 0 and other.x % 2 == 0: # Je suis impair, l'autre est pair, donc je ne suis pas plus petit
return False
else:
return self.x < other.x # utiliser les règles classiques de tri
# opérateur '==' personnalisé (method)
def __eq__(self, other):
return self.x == other.x
# opérateur '>' personnalisé (method)
def __gt__(self, other):
if self.x % 2 == 0 and other.x % 2 != 0: # Je suis pair et l'autre est impair, je ne suis pas plus grand
return False
elif self.x % 2 != 0 and other.x % 2 == 0: # Je suis impair, l'autre est pair,je suis plus grand
return True
else:
return self.x > other.x # règles de tri classique
# Cette fonction est appelée par Python quand on veut afficher un objet
def __str__(self):
return str(self.x)
Nous pouvons effectuer quelques tests simples sur les opérateurs (insérer des instructions print dans les méthodes pour vérifier la fonction utilisée)
x = MyNumber(4)
y = MyNumber(3)
print(x < y) # Attendu True (x est pair et y impair)
print(y < x) # Attendu False
Nous essayons maintenant d'appliquer la fonction de tri de liste intégrée pour vérifier que la liste triée respecte notre règle de tri personnalisée:
# Create an array of random integers
x = np.random.randint(0, 200, 10)
# Create a list of 'MyNumber' from x (using list comprehension)
y = [MyNumber(v) for v in x]
# This is the long-hand for building y
#y = []
#for v in x:
# y.append(MyNumber(v))
# Use the built-in list sort method to sort the list of 'MyNumber' objects
y.sort()
print(y)
Sans modifier l'algorithme de tri, nous avons appliqué notre propre classement. Des approches comme celle-ci sont une caractéristique de l'informatique orientée objet. Les algorithmes de tri trient les objets et les objets ont simplement besoin des opérateurs de comparaison. Les algorithmes de tri n'ont pas besoin de connaître les détails des objets.
Les méthodes spéciales Python qui commencent et se terminent par un double trait de soulignement (__
) sont des méthodes magic. Elles correspondent à des opérateurs spéciaux, généralement des opérateurs mathématiques tels que *
, /
, <
, ==
, etc.
C'est des méthodes standard car elles peuvent être appelées directement sur un objet, mais ce n'est pas leur utilisation prévue. Utilisez plutôt des opérateurs. Voici un exemple.
class SomePair:
def __init__(self, x, y):
self.x = x # Store value (attribute)
self.y = y # Store value (attribute)
# '==' operator (note that it has a return value)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
a = SomePair(23, 2)
b = SomePair(23, 4)
# Check for equality using ==
print(a == b)
# Check for equality using __eq__ (not recommended)
print(a.__eq__(b))
Un objet n'a pas besoin d'avoir toutes les fonctions magiques définies - seulement celles que vous avez l'intention d'utiliser. Si vous essayez d'utiliser et l'opérateur qui n'est pas défini, vous obtiendrez une erreur.
Créer une classe pour représenter des vecteurs de longueur arbitraire et qui est initialisée avec une liste de valeurs, par exemple:
x = MonVecteur([0, 2, 4])
Équiper la classe de méthodes qui:
size
) norm
)dot
)Tester l'implémentation en utilisant deux vecteurs de longueur $3$. Pour vous aider à démarrer, un squelette de la classe est fourni ci-dessous.
class MonVecteur:
""" Un objet vectoriel qui peut renvoyer sa taille et sa norme, et peut calculer
le produit scalaire avec un autre vecteur """
def __init__(self, x):
self.x = x
# Return length of vector
def size(self):
# Add your code here
pass # This can be removed once the body is added
# This allows access by index, e.g. y[2]
def __getitem__(self, index):
return self.x[index]
# Return norm of vector
def norm(self):
# Add your code here
pass # This can be removed once the body is added
# Return dot product of vector with another vector
def dot(self, other):
# Add your code here
pass # This can be removed once the body is added
# création de deux vecteurs
u = MonVecteur([1, 1, 2])
v = MonVecteur([2, 1, 1])
assert u.size() == 3
assert round(u.norm() - 2.449489742783178) == 0.0
assert round(u.dot(v) - 5.0, 10) == 0.0
## on peut afficher les valeurs
print(u.size())
print(u.norm())
print(u.dot(v))
Créer une classe RandExpo
qui représente une loi exponentielle de paramètre rate
(un double)
Équiper la classe de méthodes qui:
n
réalisations d'une loi exponentielle de paramètre rate
x
x
p
Tester l'implémentation sur un exemple.