Skip to article frontmatterSkip to article content

niveau intermédiaire/ avancé

en guise de complément, ce notebook introduit la notion de property

propos

on a vu qu’en général, une classe expose

il arrive qu’on se trouve dans une situation un peu mixte, où on voudrait

digression

langages dogmatiques: getter & setter

commençons par une digression: on a dit que Python était un langage pragmatique
dans des langages dogmatiques comme C++ ou Java, ce qui est considéré comme une bonne pratique, ça va être de

par exemple une classe Gauge - qui modélise une valeur confinée dans un certain intervalle - serait dans cette optique exposée au travers de deux méthodes

ici on prend volontairement un exemple où ces méthodes d’accès sont relativement simples, mais ce formalisme est très général et souvent poussé à son extrême: le code qui utilise la classe ne fait jamais directement référence aux attributs, mais toujours au travers de ces méthodes d’accès; et de cette façon on assure l’encapsulation, on peut garantir les invariants, etc...

en Python

sauf que, il faut bien le reconnaitre, le code qui résulte de cette approche est .. vraiment vilain
on se retrouve avec plein de parenthèses, et de get_ et de set_, ça rend le programme beaucoup moins lisible

c’est pourquoi on tient beaucoup à pouvoir accéder directement aux attributs
ce qui ne veut pas dire qu’on va renoncer à l’idée de pouvoir contrôler ces accès !

c’est exactement notre sujet: les properties, c’est le mécanisme qui va nous permettre:

on va voir ceci sur deux exemples

exemple 1 : indirection

dans cet exemple, on a une classe Station (de métro); elle veut exposer un attribut latitude
sauf que, en fait cette donnée n’est pas directement un attribut de Station, elle est incluse dans un autre objet qui est lui-même un attribut..

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

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
ce serait 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’attribut latitude n’est pas présent dans l’objet station

ça oblige à écrire plutôt station.row.latitude, 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
(ou, encore pire, à copier les colonnes de la dataframe sous forme d’attributs - une très mauvaise idée);

c’est l’idée derrière la notion de property: on va créer ici une property qui s’appelle latitude
ça se présente comme un attribut “normal”, mais en fait lorsqu’on accède à cet attribut on le fait au travers de fonctions

voici ce que ça donnerait dans ce premier exemple:

# maintenant ça fonctionne

class Station:
    def __init__(self, indice):
        self.row = stations.iloc[indice]
    def __repr__(self):
        # maintenant plus de problème
        return f"[Station {self.latitude:.2f}]"
    
    # car grâce à cette 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

deuxième cas d’usage, on veut une classe qui manipule une valeur, mais on veut en plus être sûr qu’elle appartient à un certain intervalle; disons entre 0 et 100

sans les properties, on serait obligé de définir une méthode set_value, qui va pouvoir faire des contrôles

# en définissant un setter
# ça marche mais c'est vraiment moche comme approche

class Gauge:
    def __init__(self, value):
        self.set_value(value)
    def __repr__(self):
        return f"[Gauge {self._value}]"
        
    def set_value(self, newvalue):
        # on force la nouvelle valeur à être dans l'intervalle
        self._value = max(0, min(newvalue, 100))

    # et pour être cohérent on fournit aussi un getter
    def get_value(self):
        return self._value

mais à nouveau ce n’est pas du tout pratique :

pour information, cette technique est celle employée dans les langages comme C++ et Java, on appelle ces méthodes des getters et setters
inutile de dire que ce n’est pas du tout pythonique comme pratique !

à nouveau dans cette situation les properties viennent à la rescousse; voici comment ça se présenterait

# version avec une property

class Gauge:
    
    def __init__(self, value):
        # on écrit .. au travers de la property
        self.value = value
        
    def __repr__(self):
        # on lit .. aussi au travers de la property
        return f"[Gauge {self.value}]"

    # la property se présente ici comme une paire de méthodes
    # d'abord le getter
    @property
    def value(self):
        """
        the docstring
        """
        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))

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]
# ou à la lecture
g.value
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
(perso je préfère celle-ci, je la trouve plus facile à retenir)

quoi qu’il en soit, voici la deuxième syntaxe, utilisée dans la classe Gauge

# version avec une property - deuxième syntaxe

class Gauge:

    # le début est inchangé
    def __init__(self, value):
        self.value = value
        
    def __repr__(self):
        return f"[Gauge {self.value}]"
    
    # le getter - généralement on le cache avec ce _ au début du nom
    def _get_value(self):
        """
        the docstring
        """
        return self._value

    # pareil
    def _set_value(self, newvalue):
        self._value = max(0, min(newvalue, 100))
        
    # et voici enfin la syntaxe pour définir la property
    value = property(_get_value, _set_value)
# à la création
g = Gauge(1000); g
[Gauge 100]
# ou à la modification
g.value = -10
g
[Gauge 0]
help(g)
Help on Gauge in module __main__ object:

class Gauge(builtins.object)
 |  Gauge(value)
 |
 |  Methods defined here:
 |
 |  __init__(self, value)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  __repr__(self)
 |      Return repr(self).
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object
 |
 |  value
 |      the docstring

use cases

quelques exemples de classes où on pourrait vouloir mettre des properties

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

comme on a parfois besoin que l’accès à un attribut passe par une couche de logique

dans ces cas-là on définit une property, qui permet de conserver le code existant (pas de changement de l’API)