properties¶
en guise de complément, ce notebook introduit la notion de property
propos¶
on a vu qu’en général, une classe expose
des attributs, pour accéder directement aux différents ‘morceaux’ qui constituent une instance
et des méthodes, qui sont des fonctions
il arrive qu’on se trouve dans une situation un peu mixte, où on voudrait
pouvoir accéder aux morceaux de données,
mais au travers d’une fonction; qui puisse, par exemple, faire des contrôles sur la validité des valeurs; ou simplement parce que l’accès en question se fait au travers d’une indirection
exemple 1 : indirection¶
imaginez par exemple que vous avez lu une dataframe, qui contient la liste des stations de métro
import pandas as pd
stations = pd.read_csv("data/stations.txt")
stations.head(2)
station_id | latitude | longitude | |
---|---|---|---|
0 | 2122 | 48.866856 | 2.290038 |
1 | 2177 | 48.829863 | 2.350623 |
et maintenant, on veut définir une classe Station
pour manipuler ce contenu
class Station:
def __init__(self, indice):
self.row = stations.iloc[indice]
on a donc une classe qui ‘emballe’ un objet de type pandas.Series
et on aimerait bien d’exposer un attribut latitude
, pour pouvoir écrire par exemple
# ce code ne marche pas
class Station:
def __init__(self, indice):
self.row = stations.iloc[indice]
def __repr__(self):
# ici self.latitude ne veut rien dire
return f"[Station {self.latitude:.2f}]"
mais bien entendu, ça ne fonctionne pas dans l’état, puisque l’attibut latitude
n’est pas présent dans l’objet station
# ça ne marche pas !
# Station(0)
ça oblige à écrire plutôt station.row.latitude
(ou, encore pire, à copier les colonnes de la dataframe sous forme d’attributs - une trè mauvaise idée); mais ça n’est pas du tout pratique, il va falloir se souvenir de cette particularité à chaque fois qu’on aura besoin d’accéder à latitude
dans ce premier exemple, on peut s’en sortir simplement avec une property :
# maintenant ça fonctionne
class Station:
def __init__(self, indice):
self.row = stations.iloc[indice]
def __repr__(self):
# plus de problème
return f"[Station {self.latitude:.2f}]"
# grâce à cett property, on peut accéder à l'attribut self.latitude
@property
def latitude(self):
return self.row.latitude
station0 = Station(0)
station0
[Station 48.87]
exemple 2 : une jauge¶
on veut une classe qui manipule une valeur, dont on veut être sûr qu’elle appartient à un intervalle; disons entre 0 et 100
sans les properties, on est obligé de définir une méthode set_value
; comme c’est une fonction, elle va pouvoir faire des contrôles
# en définissant un setter
# ça marche mais c'est vraiment moche comme approche
class Gauge:
def __repr__(self):
return f"[Gauge {self._value}]"
def __init__(self, value):
self.set_value(value)
def set_value(self, newvalue):
# on force la nouvelle valeur à être dans l'intervalle
self._value = max(0, min(newvalue, 100))
Gauge(1000)
[Gauge 100]
mais à nouveau ce n’est pas du tout pratique :
d’abord il faut “cacher” l’attribut pour éviter que l’on fasse accidentellement
gauge.value = 1000
ensuite du coup il faut aussi exposer une autr méthode
self.get_value()
pour lire la valeuret une fois qu’on a fait tout ça, on se retrouve à devoir écrire un code bavard et pas très lisible, bref c’est super moche
enfin, ça change l’API, et s’il y a déjà du code qui utilise l’attibut
.value
il faut tout changer
pour information, cette technique est celle employée dans les langages comme C++ et Java, on appelle ces méthodes des getters et setters; pas du tout pythonique comme pratique !
à nouveau dans cette situtation les properties viennent à la rescousse; voici comment ça se présenterait
# version avec une property
class Gauge:
@property
def value(self):
return self._value
# la syntaxe pour définir le 'setter' correspondant
# à la property 'value'
# et c'est pour ça bien sûr qu'on écrit '@value'
@value.setter
def value(self, newvalue):
self._value = max(0, min(newvalue, 100))
def __init__(self, value):
self.value = value
def __repr__(self):
return f"[Gauge {self._value}]"
avec ce code, on peut manipuler les objets de la classe “normalement”, et pourtant on contrôle bien la validité de la valeur
# à la création
g = Gauge(1000); g
[Gauge 100]
# ou à la modification
g.value = -10
g
[Gauge 0]
l’autre syntaxe¶
en fait il y a deux syntaxes pour définir une property, choisir entre les deux est une question de goût
voici la deuxième syntaxe utilisée dans la classe Gauge
# version avec une property - deuxième syntaxe
class Gauge:
# je choisis de cacher cette méthode car elle n'est
# pas supposée être appelée directement
def _get_value(self):
return self._value
# pareil
def _set_value(self, newvalue):
self._value = max(0, min(newvalue, 100))
# la syntaxe pour définir la property
value = property(_get_value, _set_value)
# le reste est inchangé
def __init__(self, value):
self.value = value
def __repr__(self):
return f"[Gauge {self._value}]"
# à la création
g = Gauge(1000); g
[Gauge 100]
# ou à la modification
g.value = -10
g
[Gauge 0]
conclusion¶
en Python, on aime bien accéder aux attributs d’un objet directement, et ne pas s’encombrer de getters et setters qui obscurcissent le code pour rien
on a parfois besoin que l’accès à un attribut passe par une couche de logique
soit pour implémenter une indirection
soit pour contrôler la validité des arguments
et dans ces cas-là on définit une property
qui permet de conserver le code existant (pas de changement de l’API)