Nous allons maintenant voir quelques facilités fournies par le langage python
On a vu que l'on pouvait itérer directement sur des contenants comme des listes
for x in [1,3,5]:
print x
Supposons que l'on définisse un objet qui contient lui aussi un ensemble de valeurs quelconque
class Equipe:
def __init__(self,nom,membres):
self.nom = nom
self.membres = membres
team = Equipe("Preum",["Adam","Eve"])
# on peut itérer sur les membres
for x in team.membres:
print(x)
mais on pourrait vouloir directement itérer sur l'objet, qui est une collection de membres le seul champ sur lequel on veut pouvoir itérer
for x in team:
print(x)
class Equipe:
def __init__(self,nom,membres):
self.nom = nom
self.membres = membres
# on peut définir un itérateur sur la classe.
def __iter__(self):
return iter(self.membres)
team = Equipe("Preum",["Adam","Eve"])
for x in team:
print(x)
Mais comment est défini l'itérateur sur la liste ?
Il suffit que l'objet ait une méthode __next()__, qui est déjà défini sur les listes.
Elle doit soulever une exception StopIteration quand il est censé s'arrêter (pas obligatoire...).
On peut le définir soi-même sur un objet:
Exemple: définir un itérateur qui parcours une liste à l'envers.
class Reverse:
"""itérateur pour parcourir un conteneur à l'envers"""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
def reset(self):
self.index = len(self.data)
for x in Reverse([1,2,3]):
print(x,end=" ")
un générateur est un moyen plus simple de définir des itérateurs.
le mot clef "yield" dans une fonction suffit à la définir la fonction comme générateur
def natural(n):
i = 0
while i<=n:
yield i
i = i + 1
gen = natural(3)
for x in gen:
print(x,end=" ")
Un générateur permet de générer au fur et à mesure des besoins des valeurs à énumérer, sans tout stocker explicitement.
La syntaxe cache en fait les choses suivantes:
La fin de l'itération est optionnelle !
def natural0():
i = 1
while True:
yield i
i = i + 1
gen = natural0()
gen.__next__()
Un générateur définit implicitement un itérateur, à condition de s'arrêter.
for i in natural(100):
print(i,end=" ")
En fait, natural fait la même chose que la fonction range
On peut aussi définir un parcours d'un conteneur "à l'avance"
Exemple :
from sys import getsizeof
# là on crée une liste à chaque fois
a1 = [x*(x+1) for x in range(10000)]
print(getsizeof(a1))
a1 = [x//2 for x in a1]
print(getsizeof(a1))
# ou bien ...
a2 = (x*(x+1) for x in range(100))
print(a2)
a2 = (x//2 for x in a2)
print(a2)
print(getsizeof(a2))
# plus simple
a2 = (y//2 for y in (x*(x+1) for x in range(100)))
for x in a2:
print(x,end=" ")
# attention !
print(list(a2)[:100])
Revenons à notre classe ; on aurait pu écrire l'itérateur nous mêmes:
class Equipe:
def __init__(self,nom,membres):
self.nom = nom
self.membres = membres
def __iter__(self):
for x in self.membres:
yield x
team = Equipe("Preum",["Adam","Eve"])
for x in team:
print(x)
class Equipe:
def __init__(self,nom,membres):
self.nom = nom
self.membres = membres
self.bannis = set()
def __iter__(self):
for x in self.membres:
if x not in self.bannis:
yield x
team = Equipe("Preum",["Adam","Eve","Cain","Abel"])
team.bannis.add("Cain")
for x in team:
print x
on peut avoir des attributs de classes, partagés par toutes les instances
class Bidon:
# attribut de classe
general = [1,2,3]
def __init__(self,x):
self.particulier = x
a = Bidon(5)
b = Bidon(6)
print a.general, b.general
a.general[0] = a.particulier
print b.general
a.general is b.general
Mais comment faire pour avoir une fonction partagée par toutes les instances sans l'instance elle-même ? Impossible comme les attributs car le premier argument est toujours l'objet créé (self)
-> On a besoin d'autre chose (plus tard)
Il est facile d'implémenter une telle classe en utilisant l'exception "NotImplementError"
class AbstractMaClasse:
def __init__(self,x):
self.base = x
def a_method(self,x):
return x*2
def another(self,x,y):
raise NotImplementedError, "appel à une classe abstraite"
class MaClasse(AbstractMaClasse):
def another(self,x,y):
return self.base + x*y
a = AbstractMaClasse(1)
print "objet a créé"
b = MaClasse(2)
print "objet b créé"
print b.another(2,3)
print a.another(2,3)
Un inconvénient de cette méthode est qu'il n'y a aucun contrôle que les sous-classes définissent bien toutes les méthodes au moment de la création d'une instance; il faut attendre l'appel d'une fonction non implémentée pour s'en rendre compte (tard).
class Oops(AbstractMaClasse):
pass
a = Oops(1) # ne devrait pas marcher !
print "----- instance créée"
a.another(3,4)
La aussi, il y a besoin d'autre mécanisme pour rendre ça plus général, que l'on va voir maintenant: les décorateurs
On a vu que l'on pouvait facilement définir des fonctions avec des fonctions en arguments ou même en résultat. Les fonctions sont des objets comme les autres (ou presque).
Python fournit de plus une syntaxe spéciale pour faciliter les transformations de fonctions: les décorateurs.
Ce mécanisme va permettre de traiter les problèmes mentionnés auparavant
Exemple: faisons une fonction qui aide à déclarer des fonctions "obsolètes" (en donnant un avertissement, mais en laissant le programme continuer)
def obsolete(func):
def new_func(x):
print "!! attention, appel à fonction obsolete"
return func(x)
return new_func
def mafonction(x):
return x*2
mafonction = obsolete(mafonction)
mafonction(12)
Inconvénient: ne marche que si la fonction a un seul argument.
Avec des arguments quelconques cette fois :
def obsolete(func):
def new_func(*args,**kwargs):
print "!! attention, appel à fonction obsolete"
return func(*args,**kwargs)
return new_func
def mafonction(x,y):
return x**2+y**2
mafonction = obsolete(mafonction)
mafonction(12,12)
Python fournit en fait une syntaxe plus pratique
def obsolete(func):
def new_func(*args,**kwargs):
print "!! attention, appel à fonction obsolete"
return func(*args,**kwargs)
return new_func
@obsolete
def mafonction(x,y):
return x**2+y**2
mafonction(12,12)
Avantages des décorateurs :
--> un outil d'abstraction très puissant
avec un argument : revient à empiler un autre "emballage" (wrapper) par dessus le premier
def obsolete(date):
def obsolete_decorator(func):
def new_func(*args,**kwargs):
print "!! attention, appel à fonction obsolete depuis", date
return func(*args,**kwargs)
return new_func
return obsolete_decorator
@obsolete(2005)
def mafonction(x,y):
return x**2+y**2
@obsolete(1980)
def oldfonction(x):
return x*2
mafonction(12,12)
Deux décorateurs utiles sont prédéfinis:
Et un autre du module abc permet de définir des méthodes de classes abstraites
Exemple : supposons qu'on redéfinisse une classe complexe, et qu'on veut une méthode qui donne les racines enièmes de 1, i.e.
$$\exp(i 2k\pi/n) = cos(2k\pi/n) + i\cdot sin(2k\pi/n) $$
from math import cos,sin, pi
class Complexe:
def __init__(self,r,i):
self.reel = r
self.imaginaire = i
def __repr__(self):
return "%f + i*(%f)"%(self.reel,self.imaginaire)
@staticmethod
def racine_unite(n,k):
return Complexe(cos(2.0*k*pi/n),sin(2.0*k*pi/n))
a = Complexe(1,1)
b = Complexe(-1,3)
print Complexe.racine_unite(3,1)
a.racine_unite is b.racine_unite
Imaginons maintenant qu'on veuille définir un nouveau conteneur, que l'on peut initialiser avec une liste, mais aussi à partir des éléments d'un dictionnaire.
On a deux solutions :
class Table:
def __init__(self,items):
self.contenu = items
def from_dict1(self,dico):
self.contenu = dico.values()
def from_dict2(self,dico):
return Table(dico.values())
Pas très élégant, car il faut soit initialiser deux fois une instance ou bien créer une instance pour créer une autre instance
a = Table([])
a.from_dict1({1:2,3:4})
# ou bien
c = a.from_dict2({5:6,7:8})
Déjà mieux avec une méthode statique
class Table:
def __init__(self,items):
self.contenu = items
@staticmethod
def from_dict(dico):
return Table(dico.values())
a = Table.from_dict({})
print a
Problème : si on veut maintenant faire une sous classe de Table, on doit tout réécrire si on veut garder la cohérence des constructeurs (from_dict faisant explicitement appel à la surclasse Table)
# une table qui contient plus d'information
class AutreTable(Table):
def __init__(self,items):
self.contenu = items
self.nb = len(items)
@staticmethod
def from_dict(dico):
return AutreTable(dico.values())
Ne serait-il pas mieux d'avoir une méthode de classe à la place ? Du coup rien à réécrire.
class Table:
def __init__(self,items):
self.contenu = items
@classmethod
def from_dict(cls,dico):
return cls(dico.values())
class AutreTable(Table):
def __init__(self,items):
self.contenu = items
self.nb = len(items)
a = AutreTable.from_dict({1:2})
a.__class__
Exercices: définir des décorateurs pour
Indice: définir le décorateur comme objet et redéfinir la méthode __call__
class memoise:
def __init__(self,f):
self.func = f
self.cache = {}
def __call__(self,*args):
if args in self.cache:
return self.cache[args]
else:
val = self.func(*args)
self.cache[args] = val
return val
@memoise
def fibo(n):
if n<2:
return 1
else:
return fibo(n-1)+fibo(n-2)
fibo(50)
class compte:
comptage = {}
def __init__(self,f):
self.func = f
self.comptage[f.__name__] = 0
def __call__(self,*args):
self.comptage[self.func.__name__] += 1
return self.func(*args)
@staticmethod
def stats():
return compte.comptage
def __repr__(self):
return self.func.__doc__
@compte
def add(x,y):
""" addition """
return x+y
@compte
def mult(x,y):
"""multiplication"""
return x*y
add(1,2)
add(3,4)
print compte.stats()
print add
On a vu que définir des classes abstraites seulement avec des exceptions NotImplemtend délaie les problèmes au moment des appels de méthodes, alors qu'il vaut mieux les intercepter à la création d'un objet d'une classe à problème.
Ceci est permit par le module abc, qui définit le décorateur de méthode abstraite
import abc
class AbstractMaClass(object):
__metaclass__ = abc.ABCMeta
def __init__(self):
pass
@abc.abstractmethod
def another(self):
pass
a = AbstractMaClass()