Magic/Dunder méthodes
Difficile
- Initialisations de nouveaux objets
- Représentation des objets
- Itération
- Operators overloading
- Les objets Python “callable”
- Gestionnaire de contexte
Les dunders methods sont facilement repérables car ces dernières commencent et finissent par des “__”.
Exemple:
___init___() |
Ce sont des fonctions prédéfinies dans Python.
Voici la liste des dunders methods: https://docs.python.org/3/reference
Voici la liste des dunders methods: https://docs.python.org/3/reference
Remarque:
"Dunder" = Double underscore. |
Ils sont utilisés pour imiter les types prédéfinis de Python.
Par exemple, nous pouvons avoir la longueur d'une string (fonction prédéfinie dans Python) mais pas la longueur d'une instance de classe.
C'est pourquoi, nous allons utiliser les dunders methods pour apporter de nouvelles fonctionalités à nos instances !
Par exemple, nous pouvons avoir la longueur d'une string (fonction prédéfinie dans Python) mais pas la longueur d'une instance de classe.
C'est pourquoi, nous allons utiliser les dunders methods pour apporter de nouvelles fonctionalités à nos instances !
Exemple:
class NoLenSupport:
pass
obj = NoLenSupport()
len(obj)
>>>
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-acc1060f7b33> in <module>()
3
4 obj = NoLenSupport()
----> 5 len(obj)
TypeError: object of type 'NoLenSupport' has no len()
class LenSupport:
def __len__(self):
return 42
obj = LenSupport()
len(obj)
>>> 42
Comment cela marche-t-il ?
En fait, lorsque vous faîtes:
En fait, lorsque vous faîtes:
print(len('Hello'))
>>> 5
Python fait ceci:
print('Hello'.__len__())
>>> 5
Ainsi, les dunders methods permettent de rendre vos classes plus puissantes en ajoutant des caractéristiques jusqu'alors réserver aux types prédéfinis.
Voici une liste non exhaustive de l'utilisation des dunders methods:
- Initialisations de nouveaux objets.
- Représentation des objets.
- Itération.
- Operators Overloading.
- Les objets Python "callable".
- Gestionnaire de contexte.
Initialisations de nouveaux objets
Nous utilisons des dunders methods pour créer un constructeur qui à son tour, va créer nos objets.
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
acc = Account('bob', 10)
Représentation des objets
Vous serez amené à documenter vos objets pour que d'autres personnes puissent s'y retrouver en lisant votre code.
On appele cela faire une "string representation".
Il y a 2 types de string representation:
- Une pour les développeurs = __repr__()
- Une pour les clients = __str__()
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def __repr__(self):
return 'Account({}, {})'.format(self.owner, self.amount)
def __str__(self):
return 'Account of {} with starting amount: {}'.format(
self.owner, self.amount)
acc = Account('bob', 10)
repr(acc)
>>> "Account('bob', 10)"
str(acc)
>>> 'Account of bob with starting amount: 10'
Itération
Créons un système de transaction. Elle sera composée de 2 méthodes:
- add_transaction(self, amount) qui permet de faire une transaction.
- balance(self) qui permet de consulter son compte bancaire.
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def add_transaction(self, amount):
if not isinstance(amount, int): #On vérifie si 'amount' est de type integer.
raise ValueError('please use int for amount')
return self.transactions.append(amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
acc = Account('bob', 10)
acc.add_transaction(20)
acc.add_transaction(-10)
acc.add_transaction(50)
acc.add_transaction(-20)
acc.add_transaction(30)
acc.balance
>>> 80
Désormais, nous voulons savoir plusieurs choses:
- Combien de transactions ai-je fais au total?
- Puis-je lister toutes mes transactions ?
Cas 1: Combien de transactions ai-je fais au total?
Etant donné que chaque transaction est stocké dans la liste transactions, nous pouvons nous dire que le nombre de transactions total correpond à la longueur de la liste?
len(acc)
>>>
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-27-a4d20ad835dd> in <module>()
----> 1 len(acc)
TypeError: object of type 'Account' has no len()
Comme mentioné plus haut, nous ne pouvons pas utiliser les fonctions prédéfinies de Python sur nos instances. Utilisons donc les dunders methods!
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def add_transaction(self, amount):
if not isinstance(amount, int): #Check if amount is an integer.
raise ValueError('please use int for amount')
return self.transactions.append(amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
#Here
def __len__(self):
return len(self.transactions)
acc = Account('bob', 10)
acc.add_transaction(20) #transaction 1.
acc.add_transaction(-10)#transaction 2.
acc.add_transaction(50)#transaction 3.
acc.add_transaction(-20)#transaction 4.
acc.add_transaction(30) #transaction 5.
len(acc)
>>> 5
Remarque:
Une alternative sans dunders methods est possible.
print(len(acc.transactions))
>>> 5
Cas 2: Puis-je lister toutes mes transactions ?
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def add_transaction(self, amount):
if not isinstance(amount, int): #Check if amount is an integer.
raise ValueError('please use int for amount')
return self.transactions.append(amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
def __len__(self):
return len(self.transactions)
acc = Account('bob', 10)
acc.add_transaction(20)
acc.add_transaction(-10)
acc.add_transaction(50)
acc.add_transaction(-20)
acc.add_transaction(30)
Essayons de lister toutes nos transactions à l'aide d'une boucle for.
for elt in acc:
print(elt)
>>>
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-57-05717bae11a0> in <module>()
----> 1 for elt in acc:
2 print(elt)
TypeError: 'Account' object is not iterable
Utilisons les dunders methods.
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def add_transaction(self, amount):
if not isinstance(amount, int): #Check if amount is an integer.
raise ValueError('please use int for amount')
return self.transactions.append(amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
def __len__(self):
return len(self.transactions)
#Here
def __getitem__(self, i):
return self.transactions[i]
acc = Account('bob', 10)
acc.add_transaction(20)
acc.add_transaction(-10)
acc.add_transaction(50)
acc.add_transaction(-20)
acc.add_transaction(30)
for elt in acc:
print(elt)
>>>
20
-10
50
-20
30
Nous pouvons même accéder à acc comme si c'était une liste.
acc[0]
>>> 20
Remarque:
Une alternative sans dunders methods est possible.
for elt in acc.transactions:
print(elt)
20
-10
50
-20
30
Operators overloading
Nous voulons comparer la somme d'argent entre deux comptes.
acc1 = Account('Ferdi', 100)
acc2 = Account('Test', 0)
print(acc1 > acc2)
print(acc1 < acc2)
print(acc1 == acc2)
>>>
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-94-304ccb72de6d> in <module>()
2 acc2 = Account('Test', 0)
3
----> 4 print(acc1 > acc2)
5 print(acc1 < acc2)
6 print(acc1 == acc2)
TypeError: '>' not supported between instances of 'Account' and 'Account'
Puisqu'on ne peut pas comparer des instances de classe entre eux mais seulement des entiers, nous devons utiliser les dunders methods pour adapter les opérateurs de comparaisons aux instances.
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def add_transaction(self, amount):
if not isinstance(amount, int): #Check if amount is an integer.
raise ValueError('please use int for amount')
return self.transactions.append(amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
#New here.
def __gt__(self, other):
return self.balance > other.balance
def __lt__(self, other):
return self.balance < other.balance
def __eq__(self, other):
return self.balance == other.balance
acc1 = Account("Ferdi",100)
acc2 = Account("test",0)
print(acc1 > acc2)
print(acc1 < acc2)
print(acc1 == acc2)
>>>
True
False
False
Les objets Python “callable”
A l'aide des dunders methods, vous pouvez faire en sorte qu'un objet soit "callable". Il suffit d'utiliser la dunder method __call__().
Par exemple, pour notre classe Account nous pouvons faire en sorte qu'elle affiche:
|
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self.transactions = []
def add_transaction(self, amount):
if not isinstance(amount, int): #Check if amount is an integer.
raise ValueError('please use int for amount')
return self.transactions.append(amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
def __getitem__(self, i):
return self.transactions[i]
def __call__(self):
print('Start amount: {}'.format(self.amount))
print('Transactions: ')
for transaction in self:
print(transaction)
print('\nBalance: {}'.format(self.balance))
acc = Account('bob', 10)
acc.add_transaction(20)
acc.add_transaction(-10)
acc.add_transaction(50)
acc.add_transaction(-20)
acc.add_transaction(30)
acc() #New here
>>>
Start amount: 10
Transactions:
20
-10
50
-20
30
Balance: 80
Gestionnaire de contexte
Un gestionnaire de contexte est un simple "protocole" que votre instance de classe doit suivre pour pouvoir utiliser le mot-clef with.
Suivons l'exemple suivant:
Suivons l'exemple suivant:
def my_function():
print("GLOUGLOU")
raise Exception("It should not break down everything.")
print("Before")
try:
my_function()
finally:
print("After")
>>>
Before
GLOUGLOU
>>>
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-114-36e96571c90e> in <module>()
1 print("Before")
2 try:
----> 3 my_function()
4 finally:
5 print("After")
<ipython-input-112-2851dfd2836c> in my_function()
1 def my_function():
2 print("GLOUGLOU")
----> 3 raise Exception("It should not break down everything.")
Exception: It should not break down everything.
Que s'est-il passé ici ?
Le try / finally s'assure que "After" sera toujours affiché même si "my_function()" ne s'exécute pas.
Puisque c'est une fonctionnalité utile, pourquoi ne pas rassembler le code dans une classe ?
Ainsi, cela évite d'avoir des longs blocs de try / finally dans notre code.
Le try / finally s'assure que "After" sera toujours affiché même si "my_function()" ne s'exécute pas.
Puisque c'est une fonctionnalité utile, pourquoi ne pas rassembler le code dans une classe ?
Ainsi, cela évite d'avoir des longs blocs de try / finally dans notre code.
class ContextManager():
def __enter__(self):
print("Avant")
def __exit__(self, exc_type, exc_value, traceback):
print("Après")
with ContextManager():
my_function()
>>>
Avant
GLOUGLOU
Après
>>>
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-124-3c81a3356b5a> in <module>()
8
9 with ContextManager():
---> 10 my_function()
<ipython-input-112-2851dfd2836c> in my_function()
1 def my_function():
2 print("GLOUGLOU")
----> 3 raise Exception("It should not break down everything.")
Exception: It should not break down everything.
Il y a beaucoup de choses à dire ici:
- __exit__(). - exc_value - traceback - my_function() - __exit__() |
Remarque:
Si vous avez le cas suivant:
class ContextManager():
def __enter__(self):
print("Avant")
return "OHOHO"
def __exit__(self, exc_type, exc_value, traceback):
print("Après")
#New here
with ContextManager() as c:
print(c)
my_function()
>>>
Avant
OHOHO
GLOUGLOU
Après
>>>
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-133-39b13b399eac> in <module>()
11 with ContextManager() as c:
12 print(c)
---> 13 my_function()
<ipython-input-112-2851dfd2836c> in my_function()
1 def my_function():
2 print("GLOUGLOU")
----> 3 raise Exception("It should not break down everything.")
Exception: It should not break down everything.
Comme vous aviez pu le remarquer, la valeur de retour de __enter()__ est stocké dans c !
Un autre exemple pour mieux comprendre.
Un autre exemple pour mieux comprendre.
class ContextManager():
def __enter__(self):
print("Avant")
def __exit__(self, exc_type, exc_value, traceback):
print("Après")
return "OHOHO"
#New here
with ContextManager() as c:
print(c)
my_function()
>>>
Avant
None
GLOUGLOU
Après
Cela semble ne marcher qu'avec __enter()__ !
Implémentons dans notre classe Account la possibilité de revenir en arrière. Plus précisément, lorsqu'on veut retirer de l'argent sur notre compte:
Si le retrait fait basculer notre compte bancaire dans le négatif, Alors on laisse le compte bancaire comme il était auparavant. |
class Account:
"""A simple account class"""
def __init__(self, owner, amount=0):
"""
This is the constructor that lets us create
objects from this class
"""
self.owner = owner
self.amount = amount
self.transactions = []
def withdraw_transaction(self, amount):
if not isinstance(amount, int):
raise ValueError('please use int for amount')
return self.transactions.append(-amount)
@property
def balance(self):
return self.amount + sum(self.transactions)
def __enter__(self):
self.copy_transactions = list(self.transactions) #Create a new list
#without reference.
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.balance < 0:
self.transactions = self.copy_transactions
def validate_transaction(acc, amount_to_withdraw):
with acc as a:
print("Withdraw {} to account".format(amount_to_withdraw))
a.withdraw_transaction(amount_to_withdraw)
print("Account will be at {}".format(a.balance))
if a.balance < 0:
raise ValueError ("Not enough money in bank account")
else:
print("Succesful transaction")
test = [10, 20, 100]
acc = Account('sue', 40)
i = 1
for amount in test:
print('\nExample {}:'.format(i))
i += 1
print('\nBalance start: {}\n'.format(acc.balance))
try:
validate_transaction(acc, amount)
except ValueError as exc:
print(exc)
print('\nBalance end: {}'.format(acc.balance))
>>>
Example 1:
Balance start: 40
Withdraw 10 to account
Account will be at 30
Succesful transaction
Balance end: 30
Example 2:
Balance start: 30
Withdraw 20 to account
Account will be at 10
Succesful transaction
Balance end: 10
Example 3:
Balance start: 10
Withdraw 100 to account
Account will be at -90
Not enough money in bank account
Balance end: 10
Pour résumer:
Les dunders methods permettent d'ajouter de nouvelles caractéristiques à nos instances jusqu'alors réserver aux types prédéfinies.
Voici une liste non exhaustive de l'utilisation des dunders methods: 1. Initialisations de nouveaux objets. 2. Représentation des objets. 3. Itération. 4. Operators Overloading 5. Les objets Python "callable". 6. Gestionnaire de contexte. |