mercredi 21 octobre 2009

Comprehension list, itérateurs et générateurs (Yield) en python

List Comprehension
Les list comprehensions sont est une syntaxe particulière permettant de construire facilement et rapidement des listes.

La syntaxe est les suivantes:
[ expression-manipulation for expression in sequence ]
[ expression-manipulation for expression in sequence if test]

>>> [word.upper() for word in "c est une phrase".split() ]
['C', 'EST', 'UNE', 'PHRASE']

# ou code equivalent mais plus long

>>> myList = []
>>> for word in "c est une phrase".split():
...     myList.append( word.upper() )
... 
>>> myList
['C', 'EST', 'UNE', 'PHRASE']

# Autre exemple de list comprehension
>>> [ i * 3 for i in range(10) if i%2==0]
[0, 6, 12, 18, 24]

Les itérateurs
L'iteration est un mecanisme permettant de passer en revue le contenu d'une liste en utilisant le principe d'iteration.
Un itérateur implemente la fonction next() qui retourne la prochaine valeur (ou l'exception StopIteration quand il n'y a plus d'élément).
Les itérateurs sont utilisés de façon transparente dans les syntaxes tel que "for item in sequence:".
Il est également possible d'obtenir un itérateur (implémentant la fonction next) en utilisant la primitive iter( anObjectExposingIteratorFunction ).
On appele ensuite la fonction next() de l'iterateur pour retrouver les différents élements.

L'exemple suivant est disponible dans le fichier testIterator.py .
>>> def UseIterator():
     """ l exemple suivant démontre l'utilisation
         explicite d'un itérateur. Pour information, 
  l'instruction for fait un usage implicite de 
         L'itérateur """
     mySequence = ['a', 'b', 5 ,7 ,9 ,'tz']
     _iterator = iter( mySequence )
     try:
             while True:
                     _value = _iterator.next()
                     print( _value )
     except StopIteration:
             print( '--- End of iteration ---' )
>>> testIterator.UseIterator()
a
b
5
7
9
tz
--- End of iteration ---

Définir son propre itérateur
Il est possible de definir un iterateur pour ses propres classes.
Dans ce cas, il est nécessaire d'implémenter la fonction __iter__ retournant une référence vers un object implémentant la methode next().
NB: la fonction __iter__ peut être utilisée pour retourner une référence vers l'objet lui-même si ce dernier implémente la fonction next(). Ce sera le cas de l'exemple ci-dessous aussi disponible dans  le fichier testIterator.py .

import random

def UseIterator():
     mySequence = ['a', 'b', 5 ,7 ,9 ,'tz']
     _iterator = iter( mySequence )
     try:
             while True:
                     _value = _iterator.next()
                     print( _value )
     except StopIteration:
             print( '--- End of iteration ---' )

class myIterableClass(object):
 """ class contenant une liste d'élément iterable.

     Attention: Ceci est un exemple pour faciliter la 
  compréhension.
                Cette classe ne sais produire qu'un
  seul iterateur à la fois car elle implémente 
  elle meme son propre itérateur et par conséquent
                l'index est reseter a chaque obtention de 
                l'itérateur (via __iter__).
                La solution a ce problème est d'implémenter une
                classe d'iteration indépendante. """
 _list = []
 _index = 0 # cursor for iteration

    # Constructor
 def __init__( self ):
  # prepare a random list of items
  for i in range( 1, random.randint(1,25) ):
   self._list.append( 'Value %s is %s ' % (i, 
    random.randint(1,1000) ) )
 # Iterable primitive 
  def __iter__( self ):
  self._index = 0;
  return self
 # Iteration function
 def next( self ):
  self._index += 1
  if self._index-1 >= len( self._list ):
   raise StopIteration()
  return self._list[self._index-1]

>>> # Utilisation de la classe iterable
>>> #
>>> obj = myIterableClass()
>>> for theValue in obj:
...     print( theValue )
... 
Value 1 is 687 
Value 2 is 3 
Value 3 is 427 
Value 4 is 954 


>>> # Utilisation de la classe iterable 
>>> #   via la declaration d'un iterateur
>>> it = iter( obj )
>>> it.next()
'Value 1 is 687 '
>>> it.next()
'Value 2 is 3 '
>>> it.next()
'Value 3 is 427 '
>>> it.next()
'Value 4 is 954 '
>>> it.next()

Les générateurs (Yield)
Yield est un mot clé permettant de retourner un contenu énumerable.
Dans le monde Python, la syntaxe utilisant le mot clé "yield" correspond au concept de "générateur" (generator).
L'interpréteur utilise le générateur qui mémorisera l'état de la fonction appelée. Comme précisé par Tarek Ziadé, "la directive yield constituant en quelque sorte un return avec point de sauvegarde de l'état des variables locales et de l'endroit où le code de la fonction à quitté." (Source: Programmation Python seconde Edt., Page 149, ISBN 9782-212-12483-5)

L'exemple suivant issu du script testIterator.py démontre l'usage du mot clé yield.
class YieldedItem( object ):
 """ Classe de demonstration démontrant l'utilité du mot 
            clé "yield". Voir les fonctions TestYielded et 
            BuildYielded """   
 _value = -1
 def __init__(self):
  self._value = random.randint(1,25)
 def __getValue( self ):
  return self._value
 value = property( __getValue, None, None )

def BuildYielded():
 """ Cette fonction génère une série d'objects
     de type YieldedItem() et retourne les 
            références à l'aide du mot clé "yield".
     Les objects ainsi crées seront énumerables """
 for i in range( 10 ):
  print( 'BuildYielded(): Create YieldedItem() object #%s' 
   % (i) )
  yield YieldedItem()

def TestYielded():
 """ Cette fonction démontre l'utilité du mot
     clé "yield". En effet, la fonction BuildYielded()
     retourne une série d'objet à l'aide du mot clé "yield".
     Par conséquent, les différents objets sont stockés 
     dans un générateur qui lui est énumérable. """

 # Exemple démontrant l'usage du générateur
 print( 'La fonction BuildYielded() retourne un générateur.' )
  print( 'Notez que la fonction BuildYielded() n\'est pas exécutée!')
 print( BuildYielded() )
 
 # Exemple démontrant le caractère énumérable
 print( 'Le resultat de la fonction peut être facilement énuméré.' )
 print( 'Notez l\'entrelas des appels entre TestYielded() et ' +    
               'BuildYielded()' ) 
        for item in BuildYielded():
  print( 'TestYielded(): Valeur stockée dans l\'objet #%s est %s' % (id(item),item.value) )

 # Exemple stockant la référence du générateur
 print( ' ' );
 print( 'Autre exemple en stockant la référence du générateur' )
 gen = BuildYielded();
 for item in gen:
  print( 'TestYielded(): Valeur stockée est %s' % (
   item.value) )

Le code précédent produissant le résultat ci-dessous.
Notez l'entrelas des appels entre les fonctions BuildYielded() et TestYielded()! Les générateurs sont plus particulièrement approprié lors de la manipulation de grands ensembles/set d'objets (voir l'exemple du point suivant concernant les générateurs et les listes de compréhension).

>>> TestYielded()
La fonction BuildYielded() retourne un générateur.
Notez que la fonction BuildYielded() n'est pas exécutée!
<generator object at 0x839086c>
Le resultat de la fonction peut être facilement énuméré.
Notez l'entrelas des appels entre TestYielded() et BuildYielded()
BuildYielded(): Create YieldedItem() object #0
TestYielded(): Valeur stockée dans l'objet #137953772 est 4
BuildYielded(): Create YieldedItem() object #1
TestYielded(): Valeur stockée dans l'objet #137955468 est 6
BuildYielded(): Create YieldedItem() object #2
TestYielded(): Valeur stockée dans l'objet #137953772 est 7
BuildYielded(): Create YieldedItem() object #3
TestYielded(): Valeur stockée dans l'objet #137955468 est 9
BuildYielded(): Create YieldedItem() object #4
TestYielded(): Valeur stockée dans l'objet #137953772 est 8
BuildYielded(): Create YieldedItem() object #5
TestYielded(): Valeur stockée dans l'objet #137955468 est 20
BuildYielded(): Create YieldedItem() object #6
TestYielded(): Valeur stockée dans l'objet #137953772 est 11
BuildYielded(): Create YieldedItem() object #7
TestYielded(): Valeur stockée dans l'objet #137955468 est 18
BuildYielded(): Create YieldedItem() object #8
TestYielded(): Valeur stockée dans l'objet #137953772 est 1
BuildYielded(): Create YieldedItem() object #9
TestYielded(): Valeur stockée dans l'objet #137955468 est 20
 
Autre exemple en stockant la référence du générateur
BuildYielded(): Create YieldedItem() object #0
TestYielded(): Valeur stockée est 3
BuildYielded(): Create YieldedItem() object #1
TestYielded(): Valeur stockée est 18
BuildYielded(): Create YieldedItem() object #2
TestYielded(): Valeur stockée est 19
BuildYielded(): Create YieldedItem() object #3
TestYielded(): Valeur stockée est 9
BuildYielded(): Create YieldedItem() object #4
TestYielded(): Valeur stockée est 15
BuildYielded(): Create YieldedItem() object #5
TestYielded(): Valeur stockée est 3
BuildYielded(): Create YieldedItem() object #6
TestYielded(): Valeur stockée est 4
BuildYielded(): Create YieldedItem() object #7
TestYielded(): Valeur stockée est 22
BuildYielded(): Create YieldedItem() object #8
TestYielded(): Valeur stockée est 6
BuildYielded(): Create YieldedItem() object #9
TestYielded(): Valeur stockée est 9
Les générateurs et comprehension list
Finalement, il est également possible d'associer les générateurs et les compréhensions lists (décrit en début d'article). le résultat étant appelé un "Generator Expression"!
Pour créer une générateur sur base d'une compréhension list, l'on utilise la syntaxe suivante où les parenthèses remplacent les crochets.

aGenerator = ( expression-manipulation for expression in sequence if test)

Un cas typique de son utilisation serait l'exécution de l'expression de manipulation (expression-manupilation) que lorsque l'on extrait l'information du generateur. Cela peut devenir utile si le processus est grand consommateur de ressource (comme des accès DB).
A Contrario, toutes les expressions de manipulation sont exécutées immédiatement lors de la création d'une expression list.

Voici un exemple extrait du module GenVsExpList.py .

""" Ce module d'exemple sert a mettre en évidence la différence de fonctionnement entre un Generator et une Comprehension List """

def transformIntForGen( iValue ):
 """Cette fonction de transformation est appelée pour
 transformer les valeurs d'un générateur d'expression.
 Grace à la trace, il est possible de déterminer le 
 mode de fonctionnement."""
 print( '%s: pour la value %s' % (
   'transforme for Generator', iValue) )
 return 100-iValue

def transformIntForCompList( iValue ):
 """Cette fonction de transformation est appelée pour
 transformer les valeurs d'une comprehension list.
 Grace à la trace, il est possible de déterminer le 
 mode de fonctionnement."""
 print( '%s: pour la value %s' % (
   'transform ComprehensionList', iValue) )
 return 100-iValue

def compareGenVsCompList():
 """Fonction de comparaison d'execution de generateur 
  et comprehension list"""
 print( '--- Comprehension List execution ---' )
 print( '1) prepare comprehension list' )
  _list = [ transformIntForCompList(i) for i in range(25) if i%3==0 ]
 print( '2) display comprehension list content' )
 for index in range( len(_list) ):
  print( 'Comprehension List value #%s : %s' % (
   index, _list[index]) )

 print( '--- Generator expression execution ---' )
 print( '1) prepare generator' )
  _gen = ( transformIntForGen(i) for i in range(25) if i%3==0 )
 print( '2) display generator expression content' )
 # Dans le cas d'un générayeur, il faut malheureusement
 #   gérer l'index soit même. 
 _idx = 0 
 for item in _gen: 
  print( 'Generator expression value #%s : %s' % (
   _idx, item) )
  _idx += 1

Le résultat démontre bien que dans le cas d'un generator expression, l'expression de transformation n'est appelée qu'au moment de l'extraction de la valeur!

>>> compareGenVsCompList()
--- Comprehension List execution ---
1) prepare comprehension list
transform ComprehensionList: pour la value 0
transform ComprehensionList: pour la value 3
transform ComprehensionList: pour la value 6
transform ComprehensionList: pour la value 9
transform ComprehensionList: pour la value 12
transform ComprehensionList: pour la value 15
transform ComprehensionList: pour la value 18
transform ComprehensionList: pour la value 21
transform ComprehensionList: pour la value 24
2) display comprehension list content
Comprehension List value #0 : 100
Comprehension List value #1 : 97
Comprehension List value #2 : 94
Comprehension List value #3 : 91
Comprehension List value #4 : 88
Comprehension List value #5 : 85
Comprehension List value #6 : 82
Comprehension List value #7 : 79
Comprehension List value #8 : 76
--- Generator expression execution ---
1) prepare generator
2) display generator expression content
transforme for Generator: pour la value 0
Generator expression value #0 : 100
transforme for Generator: pour la value 3
Generator expression value #1 : 97
transforme for Generator: pour la value 6
Generator expression value #2 : 94
transforme for Generator: pour la value 9
Generator expression value #3 : 91
transforme for Generator: pour la value 12
Generator expression value #4 : 88
transforme for Generator: pour la value 15
Generator expression value #5 : 85
transforme for Generator: pour la value 18
Generator expression value #6 : 82
transforme for Generator: pour la value 21
Generator expression value #7 : 79
transforme for Generator: pour la value 24
Generator expression value #8 : 76

1 commentaire:

Anonyme a dit…

C'est un peu rapide tout ça, et beaucoup de notions.

Vous pourriez développer un peu chaque notion, avec plus de détails ?

Un peu comme ça:

http://sametmax.com/python-love-les-listes-en-intention-partie/

Mais pour tous les sujets.