We already have seen the decorator @staticmethod
to define a method as static.
Decorators are wrappers which get a function and return a modified version of the function however just see them as tricks to make coding easier :-). There are plenty of them since anyone can make new ones (if you are curious, see here). Following are the main core decorators.
Properties, getters and setters¶
An object variable can be changed directly using <object name>.<variable>
:
class Dog:
def __init__(self,name):
self.name = name
dog = Dog("Max")
print(dog.name)
dog.name = "Charlie"
print(dog.name)
Max Charlie
However you may want to check that the new value of an object variable is correct. For example you may want to check that a length is positive or that a new Dog has not the same name than another one. To do so you should use methods:
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
This works fine. If you want to change the name after the creation of the object just use rename
and it will check if the new name is available.
BUT you still can do bob.name = "Maxime"
and no checks will be done and the list of all names will be wrong.
We could forbide the use of the variable name but we can do more convenient:
make bob.name = "Maxime"
calls the method rename
to do the checks. Do do so we use decorators @property
and @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
As you can see there is no more rename
method since we hide the name
variable behind 2 name
methods (one to give the value of name, the second to change its value).
This is a nice way to protect your object variables from outside errors.
Decorator deleter¶
A third decorator can be linked to an object's variable to explain how to delete it.
In our case when an Dog name is deleted we remove its name from _all_names
so another dog can have that name later.
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']
There is another way to define properties. You define 3 fonctions, the getter, the setter and the deleter, and give them to the builtin function property()
:
Exercice : modify the class Dog to correct the bug.
@classmethod¶
A method is a function of an object, a static method is a function of a class, a class method is also a function of a class used to create objects like __init__()
does. It is usefull when you want different ways to build the same kind of objects.
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
We have to proceed that way because Python does not have overloading like C++ does. With overloading you can define many methods with the same name as long they have different signature (meaning number of argument and their type). Since Python do not specify type of argument it makes difficult (useless) to have overloading. Therefore there is only one method __init__
.
By the way, for those who want to do overloading, there is a module which allows to do overloading with a decorator @overload
: https://pypi.python.org/pypi/overload/ (pip install overload)
More¶
You can imagine many more decorators
- to time a function
- to log
- to memoize (i.e. cache last value returned for each args so you have the result immediatly)
- check hardware (net, IO) before running function
This PythonDecoratorLibrary has more.
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).