Nous avons déjà vu le décorateur @staticmethod
pour définir une méthode comme statique.
Les décorateurs sont des wrappers qui prennent une fonction et renvoient une version modifiée de la fonction. On regardera à la fin de cette feuille leur mécanique, mais pour l'instant il suffit de les voir comme une astuce pour faciliter l'écriture du code.
Les accesseurs¶
Il s'agit de décorateurs très utiles qu'il est vivement conseillé d'utiliser. Ils permettent d'éviter qu'une variable d'objet soit mal modifiée. Ils font parti des décorateurs intégrés dans Python.
On a vu qu'une variable d'objet peut être modifiée directement :
class Dog:
def __init__(self,name):
self.name = name
dog = Dog("Max")
print(dog.name)
dog.name = "Charlie"
print(dog.name)
Max Charlie
C'est simple mais vous pouvez vouloir vérifier que la nouvelle valeur d'une variable d'objet est correcte. Par exemple, vous pouvez vérifier qu'une distance est positive ou qu'un nouveau chien ne porte pas le même nom qu'un autre. Pour faire ce contrôle de l'usage de la variable vous devez utiliser des méthodes :
class Dog:
_all_names = [] # class variable, shared by all objects of this class
def __init__(self, name):
self.rename(name) # call the method name(self,name)
def rename(self, name):
if name in self._all_names:
raise ValueError("Name already used")
else:
self.name = name
self._all_names.append(name) # _all_names is a class variable
# but it can also be called from the object
dog1 = Dog("Max")
dog2 = Dog("Rocky")
print(Dog._all_names)
dog2.rename("Max")
['Max', 'Rocky']
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) /tmp/ipykernel_7301/3387022365.py in <module> 2 dog2 = Dog("Rocky") 3 print(Dog._all_names) ----> 4 dog2.rename("Max") /tmp/ipykernel_7301/383084297.py in rename(self, name) 7 def rename(self, name): 8 if name in self._all_names: ----> 9 raise ValueError("Name already used") 10 else: 11 self.name = name ValueError: Name already used
Cela fonctionne bien. Si vous voulez changer le nom après la création de l'objet, utilisez simplement rename
et il vérifiera si le nouveau nom est disponible.
MAIS vous pouvez toujours faire bob.name =" Maxime "
et aucune vérification ne sera effectuée et la liste de tous les noms sera fausse.
Nous allons interdire l’utilisation du nom de la variable tout en permettant de :
faire bob.name =" Maxime "
qui appelle la méthode name()
(laquelle cache la variable _name
) pour effectuer les vérifications. Pour ce faire, nous utilisons les décorateurs @property
et@setter
.
@property
et @setter
¶
del Dog
class Dog:
_all_names = []
def __init__(self, name):
self.name = name # call name(name)
@property # this is a decorator, it starts with @ and is just before a function
def name(self):
return self._name
@name.setter
def name(self, name): # the name of the func is the name of the variable
if name in self._all_names:
raise ValueError("Name already used")
else:
self._name = name # the variable is now private : _name
self._all_names.append(name)
dog1 = Dog("Max")
dog2 = Dog("Charlie")
print(dog2.name) # uses dog2.name()
dog2.name = "Max" # call dog2.name("Max") which check if Max is used
Charlie
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) /tmp/ipykernel_7301/1076167219.py in <module> 2 dog2 = Dog("Charlie") 3 print(dog2.name) # uses dog2.name() ----> 4 dog2.name = "Max" # call dog2.name("Max") which check if Max is used /tmp/ipykernel_7301/3396828801.py in name(self, name) 14 def name(self, name): # the name of the func is the name of the variable 15 if name in self._all_names: ---> 16 raise ValueError("Name already used") 17 else: 18 self._name = name # the variable is now private : _name ValueError: Name already used
Comme vous pouvez le constater, il n'y a plus de méthode rename
puisque nous masquons la variable name
derrière deux méthodes name
(une pour donner la valeur de name, la seconde pour changer sa valeur).
C'est un bon moyen de protéger vos variables d'objet contre les erreurs extérieures.
@deleter
¶
Un troisième décorateur peut être lié à la variable d'un objet pour expliquer comment le supprimer.
Dans notre cas, lorsqu'un nom de chien est supprimé, nous supprimons son nom de _all_ noms
afin qu'un autre chien puisse avoir ce nom ultérieurement.
class Dog():
_all_names = []
def __init__(self, name):
self.name = name # call name(name)
def __del__(self): # is called when someones delete the object
del self.name # call deleter property of name
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if name in Dog._all_names:
raise ValueError("Name already used")
else:
self._name = name # here is where we store the name in the private variable _name
Dog._all_names.append(name)
@name.deleter
def name(self):
print('name deleter called for ', self._name)
Dog._all_names.remove(self._name)
dog1 = Dog("Max")
dog2 = Dog("Duke")
print('List of dogs registred:',Dog._all_names)
del dog1
print('List of dogs registred:',Dog._all_names)
List of dogs registred: ['Max', 'Duke'] name deleter called for Max List of dogs registred: ['Duke']
Méthode de classe¶
Une méthode est une fonction d'un objet, une méthode statique est une fonction d'une classe. Une méthode de classe est également une fonction de classe mais qui récupère en argument la classe et peut l'utiliser pour contruire un objet par exemple (comme le fait __init__ ()
) ce qui permet de surcharger le constructeur.
Il s'agit aussi d'un décorateur de base inclus dans le langage Python.
class Date:
def __init__(self, day, month, year):
self.day = day
self.month = month
self.year = year
@classmethod
def from_string(cls, date_as_string): # the first parameter of a class method
# is the class itself
"""
Convert string 'dd-mm-yyyy' to Date object.
"""
day, month, year = map(int, date_as_string.split('-'))
return cls(day, month, year) # calls Date constructor __init__
d = Date.from_string("12-03-2014")
print(d.year)
2014
Créer son décorateur¶
Il faut savoir qu'en Python une fonction est un objet comme un entier ou le chien ci-dessus. On peut donc passer une fonction en argument d'une fonction, on peut affecter une fonction à un variable (qui devient le nom de la fonction), etc.
say_hello = lambda name: f"Hello {name}"
say_hello('Arthur')
'Hello Arthur'
def dis_bonjour(name):
return f"Bonjour {name}"
def greet_bob(greeter_fct):
return greeter_fct("Bob")
greet_bob(dis_bonjour)
'Bonjour Bob'
Un décorateur est simplement une fonction qui prend en argument une fonction pour y ajouter quelque chose :
def remember(func):
func.cache = {} # would be a issue if the function has a value or method cache
def wrapper(*args): # decorated function to be returned
if args not in func.cache:
print("compute function for arguments ", args)
func.cache[args] = func(*args)
return func.cache[args]
return wrapper # returns the decorated function
def plus2(x):
return x + 2
plus2 = remember(plus2) # previous_arg & previous_result are defined now
plus2(4), plus2(5), plus2(4), plus2(5)
compute function for arguments (4,) compute function for arguments (5,)
(6, 7, 6, 7)
Je vous invite à prendre le temps de comprendre ce qui c'est passé ci-dessus.
Les décorateurs permettent d'écrire la même chose de facon plus compacte et plus lisible. Cela se fait ainsi avec
le décorateur @remember
automatiquement rattaché à notre fonction remember()
:
@remember
def fois3(x):
return 3 * x
fois3(1), fois3(2), fois3(2), fois3(1)
compute function for arguments (1,) compute function for arguments (2,)
(3, 6, 6, 3)
Il existe aussi un décorateur decorator
qui permet de faire des décorateurs de facon plus simple : https://github.com/micheles/decorator (et qui règle aussi des petits choses que j'ai cachées).
Plus¶
Vous pouvez imaginer beaucoup plus de décorateurs. En voici qu'on trouve dans des paquets :
@cache
à mémoire optimisée qui conserve la dernière valeur retournée pour chaque argument (functools)@singledispatch
pour surcharger une fonction avec un seul argument (functools)@profile
pour suivre et chronométrer les appels d'une fonction (https://mg.pov.lt/profilehooks/)
et encore d'autres sur