dimanche 6 décembre 2009

Structures en Python

Ce qui est un peu déstabilisant avec Python c'est qu'une partie des structures/stéréotypes que l'on identifieraient facilement en C ou Pascal portent des noms/stéréotypes différents en Python.
C'est ainsi qu'une "séquence" Python ressemble a s'y méprendre à un array en Pascal. C'est un peu déstabilisant lorsque l'on vient d'un autre langage.
Je vais donc passer un peu de temps à synthétiser ces structures en vue d'en faire un petit article.

3 décembre 2009:
Premier jet de l'article enfin terminé.  Comme pour beaucoup d'autres articles, il faut pouvoir trouver le temps nécessaire à la rédaction...
Si vous êtes déjà quelque peu familiarisé avec Python, jetez néanmoins un coup d'oeil sur la dernière section intitulée "Utilisation des mappings et optimisation". 

6 décembre 2009:

La presque totalité des exemples sont disponibles dans le fichier structures.py.

Les séquences
L'une des notions les plus importantes en Python est bien celle des séquences.
Les séquences sont des objets "container"... donc destinés à contenir/maintenir des données ou objets.
Le terme "séquence" est utilisé parce les objets de ce type sont "Itérable" (comprendre que la séquence peut être parcourue de façon "naturelle").
Le contenu de ces objets peut donc être facilement et systématiquement balayé.
Python dispose de plusieurs structures de données séquentielles (tuple, list, bytearray, str, unicode).

Certaines séquences sont dites immuables et les éléments individuels ne peuvent pas être modifiés, c'est le cas des chaines de caractères (str appelé string, unicode), les tuples et les type bytes.
Seul une opération de ré-affectation peut permettre la manipulation du contenu (phénomène identique au type string de la plateforme .Net).

D'un autre coté, les structures séquentielles tel que les lists et les array (bytearray) sont des séquences modifiable. En d'autre terme, il est possible de modifier les éléments la composant de façon individuel sans passer par des opérations de ré-affectation.

La programmation d'éléments/objets séquentiable (et donc itérable) est facile et autorise l'utilisation de syntaxe simple mais néanmoins très puissante.
Un exemple typique de la puissance de l'itération est l'instruction for en Python (équivalent du "foreach" en C#).

Les Tuples

Les tuples sont des séquences non modifiables (immuable pour reprendre le terme 
La notation des tuples ressemble fort aux types énumérés du Pascal sans pour autant avoir les même similitudes.
Les tuples permettent de maintenir une séquence de valeurs (de tous types, y compris des tuples eux-mêmes).
Cette structure immuable (dont on ne peut pas modifier indépendamment les valeurs) est principalement utilisée pour maintenir une séquence de "valeurs constantes".
Les tuples se montrent également fort utiles lorsqu'une fonction/méthode doit renvoyer des multiples valeurs. En pascal, on utiliserait un "record", dans ce cas préçis, pour retourner ces valeurs multiples. Python à l'avantage de permettre la définition/création du tuple (et types inclus) à la volée, ce qui offre une grande souplesse.
Il est possible d'accéder les différents éléments d'un tuple par l'intermédaire d'un index (0 to len-1) ou en utilisant les principes d'itération.
Bien qu'un peu déstabilisants dans un premier temps, en autre parce que le tuple semble faire double emploi avec les listes, on constate rapidement que les tuples sont des éléments de programmation efficace et puissant.

Finalement, bien qu'il ne soit pas possible de modifier indépendamment les éléments d'un tuple, il est possible de recomposer un nouveau tuple à partir d'un ancien tuple en effectuant une opération de ré-affectation:
monTuple = monTuple + ( AjouterCetteValeur )
Notez que la nouvelle valeur à inclure se trouve entre parenthèse (la notation d'un tuple).
En utilisant cette notation, on constate que le nouveau tuple n'est autre que le résultat de la concaténation de deux autres tuples.

# Utilisation de Tuple
def testTuple():
 print( '--- Tuple testing ------------------------' )
 # Déclaration avec valeur multiple
 myTuple = ( 'Une valeur', 128, None, 4+3 )
 # énumeration
 print( 'Enumeration Tuple' )
 for i in myTuple:
  print( '  %s' % i )
 # Test d'inclusion
 if 128 in myTuple:
  print( 'Contient la valeur 128' )
 # Un tuple est 'non modifiable'.
 # L'ajout de valeur passe par une operation d'assignation
 myTuple = myTuple + ('Un autre tuple', 4)
 
 # Déclaration d'un tuple vide
 anotherTuple = ()
 anotherTuple = anotherTuple + ( 1, 5, 7, 'a' )
 anotherTuple = anotherTuple + myTuple

 # L'execution de la ligne suivant produit une erreur
 try:
  anotherTuple = anotherTuple + 998 # Operation interdite
 except TypeError, err:
  print( 'Ajout de la valeur 998 au tuple produit '+
   'l\'erreur suivante' )  
  print( '  %s' % err ) # cette ligne sera executée
  
 # L'execution de la ligne suivant produit une erreur
 try:
  anotherTuple[0] = 'Modifier la valeur'
 except TypeError, err:
  print( 'Modifier directement la valeur d\'un element '+
         'd\un tuple produit l\erreur suivante' )
  print( '  %s' % err ) 

 # longueur et accès a un simple élément
 if len( anotherTuple )>0:
  print( 'longueur de anotherTuple= %s' % len(anotherTuple) )
  print( 'Deuxième valeur de anotherTuple= %s' % anotherTuple[1] )

 # Compostion d'un tuple complexe
 complexTuple = ( ['1','5','10'], anotherTuple, ('a','1'),
   ('b',('un','deux')) )
 print( 'contenu de complexTuple' )
 print( complexTuple )
 print( 'longueur de complexTuple= %s' % len( complexTuple ) )
 for i in range( len(complexTuple) ):
  print( '  élément %s est %s' % (i, complexTuple[i]) )

Liens utiles:
Les listes (séquence)
Les listes sont des éléments de type "séquence".

Python utilise une notation à l'aide de [ ] pour créer et manipuler les listes. L'instruction list() permet également de créer formellement une liste.
Bien que la notation et l'utilisation des liste soit similaire aux arrays du Pascal, il sont à ne  pas a confondre avec les arrays du Pascal!

Les éléments d'une liste sont néanmoins accessibles en utilisant un index (compris entre 0 et len-1).
Il est également possible d'extraire une séquence (sous liste) d'une liste donné.

Voici quelques exemples de manipulation de liste.

# Crée une liste vide
myEmptyList = []

# définition directe 
myList = [ '1', 123, 'Hello' ]

# définition à l'aide de list() et initialisation à l'aide d'une autre liste
myList4 = list( [1-2,'World','is','$T0n€'] )
# donnera le résultat [-1, 'World', 'is', '$T0n\x80']

# définition à l'aide de list() et initialization à l'aide d'un Tuple
myList3 = list( (1,'helloZ','test') )
# donnera le résultat [1, 'helloZ', 'test']

# définition à l'aide des méthodes de list()
myList5 = list()
myList5.append( 123 )
myList5.append( 'Zut' )
myList5.append( 'Il y a du monde' )
myList5
# donnera [123, 'Zut', 'Il y a du monde']

# Extraction d'élément par index 
print( myList5[0] )
# L'utilisation d'un index négatif permet d'accéder
#   aux éléments depuis la fin de la liste (Python
#   ajoute la longueur de la liste à l'index)
print( myList5[-1] )
# produira 'Il y a du monde'

# Extraction d'une séquence
myList6 = myList4[1:3]
myList6 
# produira le résultat ['World', 'is', '$T0n\x80']

Les listes (tout comme les Tuples) sont des éléments itérables.
Il est donc possible décrire un code comme le suivant:
for item in myList5:
     print( item )
ce qui produira le résultat:
123
Zut
Il y a du monde

Liens utiles:

Les arrays (séquence)
Le type bytearray est une structure de type séquence destiné a contenir des bytes.
Le type bytes (équivalent de str en Pyhton 2.6) sont immuables.
Les bytearray lui n'est pas immuable et peut donc être modifié. Ce type dispose d'ailleurs d'une grande partie des fonctions dévolues au type str.

Comparé aux autres types de séquences, les arrays (bytearrays) ne présente pas, de prime abord, d'intérêt particulier comme cela pourrait être le cas dans des langages comme C et Pascal.
En Pyhton, les types les plus intéressants restent quand même les list, tuples et mapping (ci-après)


Les types bytes et bytearray présente principalement un intérêt lors d'interfacage avec des API, des OS ou du materiel. Ces types permettent de transposer les notions et les structures de type "string" d'autres langages directement en Python. Le type bytearray est donc utilisé pour facilité l'interopérabilité.
A titre d'exemple, voici quelques références relatives a win32com, ironPython, Socket programming, etc.


Il est par contre important de noter une finesse de convertion entre les types str, bytes, bytearray à partir de Python 3.0. En effet, à partir de cette version, le type str sera unicode.
Plus d'information sont disponible le document "What's new in Python 2.6" à la section "Byte Literals" sur docs.python.org.

Les mappings (dictionnaire)
Bien qu'également itérable, les mappings ne sont pas des séquences à proprement parler.
Le mapping est un dictionnaire permettant d'associer et maintenir des pairs objet + clé (généralement des mots clés/chaines de caractères).
Les différents objets sont accessible via leur clé. Pour maintenir de bonne performance, les mappings utilisent des tables de hashing pour optimiser les temps d'accès/recherches.

L'exemple ci-dessous démontre l'usage général des mappings.
>>> # Definition d'un dictionnaire (directement)
>>> parents = { 'man' : 'dodo', 'woman' : 'fanfan' }
>>> parents
{'woman': 'fanfan', 'man': 'dodo'}
>>> for key, value in parents.items():
 print 'the %s is %s' % (key, value)

 
the woman is fanfan
the man is dodo

>>> # Definition d'un dictionnaire via l'objet dict()
>>> #   nb: il est également possible d'initialiser le 
>>> #   dictionnaire dans le constructeur
>>> kids = dict()
>>> kids['kid 1'] = 'didi'
>>> kids['kid 2'] = 'jess'
>>> kids['kid 3'] = 'ben'
>>> kids['kid 4'] = 'lili'
>>> kids
{'kid 1': 'didi', 'kid 3': 'ben', 'kid 2': 'jess', 'kid 4': 'lili'}
>>> familly = dict( parents )
>>> familly.update( kids )
>>> familly
{'woman': 'fanfan', 'kid 1': 'didi', 'kid 3': 'ben', 'kid 2': 'jess', 'kid 4': 'lili', 'man': 'dodo'}
>>> familly.get( 'kid2', 'NOT AVAILABLE' )
'NOT AVAILABLE'
>>> familly.get( 'kid 2', 'NOT AVAILABLE' )
'jess'
>>> familly.has_key( 'glops' )
False
>>> familly.has_key( 'man' )
True
>>> familly.iterkeys()
<dictionary-keyiterator object at 0x0119A2A0>
>>> for item in familly.iterkeys():
 print item

 
woman
kid 1
kid 3
kid 2
kid 4
man
>>> for iterKey, iterValue in familly.iteritems():
  print '%s -> %s' % (iterKey, iterValue)

  
woman -> fanfan
kid 1 -> didi
kid 3 -> ben
kid 2 -> jess
kid 4 -> lili
man -> dodo

Liens utiles
Utilisation des mappings et optimisation

Il faut cependant prendre note d'une notion importante relative à l'optimisation des programmes.
Il est par exemple possible d'obtenir les éléments d'un dictionnaire à l'aide de myDico.items() ou myDico.IterItems(). Un principe similaire s'applique également pour l'obtention séparée des clés ou valeurs.

La fonction items() crée une copie des éléments composant le mapping.
Cela consomme inévitablement de la mémoire. Il faudra l'utiliser avec attention pour évitant de l'appliquer sur des mappings de grande taille. Par ailleurs, le fait même de copier le contenu requière que la copie soit entièrement effectuée avant le retour d'appel de la fonction items().
Finalement, puisqu'il s'agit d'une copie, les modifications faites sur la copie ne sont pas remportées dans le dictionnaire source.

La fonction iteritems() crée un itérateur sur les éléments composant le mapping.
Le retour de fonction iteritem() est immédiat car il n'y a pas d'opération de copie.
Le contenu du dictionnaire ne doit même pas être dénombré/visité pour retourner un itérateur.
Cela permet d'économiser à la fois de la mémoire et les ressources CPU.
La charge CPU d'iteration sera répartie entre les différents appels (à l'itérateur) qui sera utilisé pour balayer le dictionnaire.
A noter que dans ce cas, les éléments du dictionnaire disponible dans l'itérateur ne sont également pas modifiable via les variables d'itération.
En effet, les variables d'itération contiennent une copie de la valeur/clé du dictionnaire et non une référence (ce qui les rendraient modifiable).

>>> familly.items()
[('woman', 'fanfan'), ('kid 1', 'didi'), ('kid 3', 'ben'), ('kid 2', 'jess'), ('kid 4', 'lili'), ('man', 'dodo')]

>>> # Utilisation d'un itérateur
>>> familly.iteritems()
<dictionary-itemiterator object at 0x0119A3C0>
>>> for iterKey, iterValue in familly.iteritems():
 if iterKey=='man':
  iterValue = 'Man will not be replaced (hehe!)'
  print 'replace attempt'

replace attempt
>>> familly.items()
[('woman', 'fanfan'), ('kid 1', 'didi'), ('kid 3', 'ben'), ('kid 2', 'jess'), ('kid 4', 'lili'), ('man', 'dodo')]

>>> # Utilisation d'une copie
>>> for iterKey, iterValue in familly.items():
 if iterKey=='man':
  iterValue = 'Man will not be replaced (neither!)'
  print 'Replace attempt 2'
  
Replace attempt 2
>>> familly.items()
[('woman', 'fanfan'), ('kid 1', 'didi'), ('kid 3', 'ben'), ('kid 2', 'jess'), ('kid 4', 'lili'), ('man', 'dodo')]

Aucun commentaire: