%

Python 3 Module of the Week - Chap 2 : Data Structures

Article publié, le et modifié le
69 minutes de lecture

Cet article contient 14561 mots.
Source brute de l'article :
Commit version : 9c0a93f

Structures de Données

Python inclut de nombreuses normes de programmation des structures de données, telles que list, tuple, dict, et set, dans le cadre de ses types natifs. Beaucoup d’application ne requièrent pas d’autres structures, mais quand elles le font, la bibliothèque standard fournit des versions puissantes et bien testées, prêtes à être utilisées.

Le module enum fournit une implémentation du type enumeration qui a des capacités d’itération et de comparaison. Il peut être utilisé pour créer des symboles bien définis pour les valeurs, à la place des chaînes ou entiers littéraux.

Le module collections inclus les implémentations de nombreuses structures de données qui étendent celles trouvées dans d’autres modules. Par exemple, Deque est une file d’attente double, qui permet l’addition ou la suppression d’éléments à chaque extrémité. defaultdict est un dictionnaire qui répond avec une valeur par défaut si une clé est manquante, alors que OrderedDict se souvient de la séquence dans laquelle les éléments sont ajoutés. namedtuple étend le tuple normal pour donner à chaque item membre un nom d’attribut en supplément de l’index numérique.

Pour de grandes quantités de données, un array peut utiliser plus efficacement la mémoire qu’une liste. Puisqu’un array est limité à un simple type de donnée, il peut utiliser une représentation de mémoire plus compacte qu’une list à usage général. En même temps, les instances array peuvent être manipulées en utilisant beaucoup des mêmes méthodes qu’une list, ainsi il peut être possible de remplacer une list avec un array sans faire beaucoup de changements.

Le tri d’items dans une séquence est un aspect fondamental de la manipulation de données. La list de Python inclus une méthode sort(), mais parfois il est plus efficient de maintenir une liste dans un ordre trié, sans la trier à nouveau, à chaque fois que son contenu a changé.

Les fonctions dans heapq modifient le contenu d’une liste tout en préservant l’ordre trié de la liste avec une faible surcharge.

Une autre option pour construire des listes triés ou des tableaux est bisect. Il utilise une recherche binaire pour trouver le point d’insertion des nouveaux items, et est une alternative au tri répété d’une liste qui change fréquemment.

Bien que la list native peut simuler une queue, en utilisant les méthodes insert() et pop(), ceci n’est pas sûr. Pour avoir de vraies communications ordonnées entre les threads, utilisez le module queue. multiprocessing inclus une version de Queue qui fonctionne entre processus, rendant plus facile la conversion d’un programme multithread pour utiliser des processus à la place.

struct est utile pour décoder les données d’une autre application, qui viennent peut être d’un fichier binaire ou d’un flux de données, dans les types natifs de Python pour une manipulation plus facile.

Ce chapitre couvre deux modules relatifs à la gestion de la mémoire. Pour des structures de données hautement interconnectées, telles que des graphiques et des arbres, utilisez weakref pour maintenir des références tout en permettant que le récupérateur de mémoire nettoie les objets après qu’ils ne soient plus nécessaires. Utilisez les fonctions dans copy pour dupliquer des structures de données et leurs contenus, incluant les copies récursives avec deepcopy().

Déboguer des structures de données peut consommer du temps, spécialement lorsque vous parcourez les sorties imprimées de grandes séquences ou de dictionnaires. Utilisez pprint pour créer des représentations faciles à lire qui peuvent être imprimées dans la console ou écrites dans un fichier journal pour déboguer facilement.

Enfin, si les types disponibles ne répondent pas aux exigences, sous-classez l’un des types natifs et personnalisez-le, ou créez un nouveau type de conteneur en utilisant l’une des classes de base abstraites définies dans les collections en tant que base de départ.

enum - Type d’Énumération

Le module enum définit un type d’énumération avec des capacités d’itération et de comparaison. Il peut être utilisé pour créer des symboles bien définis pour des valeurs, à la place des chaînes ou entiers littéraux.

Créer des Énumérations

Une nouvelle énumération est définie par l’utilisation de la syntaxe class en sous-classant Enum et en ajoutant des attributs de classe décrivant les valeurs.

# enum_create.py

import enum


class BugStatus(enum.Enum):

    new = 7
    incomplete = 6
    invalid = 5
    wont_fix = 4
    in_progress = 3
    fix_committed = 2
    fix_released = 1


print('\nMember name: {}'.format(BugStatus.wont_fix.name))
print('Member value: {}'.format(BugStatus.wont_fix.value))

Les membres de Enum sont convertis en instances à mesure que la classe est analysée. Chaque instance a une propriété name correspondant au nom de membre et une propriété value correspondante à la valeur assignée au nom dans la définition de classe.

$ python3 enum_create.py

Member name: wont_fix
Member value: 4

Itération

Itérer sur l’énumération class produit des membres individuels d’énumération.

# enum_iterate.py

import enum


class BugStatus(enum.Enum):

    new = 7
    incomplete = 6
    invalid = 5
    wont_fix = 4
    in_progress = 3
    fix_committed = 2
    fix_released = 1


for status in BugStatus:
    print('{:15} = {}'.format(status.name, status.value))

Les membres sont produits dans l’ordre dans lequel ils sont déclarés dans la définition de classe.

$ python3 enum_iterate.py

new             = 7
incomplete      = 6
invalid         = 5
wont_fix        = 4
in_progress     = 3
fix_committed   = 2
fix_released    = 1

Comparer les Énumérations

Parce que les membres d’énumération ne sont pas ordonnés, ils supportent la comparaison seulement par identité et égalité.

# enum_comparison.py

import enum


class BugStatus(enum.Enum):

    new = 7
    incomplete = 6
    invalid = 5
    wont_fix = 4
    in_progress = 3
    fix_committed = 2
    fix_released = 1


actual_state = BugStatus.wont_fix
desired_state = BugStatus.fix_released

print('Equality:',
      actual_state == desired_state,
      actual_state == BugStatus.wont_fix)
print('Identity:',
      actual_state is desired_state,
      actual_state is BugStatus.wont_fix)
print('Ordered by value:')
try:
    print('\n'.join('  ' + s.name for s in sorted(BugStatus)))
except TypeError as err:
    print('  Cannot sort: {}'.format(err))

Les opérateurs de comparaison plus-grand-que et plus-petit-que soulèvent des erreurs d’exceptions TypeErrors.

$ python3 enum_comparison.py

Equality: False True
Identity: False True
Ordered by value:
  Cannot sort: '<' not supported between instances of 'BugStatus
' and 'BugStatus'

Utilisez la classe IntEnum pour les énumérations dans laquelle les membres doivent se comporter comme des nombres - par exemple, pour supporter les comparaisons.

# enum_intenum.py

import enum


class BugStatus(enum.IntEnum):

    new = 7
    incomplete = 6
    invalid = 5
    wont_fix = 4
    in_progress = 3
    fix_committed = 2
    fix_released = 1


print('Ordered by value:')
print('\n'.join('  ' + s.name for s in sorted(BugStatus)))
$ python3 enum_intenum.py

Ordered by value:
  fix_released
  fix_committed
  in_progress
  wont_fix
  invalid
  incomplete
  new

Valeurs d’Énumération Uniques

Les membres d’énumération avec les mêmes valeurs sont suivis comme des références d’alias du même objet membre. Les alias n’entraînent pas la présence de valeurs répétées dans l’itérateur pour Enum.

# enum_aliases.py

import enum


class BugStatus(enum.Enum):

    new = 7
    incomplete = 6
    invalid = 5
    wont_fix = 4
    in_progress = 3
    fix_committed = 2
    fix_released = 1

    by_design = 4
    closed = 1


for status in BugStatus:
    print('{:15} = {}'.format(status.name, status.value))

print('\nSame: by_design is wont_fix: ',
      BugStatus.by_design is BugStatus.wont_fix)
print('Same: closed is fix_released: ',
      BugStatus.closed is BugStatus.fix_released)

Parce que by_design et closed sont des alias pour d’autres membres, ils n’apparaissent pas séparément dans la sortie lors de l’itération de Enum. Le nom canonique pour un membre est le premier nom attaché à la valeur.

$ python3 enum_aliases.py

new             = 7
incomplete      = 6
invalid         = 5
wont_fix        = 4
in_progress     = 3
fix_committed   = 2
fix_released    = 1

Same: by_design is wont_fix:  True
Same: closed is fix_released:  True

Pour que tous les membres aient des valeurs uniques, ajoutez le décorateur @unique à Enum.

# enum_unique_enforce.py

import enum


@enum.unique
class BugStatus(enum.Enum):

    new = 7
    incomplete = 6
    invalid = 5
    wont_fix = 4
    in_progress = 3
    fix_committed = 2
    fix_released = 1

    # This will trigger an error with unique applied.
    by_design = 4
    closed = 1

Les membres qui ont des valeurs répétées déclenchent une erreur d’exception ValueError quand la classe Enum est interprétée.

$ python3 enum_unique_enforce.py

Traceback (most recent call last):
  File "enum_unique_enforce.py", line 11, in <module>
    class BugStatus(enum.Enum):
  File ".../lib/python3.7/enum.py", line 848, in unique
    (enumeration, alias_details))
ValueError: duplicate values found in <enum 'BugStatus'>:
by_design -> wont_fix, closed -> fix_released

Création d’Énumérations par Programme

Dans certains cas, il est plus pratique de créer des énumérations par programme, plutôt que les coder en dur dans une définition de classe. Pour ces situations, Enum prend aussi en charge le fait de passer les noms des membres et les valeurs au constructeur de classe.

# enum_programmatic_create.py

import enum


BugStatus = enum.Enum(
    value='BugStatus',
    names=('fix_released fix_committed in_progress '
           'wont_fix invalid incomplete new'),
)

print('Member: {}'.format(BugStatus.new))

print('\nAll members:')
for status in BugStatus:
    print('{:15} = {}'.format(status.name, status.value))

L’argument value est le nom de l’énumération, qui est utilisée pour construire la représentation des membres. L’argument names liste les membres de l’énumération. Quand une chaîne unique est passée, elle est divisée en espaces et en virgules, et les jetons résultants sont utilisés comme noms pour les membres, auxquels sont automatiquement attribuées des valeurs commençant par 1.

$ python3 enum_programmatic_create.py

Member: BugStatus.new

All members:
fix_released    = 1
fix_committed   = 2
in_progress     = 3
wont_fix        = 4
invalid         = 5
incomplete      = 6
new             = 7

Pour plus de contrôles sur les valeurs associées aux membres, la chaîne names peut être remplacée par une séquence de deux tuples ou un dictionnaire faisant correspondre des noms aux valeurs.

# enum_programmatic_mapping.py

import enum


BugStatus = enum.Enum(
    value='BugStatus',
    names=[
        ('new', 7),
        ('incomplete', 6),
        ('invalid', 5),
        ('wont_fix', 4),
        ('in_progress', 3),
        ('fix_committed', 2),
        ('fix_released', 1),
    ],
)

print('All members:')
for status in BugStatus:
    print('{:15} = {}'.format(status.name, status.value))

Dans cet exemple, une liste de deux tuples est donnée au lieu d’une chaîne unique contenant seulement les noms des membres. Cela permet de reconstruire l’énumération BugStatus avec les membres dans le même ordre que la version définie dans enum_create.py.

$ python3 enum_programmatic_mapping.py

All members:
new             = 7
incomplete      = 6
invalid         = 5
wont_fix        = 4
in_progress     = 3
fix_committed   = 2
fix_released    = 1

Valeurs des Membres Non Entiers

Les valeurs des membres d’énumération ne sont pas restreints aux entiers. En fait, tout type d’objet peut être associé avec un membre. Si la valeur est un tuple, les membres sont passés en tant qu’argument individuel à __init__().

# enum_tuple_values.py

import enum


class BugStatus(enum.Enum):

    new = (7, ['incomplete',
               'invalid',
               'wont_fix',
               'in_progress'])
    incomplete = (6, ['new', 'wont_fix'])
    invalid = (5, ['new'])
    wont_fix = (4, ['new'])
    in_progress = (3, ['new', 'fix_committed'])
    fix_committed = (2, ['in_progress', 'fix_released'])
    fix_released = (1, ['new'])

    def __init__(self, num, transitions):
        self.num = num
        self.transitions = transitions

    def can_transition(self, new_state):
        return new_state.name in self.transitions


print('Name:', BugStatus.in_progress)
print('Value:', BugStatus.in_progress.value)
print('Custom attribute:', BugStatus.in_progress.transitions)
print('Using attribute:',
      BugStatus.in_progress.can_transition(BugStatus.new))

Dans cet exemple, chaque valeur de membre est un tuple contenant l’ID numérique (qui peut être enregistré dans une base de données) et une liste de transitions valide, sortant de l’état actuel.

$ python3 enum_tuple_values.py

Name: BugStatus.in_progress
Value: (3, ['new', 'fix_committed'])
Custom attribute: ['new', 'fix_committed']
Using attribute: True

Pour les cas les plus complexes, les tuples peuvent devenir difficiles à manier. Puisque les valeurs des membres peuvent être n’importe quel type d’objet, les dictionnaires peuvent être utilisés pour les cas où il y a beaucoup d’attributs distincts à suivre pour chaque valeur d’énumération. Les valeurs complexes sont directement transmises à __init __ () en tant que seul argument autre que self.

# enum_complex_values.py

import enum


class BugStatus(enum.Enum):

    new = {
        'num': 7,
        'transitions': [
            'incomplete',
            'invalid',
            'wont_fix',
            'in_progress',
        ],
    }
    incomplete = {
        'num': 6,
        'transitions': ['new', 'wont_fix'],
    }
    invalid = {
        'num': 5,
        'transitions': ['new'],
    }
    wont_fix = {
        'num': 4,
        'transitions': ['new'],
    }
    in_progress = {
        'num': 3,
        'transitions': ['new', 'fix_committed'],
    }
    fix_committed = {
        'num': 2,
        'transitions': ['in_progress', 'fix_released'],
    }
    fix_released = {
        'num': 1,
        'transitions': ['new'],
    }

    def __init__(self, vals):
        self.num = vals['num']
        self.transitions = vals['transitions']

    def can_transition(self, new_state):
        return new_state.name in self.transitions


print('Name:', BugStatus.in_progress)
print('Value:', BugStatus.in_progress.value)
print('Custom attribute:', BugStatus.in_progress.transitions)
print('Using attribute:',
      BugStatus.in_progress.can_transition(BugStatus.new))

Cet exemple exprime les mêmes données que l’exemple précédent, utilisant des dictionnaires plutôt que des tuples.

$ python3 enum_complex_values.py

Name: BugStatus.in_progress
Value: {'num': 3, 'transitions': ['new', 'fix_committed']}
Custom attribute: ['new', 'fix_committed']
Using attribute: True

Lire aussi

collections - Types de Données de Conteneur

But : Types de données de conteneur.

Le module collections inclus les types de données de conteneur au-delà des types natifs list, dict et tuple.

Lire aussi

ChainMap - Recherche dans plusieurs dictionnaires

La classe ChainMap gère une séquence de dictionnaires, et recherche au-travers, dans l’ordre où ils sont donnés, pour trouver les valeurs associées aux clés. Une ChainMap constitue un bon conteneur “contextuel”, car il peut être traité comme une pile pour laquelle des modifications sont apportées à mesure que la pile grandit, ces modifications étant à nouveau ignorées à mesure que la pile se réduit.

Accéder à des valeurs

La ChainMap supporte la même API qu’une dictionnaire classique pour accéder aux valeurs existantes.

# collections_chainmap_read.py

import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)

print('Individual Values')
print('a = {}'.format(m['a']))
print('b = {}'.format(m['b']))
print('c = {}'.format(m['c']))
print()

print('Keys = {}'.format(list(m.keys())))
print('Values = {}'.format(list(m.values())))
print()

print('Items:')
for k, v in m.items():
    print('{} = {}'.format(k, v))
print()

print('"d" in m: {}'.format(('d' in m)))

Les correspondances sous-jacentes font l’objet d’une recherche dans l’ordre dans lequel elles ont été transmises au constructeur, ainsi la valeur indiquée pour la clé c provient du dictionnaire a.

$ python3 collections_chainmap_read.py

Individual Values
a = A
b = B
c = C

Keys = ['b', 'c', 'a']
Values = ['B', 'C', 'A']

Items:
b = B
c = C
a = A

"d" in m: False

Réordonner

La ChainMap enregistre dans son attribut maps une liste de correspondances, sur laquelle elle fait sa recherche. Cette liste est mutable, ainsi il est possible d’ajouter de nouvelles correspondances directement ou de changer l’ordre des éléments pour contrôler la recherche et le comportement de mise à jour.

# collections_chainmap_reorder.py

import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)

print(m.maps)
print('c = {}\n'.format(m['c']))

# reverse the list
m.maps = list(reversed(m.maps))

print(m.maps)
print('c = {}'.format(m['c']))

Quand la liste de correspondances est renversée, la valeur associée à c change.

$ python3 collections_chainmap_reorder.py

[{'a': 'A', 'c': 'C'}, {'b': 'B', 'c': 'D'}]
c = C

[{'b': 'B', 'c': 'D'}, {'a': 'A', 'c': 'C'}]
c = D

Mise à jour des Valeurs

Une ChainMap ne met pas en cache les valeurs dans les correspondances sous-jacentes. Ainsi, si leur contenu est modifié, les résultats sont reflétés lors de l’accès à ChainMap.

# collections_chainmap_update_behind.py

import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)
print('Before: {}'.format(m['c']))
a['c'] = 'E'
print('After : {}'.format(m['c']))

Changer les valeurs associées avec les clés existantes et ajouter de nouveaux éléments fonctionne de la même manière.

$ python3 collections_chainmap_update_behind.py

Before: C
After : E

Il est également possible de définir directement des valeurs via ChainMap, bien que seule la première correspondance de la chaîne soit réellement modifiée.

# collections_chainmap_update_directly.py

import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)
print('Before:', m)
m['c'] = 'E'
print('After :', m)
print('a:', a)

Quand la nouvelle valeur est enregistrée en utilisant m, la correspondance a est mise à jour.

$ python3 collections_chainmap_update_directly.py

Before: ChainMap({'a': 'A', 'c': 'C'}, {'b': 'B', 'c': 'D'})
After : ChainMap({'a': 'A', 'c': 'E'}, {'b': 'B', 'c': 'D'})
a: {'a': 'A', 'c': 'E'}

ChainMap fournit une méthode pratique pour créer une nouvelle instance avec une correspondance supplémentaire au début de la liste des correspondances pour éviter de modifier les structures de données sous-jacentes existantes.

# collections_chainmap_new_child.py

import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m1 = collections.ChainMap(a, b)
m2 = m1.new_child()

print('m1 before:', m1)
print('m2 before:', m2)

m2['c'] = 'E'

print('m1 after:', m1)
print('m2 after:', m2)

Ce comportement d’empilement est pratique pour utiliser les instances de ChainMap comme contextes de modèles ou d’applications. Il est spécifiquement facile d’ajouter ou de mettre à jour des valeurs en une itération, puis d’annuler les changements pour la prochaine itération.

$ python3 collections_chainmap_new_child.py

m1 before: ChainMap({'a': 'A', 'c': 'C'}, {'b': 'B', 'c': 'D'})
m2 before: ChainMap({}, {'a': 'A', 'c': 'C'}, {'b': 'B', 'c':
'D'})
m1 after: ChainMap({'a': 'A', 'c': 'C'}, {'b': 'B', 'c': 'D'})
m2 after: ChainMap({'c': 'E'}, {'a': 'A', 'c': 'C'}, {'b': 'B',
'c': 'D'})

Pour les situations où le nouveau contexte est connu ou natif à l’avance, il est aussi possible de passer une correspondance à new_child().

# collections_chainmap_new_child_explicit.py

import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}
c = {'c': 'E'}

m1 = collections.ChainMap(a, b)
m2 = m1.new_child(c)

print('m1["c"] = {}'.format(m1['c']))
print('m2["c"] = {}'.format(m2['c']))

Ce qui est l’équivalent de :

m2 = collections.ChainMap(c, *m1.maps)

et produit :

$ python3 collections_chainmap_new_child_explicit.py

m1["c"] = C
m2["c"] = E

Compter les objets hachables

Un Counter est un conteneur qui garde une trace du nombre de fois où des valeurs équivalentes sont ajoutées. Il peut être utilisé pour implémenter les mêmes algorithmes pour lesquels d’autres langages utilisent couramment des structures de données en sacs ou en multi-ensembles.

Initialisation

Counter prend en charge trois formes d’initialisation. Son constructeur peut être appelé avec une séquence d’items, un dictionnaire contenant des clés et des compteurs, ou en utilisant des arguments de mots-clés qui associent les noms de chaîne aux compteurs.

# collections_counter_init.py

import collections

print(collections.Counter(['a', 'b', 'c', 'a', 'b', 'b']))
print(collections.Counter({'a': 2, 'b': 3, 'c': 1}))
print(collections.Counter(a=2, b=3, c=1))

Les résultats des trois formes d’initialisation sont les mêmes.

$ python3 collections_counter_init.py

Counter({'b': 3, 'a': 2, 'c': 1})
Counter({'b': 3, 'a': 2, 'c': 1})
Counter({'b': 3, 'a': 2, 'c': 1})

Un Counter vide peut être construit sans arguments et rempli via la méthode update().

# collections_counter_update.py

import collections

c = collections.Counter()
print('Initial :', c)

c.update('abcdaab')
print('Sequence:', c)

c.update({'a': 1, 'd': 5})
print('Dict    :', c)

Les valeurs comptées sont augmentées en se basant sur les nouvelles données, plutôt que remplacées. Dans l’exemple précédent, le compteur pour a va de 3 à 4.

$ python3 collections_counter_update.py

Initial : Counter()
Sequence: Counter({'a': 3, 'b': 2, 'c': 1, 'd': 1})
Dict    : Counter({'d': 6, 'a': 4, 'b': 2, 'c': 1})

Accéder aux Compteurs

Une fois qu’un Counter est rempli, ses valeurs peuvent être récupérées en utilisant l’API de dictionnaire.

# collections_counter_get_values.py

import collections

c = collections.Counter('abcdaab')

for letter in 'abcde':
    print('{} : {}'.format(letter, c[letter]))

Le Counter ne soulève pas d’erreur KeyError pour les items inconnus. Si une valeur n’est pas vue dans l’entrée (comme c’est le cas dans l’exemple avec e), son compteur est à 0.

$ python3 collections_counter_get_values.py

a : 3
b : 2
c : 1
d : 1
e : 0

La méthode elements() retourne un itérateur qui produit tous les items connus de Counter.

# collections_counter_elements.py

import collections

c = collections.Counter('extremely')
c['z'] = 0
print(c)
print(list(c.elements()))

L’ordre des éléments n’est pas garanti, et les items avec des compteurs inférieurs ou égales à zéro ne sont pas inclus.

$ python3 collections_counter_elements.py

Counter({'e': 3, 'x': 1, 't': 1, 'r': 1, 'm': 1, 'l': 1, 'y': 1,
'z': 0})
['e', 'e', 'e', 'x', 't', 'r', 'm', 'l', 'y']

Utilisez most_common()pour produire une séquence de n valeurs d’entrées les plus fréquemment rencontrées et de leurs compteurs respectifs.

# collections_counter_most_common.py

import collections

c = collections.Counter()
with open('/usr/share/dict/words', 'rt') as f:
    for line in f:
        c.update(line.rstrip().lower())

print('Most common:')
for letter, count in c.most_common(3):
    print('{}: {:>7}'.format(letter, count))

Cet exemple compte les lettres apparaissant dans tous les mots du dictionnaire système pour produire une distribution de fréquence, puis imprime les trois lettres les plus courantes. Si vous omettez l’argument à most_common (), vous obtenez une liste de tous les éléments, par ordre de fréquence.

$ python3 collections_counter_most_common.py

Most common:
e:  235331
i:  201032
a:  199554

Arithmétique

Les instances de Counter supportent l’arithmétique et paramètrent des opérations pour l’agrégation des résultats. Cet exemple montre les opérateurs standards pour créer de nouvelles instances de Counter, mais les opérateurs sur place +=, -=, &=, et |= sont aussi supportés.

# collections_counter_arithmetic.py

import collections

c1 = collections.Counter(['a', 'b', 'c', 'a', 'b', 'b'])
c2 = collections.Counter('alphabet')

print('C1:', c1)
print('C2:', c2)

print('\nCombined counts:')
print(c1 + c2)

print('\nSubtraction:')
print(c1 - c2)

print('\nIntersection (taking positive minimums):')
print(c1 & c2)

print('\nUnion (taking maximums):')
print(c1 | c2)

Chaque fois qu’un nouveau Counter est produit par opération, chaque item avec un compteur zéro ou négatif est supprimé. Le compteur pour a est le même dans c1 et c2, la soustraction le laisse à zéro.

$ python3 collections_counter_arithmetic.py

C1: Counter({'b': 3, 'a': 2, 'c': 1})
C2: Counter({'a': 2, 'l': 1, 'p': 1, 'h': 1, 'b': 1, 'e': 1, 't': 1})

Combined counts:
Counter({'a': 4, 'b': 4, 'c': 1, 'l': 1, 'p': 1, 'h': 1, 'e': 1, 't': 1})

Subtraction:
Counter({'b': 2, 'c': 1})

Intersection (taking positive minimums):
Counter({'a': 2, 'b': 1})

Union (taking maximums):
Counter({'b': 3, 'a': 2, 'c': 1, 'l': 1, 'p': 1, 'h': 1, 'e': 1, 't': 1})

defaultdict - Retourner une Valeur par Défaut aux Clés Manquantes

Le dictionnaire standard inclut la méthode setdefault() pour récupérer une valeur et en établir une par défaut si la valeur n’existe pas. Par contraste, defaultdict permet à l’appelant de spécifier la valeur par défaut dès l’initialisation du conteneur.

# collections_defaultdict.py

import collections


def default_factory():
    return 'default value'


d = collections.defaultdict(default_factory, foo='bar')
print('d:', d)
print('foo =>', d['foo'])
print('bar =>', d['bar'])

Cette méthode fonctionne aussi dans la mesure où il est approprié que toutes les clés aient la même valeur par défaut. Elle peut être spécialement utile si la valeur par défaut est un type utilisé pour l’agrégation ou l’accumulation de valeurs, tels que list, set, ou même int. La documentation standard de la bibliothèque inclus de nombreux exemples dans lesquels defaultdict est utilisé de cette manière.

$ python3 collections_defaultdict.py

d: defaultdict(<function default_factory at 0x101341950>,
{'foo': 'bar'})
foo => bar
bar => default value

Lire aussi

deque - File d’Attente Double

Une file d’attente double, ou deque prend en charge l’ajout ou la suppression d’éléments depuis chaque extrémité de la queue. Les piles et les files d’attente les plus couramment utilisées sont des formes dégénérées de deques où les entrées et les sorties sont restreintes à une seule extrémité.

# collections_deque.py

import collections

d = collections.deque('abcdefg')
print('Deque:', d)
print('Length:', len(d))
print('Left end:', d[0])
print('Right end:', d[-1])

d.remove('c')
print('remove(c):', d)

Puisque les deques sont un type de conteneur de séquences, ils prennent en charge certaines des opérations similaires à list, tel que l’examen des contenus avec __getitem__(), déterminer la longueur, et la suppression des éléments depuis le milieu de la queue par correspondance d’identité.

$ python3 collections_deque.py

Deque: deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
Length: 7
Left end: a
Right end: g
remove(c): deque(['a', 'b', 'd', 'e', 'f', 'g'])

Remplissage

Une deque peut être remplis depuis chaque extrémité, décrite à «gauche» ou à «droite» dans l’implémentation Python.

# collections_deque_populating.py

import collections

# Add to the right
d1 = collections.deque()
d1.extend('abcdefg')
print('extend    :', d1)
d1.append('h')
print('append    :', d1)

# Add to the left
d2 = collections.deque()
d2.extendleft(range(6))
print('extendleft:', d2)
d2.appendleft(6)
print('appendleft:', d2)

La fonction extendleft() effectue une itération sur son entrée et effectue l’équivalent d’un appendleft() pour chaque item. Le résultat final est que deque contient la séquence d’entrée dans l’ordre inverse.

$ python3 collections_deque_populating.py

extend    : deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
append    : deque(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
extendleft: deque([5, 4, 3, 2, 1, 0])
appendleft: deque([6, 5, 4, 3, 2, 1, 0])

Consommation

De la même manière, les éléments de deque peuvent être consommés depuis les deux extrémités, ou de l’une, selon l’algorithme qui est appliqué.

# collections_deque_consuming.py

import collections

print('From the right:')
d = collections.deque('abcdefg')
while True:
    try:
        print(d.pop(), end='')
    except IndexError:
        break
print()

print('\nFrom the left:')
d = collections.deque(range(6))
while True:
    try:
        print(d.popleft(), end='')
    except IndexError:
        break
print()

Utilisez pop() pour supprimer un item depuis l’extrémité «droite» de deque et popleft() pour prendre un item depuis l’extrémité «gauche».

$ python3 collections_deque_consuming.py

From the right:
gfedcba

From the left:
012345

Puisque les deques sont des processus sûrs (dits «thead-safe»), les contenus peuvent être consommés depuis chaque extrémité en même temps, depuis des processus séparés.

# collections_deque_both_ends.py

import collections
import threading
import time

candle = collections.deque(range(5))


def burn(direction, nextSource):
    while True:
        try:
            next = nextSource()
        except IndexError:
            break
        else:
            print('{:>8}: {}'.format(direction, next))
            time.sleep(0.1)
    print('{:>8} done'.format(direction))
    return


left = threading.Thread(target=burn,
                        args=('Left', candle.popleft))
right = threading.Thread(target=burn,
                         args=('Right', candle.pop))

left.start()
right.start()

left.join()
right.join()

Les processus dans cet exemple alternent entre chaque extrémité, supprimant les éléments jusqu’à ce que deque soit vide.

 $ python3 collections_deque_both_ends.py

 Left: 0
Right: 4
Right: 3
 Left: 1
Right: 2
 Left done
Right done

Rotation

Un autre aspect utile de deque est la capacité à se retourner dans chaque direction, et ainsi à sauter par dessus certains items.

# collections_deque_rotate.py

import collections

d = collections.deque(range(10))
print('Normal        :', d)

d = collections.deque(range(10))
d.rotate(2)
print('Right rotation:', d)

d = collections.deque(range(10))
d.rotate(-2)
print('Left rotation :', d)

La rotation de deque par la droite (utilisant la rotation positive) prend les éléments depuis l’extrémité droite et les déplacer à l’extrémité gauche. Les retourner par la gauche (avec une valeur négative) prend les items depuis l’extrémité gauche et les déplacent vers l’extrémité droite. Il peut être utile de visualiser les éléments dans la deque comme étant gravés le long du bord d’un cadran.

$ python3 collections_deque_rotate.py

Normal        : deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Right rotation: deque([8, 9, 0, 1, 2, 3, 4, 5, 6, 7])
Left rotation : deque([2, 3, 4, 5, 6, 7, 8, 9, 0, 1])

Contraindre la Taille de la Queue

Une instance deque peut être configuré avec une longueur maximale, ainsi elle ne pourra jamais grandir plus que sa taille. Quand la file d’attente atteint la taille spécifiée, les éléments existants sont abandonnés au profit des nouveaux items ajoutés. Ce comportement est pratique pour chercher les n derniers éléments d’un flux d’une longueur indéterminé.

# collections_deque_maxlen.py

import collections
import random

# Set the random seed so we see the same output each time
# the script is run.
random.seed(1)

d1 = collections.deque(maxlen=3)
d2 = collections.deque(maxlen=3)

for i in range(5):
    n = random.randint(0, 100)
    print('n =', n)
    d1.append(n)
    d2.appendleft(n)
    print('D1:', d1)
    print('D2:', d2)

La longueur de la file d’attente est maintenue quelque soit l’extrémité à laquelle sont ajoutés les éléments.

$ python3 collections_deque_maxlen.py

n = 17
D1: deque([17], maxlen=3)
D2: deque([17], maxlen=3)
n = 72
D1: deque([17, 72], maxlen=3)
D2: deque([72, 17], maxlen=3)
n = 97
D1: deque([17, 72, 97], maxlen=3)
D2: deque([97, 72, 17], maxlen=3)
n = 8
D1: deque([72, 97, 8], maxlen=3)
D2: deque([8, 97, 72], maxlen=3)
n = 32
D1: deque([97, 8, 32], maxlen=3)
D2: deque([32, 8, 97], maxlen=3)

Lire aussi

  • Wikipedia : Deque - Une discussion sur la structure de données deque (en anglais)
  • Recettes Deque - Exemples d’utilisations des files d’attente depuis la documentation de la bibliothèque standard.

namedtuple - Sous-classe de tuple avec champs nommés

Le tuple standard utilise les index numériques pour accéder à ses membres.

# collections_tuple.py

bob = ('Bob', 30, 'male')
print('Representation:', bob)

jane = ('Jane', 29, 'female')
print('\nField by index:', jane[0])

print('\nFields by index:')
for p in [bob, jane]:
    print('{} is a {} year old {}'.format(*p))

Ce qui fait des tuples des conteneurs pratiques pour des utilisations simples.

$ python3 collections_tuple.py

Representation: ('Bob', 30, 'male')

Field by index: Jane

Fields by index:
Bob is a 30 year old male
Jane is a 29 year old female

Par contraste, se souvenir quel index utiliser pour chaque valeur peut entraîner des erreurs, spécialement si le tuple a beaucoup de champs et est construit loin de l’endroit où il est utilisé. Un namedtuple assigne des noms, aussi bien que des index numériques, pour chaque membre.

Définitions

Les instances namedtuple sont juste plus efficient en mémoire que les tuples ordinaires car ils n’ont pas de dictionnaires par instance. Chaque type de namedtuple est représenté par sa propre classe, qui est créée en utilisant la fonction de fabrication namedtuple(). Les arguments sont des noms de la nouvelle classe et une chaîne contenant les noms des éléments.

# collections_namedtuple_person.py

import collections

Person = collections.namedtuple('Person', 'name age')

bob = Person(name='Bob', age=30)
print('\nRepresentation:', bob)

jane = Person(name='Jane', age=29)
print('\nField by name:', jane.name)

print('\nFields by index:')
for p in [bob, jane]:
    print('{} is {} years old'.format(*p))

Comme l’exemple l’illustre, il est possible d’accéder aux champs du namedtuple par nom en utilisant la notation (obj.attr) aussi bien que par l’utilisation des index positionnés des tuples standards.

$ python3 collections_namedtuple_person.py


Representation: Person(name='Bob', age=30)

Field by name: Jane

Fields by index:
Bob is 30 years old
Jane is 29 years old

Tout comme un tuple ordinaire, un namedtuple est immuable. Cette restriction permet aux instances tuples d’avoir une valeur de hachage cohérente, ce qui rend possible leur utilisation comme clés de dictionnaires et d’être incluses dans des ensembles.

# collections_namedtuple_immutable.py

import collections

Person = collections.namedtuple('Person', 'name age')

pat = Person(name='Pat', age=12)
print('\nRepresentation:', pat)

pat.age = 21

Essayer de changer sa valeur par son nom d’attribut résulte en une erreur AttributError.

$ python3 collections_namedtuple_immutable.py


Representation: Person(name='Pat', age=12)
Traceback (most recent call last):
  File "collections_namedtuple_immutable.py", line 17, in
<module>
    pat.age = 21
AttributeError: can't set attribute

Noms de Champs Invalides

Les noms de champs sont invalides s’ils sont répétés ou en conflit avec des mots clés Python.

# collections_namedtuple_bad_fields.py

import collections

try:
    collections.namedtuple('Person', 'name class age')
except ValueError as err:
    print(err)

try:
    collections.namedtuple('Person', 'name age age')
except ValueError as err:
    print(err)

Comme les noms des champs sont analysés, les valeurs invalides causent des erreurs d’exceptions ValueError.

$ python3 collections_namedtuple_bad_fields.py

Type names and field names cannot be a keyword: 'class'
Encountered duplicate field name: 'age'

Dans les situations où un namedtuple est créé sur la base des valeurs en-dehors du contrôle du programme (telle que représenter les lignes renvoyées par une requête de base de données, lorsque le schéma n’est pas connu à l’avance), l’option rename doit être paramétrée à True pour que les champs invalides soient renommés.

# collections_namedtuple_rename.py

import collections

with_class = collections.namedtuple(
    'Person', 'name class age',
    rename=True)
print(with_class._fields)

two_ages = collections.namedtuple(
    'Person', 'name age age',
    rename=True)
print(two_ages._fields)

Les nouveaux noms des champs renommés dépendent de leur index dans le tuple, ainsi le champ avec le nom class devient _1 et le champ dupliqué age est changé en _2.

$ python3 collections_namedtuple_rename.py

('name', '_1', 'age')
('name', 'age', '_2')

Attributs Spéciaux

namedtuple fournit de nombreux attributs et méthodes pratiques pour travailler avec les sous-classes et les instances. Toutes ces propriétés natives ont des noms préfixés avec un tiret bas (_), qui par convention dans la plupart des programmes Python indique un attribut privé. Pour namedtuple, cependant, le préfixe est intentionnel pour protéger le nom d’une collision avec les noms d’attributs fournis par utilisateur.

Les noms de ces champs passés à namedtuple pour définir de nouvelle classe sont sauvegardés sont l’attribut _fields.

# collections_namedtuple_fields.py

import collections

Person = collections.namedtuple('Person', 'name age')

bob = Person(name='Bob', age=30)
print('Representation:', bob)
print('Fields:', bob._fields)

Bien que l’argument soit une seule chaîne séparée par des espaces, la valeur enregistrée est la séquence de noms individuels.

$ python3 collections_namedtuple_fields.py

Representation: Person(name='Bob', age=30)
Fields: ('name', 'age')

Les instances namedtuple peuvent être converties en instances OrderedDict par l’usage de _asdict().

# collections_namedtuple_asdict.py

import collections

Person = collections.namedtuple('Person', 'name age')

bob = Person(name='Bob', age=30)
print('Representation:', bob)
print('As Dictionary:', bob._asdict())

Les clés de OrderedDict sont dans le même ordre que les champs du namedtuple.

$ python3 collections_namedtuple_asdict.py

Representation: Person(name='Bob', age=30)
As Dictionary: OrderedDict([('name', 'Bob'), ('age', 30)])

La méthode _replace() construit une nouvelle instance, en remplaçant les valeur de certains champs dans le processus.

# collections_namedtuple_replace.py

import collections

Person = collections.namedtuple('Person', 'name age')

bob = Person(name='Bob', age=30)
print('\nBefore:', bob)
bob2 = bob._replace(name='Robert')
print('After:', bob2)
print('Same?:', bob is bob2)

Bien que le nom implique que l’objet existant ait été modifié, parce que les instances namedtuple sont immuables, la méthode retourne actuellement un nouvel objet.

$ python3 collections_namedtuple_replace.py


Before: Person(name='Bob', age=30)
After: Person(name='Robert', age=30)
Same?: False

OrderedDict - Se souvenir de l’ordre des clés ajoutées à un dictionnaire

Un OrderedDict est une sous-classe de dictionnaire qui se souvient de l’ordre dans lequel son contenu est ajouté.

# collections_ordereddict_iter.py

import collections

print('Regular dictionary:')
d = {}
d['a'] = 'A'
d['b'] = 'B'
d['c'] = 'C'

for k, v in d.items():
    print(k, v)

print('\nOrderedDict:')
d = collections.OrderedDict()
d['a'] = 'A'
d['b'] = 'B'
d['c'] = 'C'

for k, v in d.items():
    print(k, v)

Avant Python 3.6 un dict ordinaire ne piste pas l’ordre d’insertion, et itère dessus les valeurs produites dans l’ordre en fonction de la manière dont les clés étaient stockées dans la table de hachage, qui à son tour, est influencé par une valeur aléatoire pour réduire les collisions. Dans un OrderedDict, par contraste, l’ordre dans lequel les éléments sont insérés est retenu et utilisé lors de la création de l’itérateur.

$ python3.5 collections_ordereddict_iter.py

Regular dictionary:
c C
b B
a A

OrderedDict:
a A
b B
c C

Sous Python 3.6, le natif dict piste l’ordre d’insertion, bien que ce comportement soit l’un des effets secondaires d’un changement d’implémentation, il ne faut pas s’y fier.

$ python3.6 collections_ordereddict_iter.py

Regular dictionary:
a A
b B
c C

OrderedDict:
a A
b B
c C

Égalité

Un dict ordinaire cherche son contenu lorsqu’il teste l’égalité. Un OrderedDict considère lui aussi l’ordre dans lequel les items sont ajoutés.

# collections_ordereddict_equality.py

import collections

print('dict       :', end=' ')
d1 = {}
d1['a'] = 'A'
d1['b'] = 'B'
d1['c'] = 'C'

d2 = {}
d2['c'] = 'C'
d2['b'] = 'B'
d2['a'] = 'A'

print(d1 == d2)

print('OrderedDict:', end=' ')

d1 = collections.OrderedDict()
d1['a'] = 'A'
d1['b'] = 'B'
d1['c'] = 'C'

d2 = collections.OrderedDict()
d2['c'] = 'C'
d2['b'] = 'B'
d2['a'] = 'A'

print(d1 == d2)

Dans ce cas, puisque deux dictionnaires ordonnés sont créés depuis des valeurs dans un ordre différent, ils sont considérés différents.

$ python3 collections_ordereddict_equality.py

dict       : True
OrderedDict: False

Réordonner

Il est possible de changer l’ordre des clés dans un OrderedDict en les bougeant selon le début ou la fin de la séquence en utilisant move_to_end().

# collections_ordereddict_move_to_end.py

import collections

d = collections.OrderedDict(
    [('a', 'A'), ('b', 'B'), ('c', 'C')]
)

print('Before:')
for k, v in d.items():
    print(k, v)

d.move_to_end('b')

print('\nmove_to_end():')
for k, v in d.items():
    print(k, v)

d.move_to_end('b', last=False)

print('\nmove_to_end(last=False):')
for k, v in d.items():
    print(k, v)

Le dernier argument demande à move_to_end() de bouger l’élément afin de devenir le dernier dans la séquence de clé (quand True) ou le premier (quand False).

$ python3 collections_ordereddict_move_to_end.py

Before:
a A
b B
c C

move_to_end():
a A
c C
b B

move_to_end(last=False):
b B
a A
c C

Lire aussi

collections.abc - Classes de base abstraites pour les conteneurs

But : Classes de bases abstraites pour types de données de conteneur.

Le module collections.abc contient des classes de base abstraites qui définissent les API pour les structures de conteneur de données natives dans Python et fournies dans le module collections. Référez-vous à la table ci-dessous pour la liste des classes et leur but.

+——————–+——————————+——————————————————————————————————————-+ | Classe | Classe(s) de Base | But de l’API | +——————–+——————————+——————————————————————————————————————-+ | Container | | Fonctionnalités de base de conteneur, tel que l’opérateur inc. | | Hashable | | Ajoute le support de fourniture d’une valeur de hachage pour l’instance du conteneur. | | Iterable | | Peut créer un itérateur sur le contenu du conteneur. | | Iterator | Iterable | Est un itérateur sur le contenu du conteneur. | | Generator | Iterator | Étend les itérateurs avec le protocole générateur du PEP 342. | | Sized | | Ajoute les méthodes de conteneurs pour connaître la grosseur de leur taille. | | Callable | | Pour les conteneurs invoqués en tant que fonction. | | Sequence | Sized, Iterable, Container | Prend en charge la récupération d’éléments individuels, l’itération et la modification de l’ordre des éléments. | | MutableSequence | Sequence | Prend en charge l’ajout et la suppression des éléments d’une instance après qu’ils soient créés. | | ByteString | Sequence | API combinées de bytes et de bytearray. | | Set | Sized, Iterable, Container | Prend en charges les opérations d’ensemble telles que l’intersection et l’union. | | MutableSet | Set | Ajoute les méthodes de manipulation pour le contenu d’ensemble après qu’il soit créé. | | Mapping | Sized, Iterable, Container | Définit l’API en lecture seule utilisé par dict. | | MutableMapping | Mapping | Définit les méthodes de manipulation des contenus pour la correspondance après qu’ils soient créés. | | MappingView | Sized | Définit la vue de l’API pour l’accès à la correspondance depuis un itérateur. | | ItemsView | MappingView, Set | Partie de la vue de l’API. | | KeysView | MappingView, Set | Partie de la vue de l’API. | | ValuesView | MappingView | Partie de la vue de l’API. | | Awaitable | | API pour les objets qui peuvent être utilisés dans des expressions await, telles que des coroutines. | | Coroutine | Awaitable | API pour les classes qui implémentent le protocole coroutine. | | AsyncIterable | | API pour les itérables compatibles avec async for, telle que définit dans la PEP 492. | | AsyncIterator | AsyncIterable | API pour les itérateurs asynchrones. | +——————–+——————————+——————————————————————————————————————-+

En plus de définir clairement les API pour les conteneurs avec des sémantiques différentes, ces classes de base abstraites peuvent être utilisées pour tester n’importe quel objet qui prend en charge une API avant de l’invoquer par l’usage d’ isinstance(). Certaines de ces classes fournissent aussi des implémentations de méthodes, et elles peuvent être utilisées comme mixages(?) pour créer des types de conteneurs personnalisés sans implémenter chaque méthode à partir de rien.

array - Séquence de Données de Type Fixé

But : Gérer efficacement des séquences de données numériques de type fixé.

Le module array définit une structure de données de séquence qui ressemble beaucoup à une list, excepté que tous les membres doivent être du même type primitif. Les types supportés sont tous les types numériques ou les autres primitifs de taille fixée, tels que les bytes.

Référez-vous à la table ci-dessous pour connaître certains des types supportés. La documentation de la bibliothèque standard pour array inclus une liste complète des codes de types.

+———-+———————-+—————————–+ | Code | Type | Taille minimal (octets) | +———-+———————-+—————————–+ | b | int | 1 | | B | int | 1 | | h | signed short | 2 | | H | unsigned short | 2 | | i | signed int | 2 | | I | unsigned int | 2 | | l | signed long | 4 | | L | unsigned long | 4 | | q | signed long long | 8 | | Q | unsigned long long | 8 | | f | float | 4 | | d | double float | 8 | +———-+———————-+—————————–+

Initialisation

Un array est instancié avec un argument décrivant le type de donnée à allouer, et si possible une séquence initiale de données à enregistrer dans le tableau.

# array_string.py

import array
import binascii

s = b'This is the array.'
a = array.array('b', s)

print('As byte string:', s)
print('As array      :', a)
print('As hex        :', binascii.hexlify(a))

Dans cet exemple, le tableau est configuré pour capturer une séquence d’octets et est initialisé avec une simple chaîne d’octets.

$ python3 array_string.py

As byte string: b'This is the array.'
As array      : array('b', [84, 104, 105, 115, 32, 105, 115, 32,
 116, 104, 101, 32, 97, 114, 114, 97, 121, 46])
As hex        : b'54686973206973207468652061727261792e'

Manipuler des Tableaux

Un array peut être étendu ou autrement manipulé de la même manière que les autres séquences Python.

# array_sequence.py

import array
import pprint

a = array.array('i', range(3))
print('Initial :', a)

a.extend(range(3))
print('Extended:', a)

print('Slice   :', a[2:5])

print('Iterator:')
print(list(enumerate(a)))

Les opérations supportées incluent le découpage, l’itération, et l’ajout d’éléments à la fin.

$ python3 array_sequence.py

Initial : array('i', [0, 1, 2])
Extended: array('i', [0, 1, 2, 0, 1, 2])
Slice   : array('i', [2, 0, 1])
Iterator:
[(0, 0), (1, 1), (2, 2), (3, 0), (4, 1), (5, 2)]

Tableaux et Fichiers

Le contenu d’un tableau peut être écrit et lit depuis et vers des fichiers en utilisant des méthodes natives codées efficacement dans ce but.

# array_file.py

import array
import binascii
import tempfile

a = array.array('i', range(5))
print('A1:', a)

# Write the array of numbers to a temporary file
output = tempfile.NamedTemporaryFile()
a.tofile(output.file)  # must pass an *actual* file
output.flush()

# Read the raw data
with open(output.name, 'rb') as input:
    raw_data = input.read()
    print('Raw Contents:', binascii.hexlify(raw_data))

    # Read the data into an array
    input.seek(0)
    a2 = array.array('i')
    a2.fromfile(input, len(a))
    print('A2:', a2)

Cet exemple illustre la lecture de donnée «brute», venant directement depuis le fichier binaire, versus la lecture dans un autre tableau et convertissant les octets dans les types appropriés.

$ python3 array_file.py

A1: array('i', [0, 1, 2, 3, 4])
Raw Contents: b'0000000001000000020000000300000004000000'
A2: array('i', [0, 1, 2, 3, 4])

tofile() utilise tobytes() pour formater les données, et fromfile() utilise frombytes() pour les convertir à nouveau en une instance de tableau.

# array_tobytes.py

import array
import binascii

a = array.array('i', range(5))
print('A1:', a)

as_bytes = a.tobytes()
print('Bytes:', binascii.hexlify(as_bytes))

a2 = array.array('i')
a2.frombytes(as_bytes)
print('A2:', a2)

Les deux fonctions tobytes() et frombytes() fonctionnent sur des chaînes d’octets, et non pas des chaînes Unicode.

$ python3 array_tobytes.py

A1: array('i', [0, 1, 2, 3, 4])
Bytes: b'0000000001000000020000000300000004000000'
A2: array('i', [0, 1, 2, 3, 4])

Ordre Alternatif d’Octet

Si la donnée dans le tableau n’est pas de l’ordre natif d’octet, ou si la donnée nécessite d’être échangée avant d’être envoyée au système avec un ordre d’octet différent (ou sur le réseau), il est possible de convertir un tableau entier sans itérer les éléments dans Python.

# array_byteswap.py

import array
import binascii


def to_hex(a):
    chars_per_item = a.itemsize * 2  # 2 hex digits
    hex_version = binascii.hexlify(a)
    num_chunks = len(hex_version) // chars_per_item
    for i in range(num_chunks):
        start = i * chars_per_item
        end = start + chars_per_item
        yield hex_version[start:end]


start = int('0x12345678', 16)
end = start + 5
a1 = array.array('i', range(start, end))
a2 = array.array('i', range(start, end))
a2.byteswap()

fmt = '{:>12} {:>12} {:>12} {:>12}'
print(fmt.format('A1 hex', 'A1', 'A2 hex', 'A2'))
print(fmt.format('-' * 12, '-' * 12, '-' * 12, '-' * 12))
fmt = '{!r:>12} {:12} {!r:>12} {:12}'
for values in zip(to_hex(a1), a1, to_hex(a2), a2):
    print(fmt.format(*values))

La méthode byteswap() échange l’ordre d’octet des éléments dans le tableau depuis le langage C, qui est plus efficace pour boucler sur les données en Python.

$ python3 array_byteswap.py

      A1 hex           A1       A2 hex           A2
------------ ------------ ------------ ------------
 b'78563412'    305419896  b'12345678'   2018915346
 b'79563412'    305419897  b'12345679'   2035692562
 b'7a563412'    305419898  b'1234567a'   2052469778
 b'7b563412'    305419899  b'1234567b'   2069246994
 b'7c563412'    305419900  b'1234567c'   2086024210

Lire aussi

heapq - Algorithme de Tri Heap

But : Heapq implémente un algorithme de tri minitas(?) approprié pour utiliser les listes Python.

Un heap est tel une arborescence de structure de données dans laquelle les nœuds enfants ont une relation d’ordre trié avec les nœuds parents. Le binary heaps peut être représenté en utilisant une liste ou un tableau organisé afin que les nœuds enfants d’élément N soient à la position 2 * N + 1 et 2 * N + 2 (pour les index basés sur zéro). Ce calque rend cela possible en réarrangeant le tas en place, ainsi il est n’est pas nécessaire de ré-allouer beaucoup de mémoire lors de l’ajout ou de la suppression d’items.

Un max-heap s’assure que le nœud parent est plus large que ou égal à deux de ces enfants. Un min-heap requiert que le nœud parent soit plus petit ou égal à ces nœuds enfants. Le module Python heapq implémente un min-heap.

Exemple de Donnée

Cet exemple dans ce chapitre utilise la donnée dans le script heapq_heapdata.py.

# heapq_heapdata.py

# This data was generated with the random module.

data = [19, 9, 4, 10, 11]

La sortie du tas est imprimé en utilisant le script heapq_showtree.py.

# heapq_showtree.py

import math
from io import StringIO


def show_tree(tree, total_width=36, fill=' '):
    """Pretty-print a tree."""
    output = StringIO()
    last_row = -1
    for i, n in enumerate(tree):
        if i:
            row = int(math.floor(math.log(i + 1, 2)))
        else:
            row = 0
        if row != last_row:
            output.write('\n')
        columns = 2 ** row
        col_width = int(math.floor(total_width / columns))
        output.write(str(n).center(col_width, fill))
        last_row = row
    print(output.getvalue())
    print('-' * total_width)
    print()

Créer un Heap

Il y a deux manières de base de créer un heap : heappush() et heapify().

# heapq_heappush.py

import heapq
from heapq_showtree import show_tree
from heapq_heapdata import data

heap = []
print('random :', data)
print()

for n in data:
    print('add {:>3}:'.format(n))
    heapq.heappush(heap, n)
    show_tree(heap)

Quand heappush() est utilisé, l’ordre de tri du tas des éléments est maintenu lorsque de nouveaux éléments sont ajoutés à partir d’une source de données.

$ python3 heapq_heappush.py

random : [19, 9, 4, 10, 11]

add  19:

                 19
------------------------------------

add   9:

                 9
        19
------------------------------------

add   4:

                 4
        19                9
------------------------------------

add  10:

                 4
        10                9
    19
------------------------------------

add  11:

                 4
        10                9
    19       11
------------------------------------

Si la donnée est toujours en mémoire, il est plus efficient d’utiliser heapify() pour réarranger les items dans la liste.

# heapq_heapify.py

import heapq
from heapq_showtree import show_tree
from heapq_heapdata import data

print('random    :', data)
heapq.heapify(data)
print('heapified :')
show_tree(data)

Le résultat d’une construction d’une liste dans un ordre heap, un item à la fois, est le même que le fait de construire une liste non ordonnée en appelant heapify().

$ python3 heapq_heapify.py

random    : [19, 9, 4, 10, 11]
heapified :

                 4
        9                 19
    10       11
------------------------------------

Accéder au Contenu d’un Heap

Une fois que le heap est organisé correctement, utilisez heappop() pour supprimer l’élément ayant la valeur la plus petite.

# heapq_heappop.py

import heapq
from heapq_showtree import show_tree
from heapq_heapdata import data

print('random    :', data)
heapq.heapify(data)
print('heapified :')
show_tree(data)
print()

for i in range(2):
    smallest = heapq.heappop(data)
    print('pop    {:>3}:'.format(smallest))
    show_tree(data)

Dans cet exemple, adapté de la documentation de la bibliothèque standard, heapify() et heappop() sont utilisées pour trier une liste de nombres.

$ python3 heapq_heappop.py

random    : [19, 9, 4, 10, 11]
heapified :

                 4
        9                 19
    10       11
------------------------------------


pop      4:

                 9
        10                19
    11
------------------------------------

pop      9:

                 10
        11                19
------------------------------------

Pour supprimer des éléments existants et les remplacer par de nouvelles valeurs en une unique opération, utilisez heapreplace().

# heapq_heapreplace.py

import heapq
from heapq_showtree import show_tree
from heapq_heapdata import data

heapq.heapify(data)
print('start:')
show_tree(data)

for n in [0, 13]:
    smallest = heapq.heapreplace(data, n)
    print('replace {:>2} with {:>2}:'.format(smallest, n))
    show_tree(data)

Remplacer des éléments rend possible de maintenir un heap de taille fixée, tel qu’une file de travaux ordonnés par priorité.

$ python3 heapq_heapreplace.py

start:

                 4
        9                 19
    10       11
------------------------------------

replace  4 with  0:

                 0
        9                 19
    10       11
------------------------------------

replace  0 with 13:

                 9
        10                19
    13       11
------------------------------------

Extrêmes des Données d’un Heap

heapq inclus aussi deux fonctions qui examine un itérable et trouve une rangée des valeurs les plus grandes ou les plus petites qu’il contient.

# heapq_extremes.py

import heapq
from heapq_heapdata import data

print('all       :', data)
print('3 largest :', heapq.nlargest(3, data))
print('from sort :', list(reversed(sorted(data)[-3:])))
print('3 smallest:', heapq.nsmallest(3, data))
print('from sort :', sorted(data)[:3])

Utiliser nlargest() et nsmallest() est efficace seulement pour les valeurs relativement petites de n > 1, mais peut toujours devenir plus pratique dans quelques cas.

$ python3 heapq_extremes.py

all       : [19, 9, 4, 10, 11]
3 largest : [19, 11, 10]
from sort : [19, 11, 10]
3 smallest: [4, 9, 10]
from sort : [4, 9, 10]

Fusion Efficace des Séquences Triées

Combiner de nombreuses séquences triées en une nouvelle séquence est facile pour de petits ensembles de données.

python list(sorted(itertools.chain(*data)))

Pour de plus grand ensembles de données, cette technique peut utiliser une somme considérable de mémoire. Au lieu de trier la séquence triée en entier, merge() utilise un tas pour générer un nouvelle séquence une à la fois, déterminant le nouvel item en utilisant une somme de mémoire fixée.

# heapq_merge.py

import heapq
import random


random.seed(2016)

data = []
for i in range(4):
    new_data = list(random.sample(range(1, 101), 5))
    new_data.sort()
    data.append(new_data)

for i, d in enumerate(data):
    print('{}: {}'.format(i, d))

print('\nMerged:')
for i in heapq.merge(*data):
    print(i, end=' ')
print()

Puisque l’implémentation de merge() utilise un tas, il consomme la mémoire basée sur le nombre de séquences à fusionner, plutôt que le nombre des éléments dans ces séquences.

$ python3 heapq_merge.py

0: [33, 58, 71, 88, 95]
1: [10, 11, 17, 38, 91]
2: [13, 18, 39, 61, 63]
3: [20, 27, 31, 42, 45]

Merged:
10 11 13 17 18 20 27 31 33 38 39 42 45 58 61 63 71 88 91 95

Lire aussi

bisect - Maintenir les Listes dans un Ordre Trié

But : Maintenir une liste dans un ordre trié sans avoir à appeler le tri chaque fois qu’un item est ajouté à la liste.

Le module bisect implémente un algorithme pour insérer des éléments dans une liste tout en maintenant la liste dans un ordre trié.

Insérer dans un Ordre Trié

Voici un exemple dans lequel insort() est utilisé pour insérer des items dans la liste en ordre trié.

# bisect_example.py

import bisect

# A series of random numbers
values = [14, 85, 77, 26, 50, 45, 66, 79, 10, 3, 84, 77, 1]

print('New  Pos  Contents')
print('---  ---  --------')

l = []
for i in values:
    position = bisect.bisect(l, i)
    bisect.insort(l, i)
    print('{:3}  {:3}'.format(i, position), l)

La première colonne de sortie montre un nouveau nombre aléatoire. La seconde colonne montre la position où le nombre sera inséré dans la liste. Le reste de chaque ligne est la liste triée actuelle.

$ python3 bisect_example.py

New  Pos  Contents
---  ---  --------
 14    0 [14]
 85    1 [14, 85]
 77    1 [14, 77, 85]
 26    1 [14, 26, 77, 85]
 50    2 [14, 26, 50, 77, 85]
 45    2 [14, 26, 45, 50, 77, 85]
 66    4 [14, 26, 45, 50, 66, 77, 85]
 79    6 [14, 26, 45, 50, 66, 77, 79, 85]
 10    0 [10, 14, 26, 45, 50, 66, 77, 79, 85]
  3    0 [3, 10, 14, 26, 45, 50, 66, 77, 79, 85]
 84    9 [3, 10, 14, 26, 45, 50, 66, 77, 79, 84, 85]
 77    8 [3, 10, 14, 26, 45, 50, 66, 77, 77, 79, 84, 85]
  1    0 [1, 3, 10, 14, 26, 45, 50, 66, 77, 77, 79, 84, 85]

Ceci est un simple exemple. En fait, compte tenu de la quantité de données manipulées, il est plus rapide de simplifier la construction de la liste et la trier en une fois. Par contraste, pour des listes longues, des économies de temps et de mémoire considérables peuvent être obtenues à l’aide d’un algorithme de tri par insertion tel que celui-ci, spécialement quand l’opération comparant deux membres d’une liste requiert de coûteux calculs.

Traitement des Doublons

Le résultat de l’ensemble montré précédemment inclus une valeur répétée, 77. Le module bisect fournit deux manière de traiter les répétitions : les nouvelles valeurs peuvent être insérées soit à la gauche des valeurs existantes, soit à la droite. La fonction insort() est actuellement un alias de insort_right(), qui insère un item après une valeur existante. La fonction correspondante insort_left() insère un item avant la valeur existante.

# bisect_example2.py

import bisect

# A series of random numbers
values = [14, 85, 77, 26, 50, 45, 66, 79, 10, 3, 84, 77, 1]

print('New  Pos  Contents')
print('---  ---  --------')

# Use bisect_left and insort_left.
l = []
for i in values:
    position = bisect.bisect_left(l, i)
    bisect.insort_left(l, i)
    print('{:3}  {:3}'.format(i, position), l)

Quand la même donnée est manipulée par l’utilisation des fonctions bisect_left() et insort_left(), il en résulte la même liste triée mais l’insertion des positions est différente pour les valeurs dupliquées.

$ python3 bisect_example2.py

New  Pos  Contents
---  ---  --------
 14    0 [14]
 85    1 [14, 85]
 77    1 [14, 77, 85]
 26    1 [14, 26, 77, 85]
 50    2 [14, 26, 50, 77, 85]
 45    2 [14, 26, 45, 50, 77, 85]
 66    4 [14, 26, 45, 50, 66, 77, 85]
 79    6 [14, 26, 45, 50, 66, 77, 79, 85]
 10    0 [10, 14, 26, 45, 50, 66, 77, 79, 85]
  3    0 [3, 10, 14, 26, 45, 50, 66, 77, 79, 85]
 84    9 [3, 10, 14, 26, 45, 50, 66, 77, 79, 84, 85]
 77    7 [3, 10, 14, 26, 45, 50, 66, 77, 77, 79, 84, 85]
  1    0 [1, 3, 10, 14, 26, 45, 50, 66, 77, 77, 79, 84, 85]

Lire aussi

queue - Implémentation FIFO Thread-Safe

But : Fournir une implémentation FIFO de processus sûrs.

Le module queue fournit une structure de données premier entré, premier sorti (FIFO) adaptée à la programmation multithread. Elle peut être utilisée pour passer des messages ou d’autres choses en toute sécurité entre les threads producteur et consommateur. Le verrouillage est géré pour l’appelant. Ainsi, de nombreux threads peuvent fonctionner avec la même instance de Queue facilement et en toute sécurité. La taille de la Queue (le nombre des éléments qu’elle contient) peut être restreinte à l’utilisation ou au traitement de la mémoire.

Note Cette discussion présume que vous comprenez la nature générale d’une file d’attente. Si ce n’est pas le cas, vous devriez lire certaines références avant de continuer.

File d’attente FIFO de base

La classe Queue implémente un conteneur premier entré, premier sorti de base. Les éléments sont ajoutés à la «fin» de la séquence en utilisant la fonction put(), et supprimé à la fin d’une autre par l’utilisation de get().

# queue_fifo.py
import queue

q = queue.Queue()

for i in range(5):
    q.put(i)

while not q.empty():
    print(q.get(), end=' ')
print()

Cet exemple utilise un unique thread pour illustrer comment les éléments sont supprimés de la file d’attente, dans le même ordre dans lequel ils ont été insérés.

$ python3 queue_fifo.py

0 1 2 3 4

File d’Attente LIFO

Par contraste à l’implémentation standard FIFO de Queue, la LifoQueue utilise l’ordre dernier entré, premier sorti (normalement associé à la pile de la structure de données).

# queue_lifo.py
import queue

q = queue.LifoQueue()

for i in range(5):
    q.put(i)

while not q.empty():
    print(q.get(), end=' ')
print()

L’élément le plus récemment entré dans la file d’attente est supprimé par get.

$ python3 queue_lifo.py

4 3 2 1 0

Priorité dans la File d’Attente

Parfois, l’ordre de traitement des éléments d’une file d’attente doit être basé sur les caractéristiques de ces éléments, plutôt que sur l’ordre dans lequel ils ont été créés ou ajoutés à la file d’attente. Par exemple, les travaux d’impression du service de la comptabilité peuvent avoir priorité sur une liste de codes qu’un développeur souhaite imprimer. PriorityQueues utilise l’ordre de tri de la file d’attente pour décider quel élément est à récupérer.

# queue_priority.py
import functools
import queue
import threading


@functools.total_ordering
class Job:

    def __init__(self, priority, description):
        self.priority = priority
        self.description = description
        print('New job:', description)
        return

    def __eq__(self, other):
        try:
            return self.priority == other.priority
        except AttributeError:
            return NotImplemented

    def __lt__(self, other):
        try:
            return self.priority < other.priority
        except AttributeError:
            return NotImplemented


q = queue.PriorityQueue()

q.put(Job(3, 'Mid-level job'))
q.put(Job(10, 'Low-level job'))
q.put(Job(1, 'Important job'))


def process_job(q):
    while True:
        next_job = q.get()
        print('Processing job:', next_job.description)
        q.task_done()


workers = [
    threading.Thread(target=process_job, args=(q,)),
    threading.Thread(target=process_job, args=(q,)),
]
for w in workers:
    w.setDaemon(True)
    w.start()

q.join()

Cet exemple comporte de multiples threads consommant les travaux, qui sont traités en fonction de la priorité des éléments dans la file d’attente au moment où get() est appelé. L’ordre de traitement des éléments ajoutés à la file d’attente, pendant l’exécution des threads consommateurs, dépend du basculement du contexte du thread.

$ python3 queue_priority.py

New job: Mid-level job
New job: Low-level job
New job: Important job
Processing job: Important job
Processing job: Mid-level job
Processing job: Low-level job

Construire un Thread Client de Podcast

Le code source pour un client de podcast dans ce chapitre démontre comment utiliser la classe Queue avec de multiples threads. Le programme lit un ou plusieurs flux RSS, met en file d’attente les pièces jointes des cinq derniers épisodes de chaque flux à télécharger et traite plusieurs téléchargements en parallèle à l’aide de threads. La gestion des erreurs n’est pas suffisante pour une utilisation en production, mais l’implémentation du squelette illustre l’utilisation du module queue.

En premier, certains paramètres de fonctionnement sont établis. Habituellement, ce sont des entrées d’utilisateur (p.ex. : des préférences ou d’une base de données). L’exemple utilise des valeurs codées en dur pour le nombre des threads et la liste des URL à récupérer.

# fetch_podcasts.py
from queue import Queue
import threading
import time
import urllib
from urllib.parse import urlparse

import feedparser

# Set up some global variables
num_fetch_threads = 2
enclosure_queue = Queue()

# A real app wouldn't use hard-coded data...
feed_urls = [
    'http://talkpython.fm/episodes/rss',
]


def message(s):
    print('{}: {}'.format(threading.current_thread().name, s))

La fonction download_enclosures() s’exécute dans le thread de travail et traite les téléchargements en utilisant urllib.

def download_enclosures(q):
    """This is the worker thread function.
    It processes items in the queue one after
    another.  These daemon threads go into an
    infinite loop, and exit only when
    the main thread ends.
    """
    while True:
        message('looking for the next enclosure')
        url = q.get()
        filename = url.rpartition('/')[-1]
        message('downloading {}'.format(filename))
        response = urllib.request.urlopen(url)
        data = response.read()
        # Save the downloaded file to the current directory
        message('writing to {}'.format(filename))
        with open(filename, 'wb') as outfile:
            outfile.write(data)
        q.task_done()

Une fois que la fonction cible pour les threads est définie, le thread de travail peut être démarré. Quand download_enclosures() s’occupe de l’instruction url = q.get(), elle la bloque et attend jusqu’à ce que la file d’attente ait quelque chose à retourner. Cela signifie qu’il est prudent de démarrer les threads avant qu’il n’y ait quoi que ce soit dans la file d’attente.

# Set up some threads to fetch the enclosures
for i in range(num_fetch_threads):
    worker = threading.Thread(
        target=download_enclosures,
        args=(enclosure_queue,),
        name='worker-{}'.format(i),
    )
    worker.setDaemon(True)
    worker.start()

L’étape suivante est de récupérer le contenu de flux en utilisant le module feedparser et de mettre en file d’attente les URL. Dès que possible, la première URL est ajoutée à la file d’attente, un des threads de travail la prend et démarre le téléchargement. La boucle continue d’ajouter les éléments jusqu’à ce que le flux soit complet, et les threads de travail retirent à tour de rôle de la file d’attente les URL pour les télécharger.

# Download the feed(s) and put the enclosure URLs into
# the queue.
for url in feed_urls:
    response = feedparser.parse(url, agent='fetch_podcasts.py')
    for entry in response['entries'][:5]:
        for enclosure in entry.get('enclosures', []):
            parsed_url = urlparse(enclosure['url'])
            message('queuing {}'.format(
                parsed_url.path.rpartition('/')[-1]))
            enclosure_queue.put(enclosure['url'])

Il ne reste plus qu’à attendre que la file se vide à nouveau, par l’utilisation de join().

# Now wait for the queue to be empty, indicating that we have
# processed all of the downloads.
message('*** main thread waiting')
enclosure_queue.join()
message('*** done')

L’exécution de cet exemple de script produit une sortie similaire à celle qui suit.

python3 fetch_podcasts.py

worker-0: looking for the next enclosure
worker-1: looking for the next enclosure
MainThread: queuing turbogears-and-the-future-of-python-web-frameworks.mp3
MainThread: queuing continuum-scientific-python-and-the-business-of-open-source.mp3
MainThread: queuing openstack-cloud-computing-built-on-python.mp3
MainThread: queuing pypy.js-pypy-python-in-your-browser.mp3
MainThread: queuing machine-learning-with-python-and-scikit-learn.mp3
MainThread: *** main thread waiting
worker-0: downloading turbogears-and-the-future-of-python-web-frameworks.mp3
worker-1: downloading continuum-scientific-python-and-the-business-of-open-source.mp3
worker-0: looking for the next enclosure
worker-0: downloading openstack-cloud-computing-built-on-python.mp3
worker-1: looking for the next enclosure
worker-1: downloading pypy.js-pypy-python-in-your-browser.mp3
worker-0: looking for the next enclosure
worker-0: downloading machine-learning-with-python-and-scikit-learn.mp3
worker-1: looking for the next enclosure
worker-0: looking for the next enclosure
MainThread: *** done

La sortie actuelle dépendra du contenu des flux RSS utilisés.

Lire aussi

struct - Structures de Données Binaires

But : Conversion entre chaînes et données binaires.

Le module struct inclut des fonctions pour la conversion entre les octets de chaînes et les types de données Python natifs tels que les nombres et les chaînes.

Fonctions versus Classe Struct

Un ensemble de fonctions au niveau du module est disponible pour travailler avec des valeurs structurées, tout comme la classe Struct. Les spécificateurs de format sont convertis depuis leur format de chaînes vers une représentation compilée, similaire à la façon dont sont pratiques les expressions régulières. La conversion nécessite certaines ressources, ainsi il est plus efficient de la faire en une fois lors de la création de l’instance Struct et de l’appel des méthodes sur l’instance plutôt que d’utiliser les fonctions au niveau du module. Tous les exemples suivants utilisent la classe Struct.

Emballage et Déballage

Les structures prennent en charge l’emballage de données dans les chaînes, et le déballage de données depuis les chaînes par l’usage des spécificateurs de format composés de caractères représentant le type des données et d’indicateurs facultatifs de comptage et de finalité. Se référer à la documentation standard de la bibliothèque pour avoir la liste complète des spécificateurs de format supportés.

Dans cet exemple, le spécificateur appelle comme valeurs, un entier, un entier long, et un nombre à virgule flottante. Les espaces dans le spécificateur de format sont inclus pour séparer le type d’indicateurs, et sont ignorés lorsque le format est compilé.

# struct_pack.py
import struct
import binascii

values = (1, 'ab'.encode('utf-8'), 2.7)
s = struct.Struct('I 2s f')
packed_data = s.pack(*values)

print('Original values:', values)
print('Format string  :', s.format)
print('Uses           :', s.size, 'bytes')
print('Packed Value   :', binascii.hexlify(packed_data))

L’exemple convertit la valeur encodée en une séquence d’octets hexadécimaux pour l’imprimer avec la fonction binascii.hexlify(), puisque certains des caractères sont nuls.

$ python3 struct_pack.py

Original values: (1, b'ab', 2.7)
Format string  : I 2s f
Uses           : 12 bytes
Packed Value   : b'0100000061620000cdcc2c40'

Utilisez unpack() pour extraire la donnée de sa représentation encodée.

# struct_unpack.py
import struct
import binascii

packed_data = binascii.unhexlify(b'0100000061620000cdcc2c40')

s = struct.Struct('I 2s f')
unpacked_data = s.unpack(packed_data)
print('Unpacked Values:', unpacked_data)

Passer la valeur condensée à unpack() renvoie les mêmes valeurs (notez l’écart dans la valeur à virgule flottante).

$ python3 struct_unpack.py

Unpacked Values: (1, b'ab', 2.700000047683716)

Endianness

Par défaut, les valeurs sont codées à l’aide de la notion d’endianness de la bibliothèque C native. Il est facile de surcharger ce choix en fournissant une directive endianness explicite dans le format de chaînes.

# struct_endianness.py
import struct
import binascii

values = (1, 'ab'.encode('utf-8'), 2.7)
print('Original values:', values)

endianness = [
    ('@', 'native, native'),
    ('=', 'native, standard'),
    ('<', 'little-endian'),
    ('>', 'big-endian'),
    ('!', 'network'),
]

for code, name in endianness:
    s = struct.Struct(code + ' I 2s f')
    packed_data = s.pack(*values)
    print()
    print('Format string  :', s.format, 'for', name)
    print('Uses           :', s.size, 'bytes')
    print('Packed Value   :', binascii.hexlify(packed_data))
    print('Unpacked Value :', s.unpack(packed_data))

La table ci-dessous liste les spécificateurs d’ordre d’octets utilisés par Struct.

+——+—————–+ | Code | Signification | +——+—————–+ | @ | Ordre natif | | = | Standard natif | | < | little-endian | | > | big-endian | | ! | Ordre réseau | +——+—————–+

$ python3 struct_endianness.py

Original values: (1, b'ab', 2.7)

Format string  : @ I 2s f for native, native
Uses           : 12 bytes
Packed Value   : b'0100000061620000cdcc2c40'
Unpacked Value : (1, b'ab', 2.700000047683716)

Format string  : = I 2s f for native, standard
Uses           : 10 bytes
Packed Value   : b'010000006162cdcc2c40'
Unpacked Value : (1, b'ab', 2.700000047683716)

Format string  : < I 2s f for little-endian
Uses           : 10 bytes
Packed Value   : b'010000006162cdcc2c40'
Unpacked Value : (1, b'ab', 2.700000047683716)

Format string  : > I 2s f for big-endian
Uses           : 10 bytes
Packed Value   : b'000000016162402ccccd'
Unpacked Value : (1, b'ab', 2.700000047683716)

Format string  : ! I 2s f for network
Uses           : 10 bytes
Packed Value   : b'000000016162402ccccd'
Unpacked Value : (1, b'ab', 2.700000047683716)

Tampons

Travailler avec une donnée encodée en binaire est typiquement réservé pour les situations nécessitant de la performance ou de passer la donnée vers ou depuis des modules d’extensions. Ces cas peuvent être optimisés en évitant la surcharge liée à l’allocation d’un nouveau tampon pour chaque structure codée. Les méthodes pack_into() et unpack_from() prennent en charge l’écriture directe de tampons préalloués.

# struct_buffers.py
import array
import binascii
import ctypes
import struct

s = struct.Struct('I 2s f')
values = (1, 'ab'.encode('utf-8'), 2.7)
print('Original:', values)

print()
print('ctypes string buffer')

b = ctypes.create_string_buffer(s.size)
print('Before  :', binascii.hexlify(b.raw))
s.pack_into(b, 0, *values)
print('After   :', binascii.hexlify(b.raw))
print('Unpacked:', s.unpack_from(b, 0))

print()
print('array')

a = array.array('b', b'\0' * s.size)
print('Before  :', binascii.hexlify(a))
s.pack_into(a, 0, *values)
print('After   :', binascii.hexlify(a))
print('Unpacked:', s.unpack_from(a, 0))

L’attribut size à Struct nous dit la taille dont a besoin le tampon.

$ python3 struct_buffers.py

Original: (1, b'ab', 2.7)

ctypes string buffer
Before  : b'000000000000000000000000'
After   : b'0100000061620000cdcc2c40'
Unpacked: (1, b'ab', 2.700000047683716)

array
Before  : b'000000000000000000000000'
After   : b'0100000061620000cdcc2c40'
Unpacked: (1, b'ab', 2.700000047683716)

Lire aussi

weakref - Références Impermanentes aux Objets

But: Référer un objet «coûteux», tout en permettant à sa mémoire d’être réclamée par le ramasse miettes s’il n’y a pas d’autres références non faibles.

Le module weakref prend en charge les références faibles aux objets. Une référence normale incrémente le compteur de référence sur l’objet et l’empêche d’être nettoyé. Ce résultat n’est pas toujours souhaitable, spécifiquement quand une référence circulaire doit être présente ou quand un cache d’objet doit être supprimé quand de la mémoire est nécessaire. Une référence faible est une poignée pour un objet qui ne l’empêche pas d’être nettoyé automatiquement.

Références

Les références faibles d’objets sont gérés au-travers de la classe ref. Pour récupérer l’objet original, appelez la référence objet.

# weakref_ref.py
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


obj = ExpensiveObject()
r = weakref.ref(obj)

print('obj:', obj)
print('ref:', r)
print('r():', r())

print('deleting obj')
del obj
print('r():', r())

Dans ce cas, puisque obj est supprimé avant le second appel à la référence, la ref retourne None.

$ python3 weakref_ref.py

obj: <__main__.ExpensiveObject object at 0x1007b1a58>
ref: <weakref at 0x1007a92c8; to 'ExpensiveObject' at
0x1007b1a58>
r(): <__main__.ExpensiveObject object at 0x1007b1a58>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x1007b1a58>)
r(): None

Rappels de Référence

Le constructeur ref accepte une fonction optionnelle de rappel qui est invoquée quand la référence de l’objet est supprimée.

# weakref_ref_callback.py
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def callback(reference):
    """Invoked when referenced object is deleted"""
    print('callback({!r})'.format(reference))


obj = ExpensiveObject()
r = weakref.ref(obj, callback)

print('obj:', obj)
print('ref:', r)
print('r():', r())

print('deleting obj')
del obj
print('r():', r())

Le rappel reçoit la référence de l’objet en tant qu’argument après que la référence soit «morte» et ne se réfère plus à l’objet d’origine. Une utilisation de cette fonctionnalité est de supprimer la référence faible d’un objet depuis un cache.

$ python3 weakref_ref_callback.py

obj: <__main__.ExpensiveObject object at 0x1010b1978>
ref: <weakref at 0x1010a92c8; to 'ExpensiveObject' at
0x1010b1978>
r(): <__main__.ExpensiveObject object at 0x1010b1978>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x1010b1978>)
callback(<weakref at 0x1010a92c8; dead>)
r(): None

Finalisation des Objets

Pour une gestion plus robuste des ressources lorsque des références faibles sont nettoyées, utilisez la méthode finalize pour associer les rappels aux objets. Une instance finalize est retenue jusqu’à ce que l’objet attaché soit supprimé, même si l’application ne retient pas de référence à la fin.

# weakref_finalize.py
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def on_finalize(*args):
    print('on_finalize({!r})'.format(args))


obj = ExpensiveObject()
weakref.finalize(obj, on_finalize, 'extra argument')

del obj

Les arguments de finalize sont les objets à suivre, un rappel à invoquer quand les objets sont nettoyés, et que les arguments de positions ou nommés sont passés au rappel.

$ python3 weakref_finalize.py

(Deleting <__main__.ExpensiveObject object at 0x1019b10f0>)
on_finalize(('extra argument',))

L’instance finalize a une propriété en écriture nommée atexit pour contrôler quand le rappel est invoqué lorsqu’un programme se termine, s’il n’a pas déjà été appelé.

# weakref_finalize_atexit.py
import sys
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def on_finalize(*args):
    print('on_finalize({!r})'.format(args))


obj = ExpensiveObject()
f = weakref.finalize(obj, on_finalize, 'extra argument')
f.atexit = bool(int(sys.argv[1]))

Le comportement par défaut est d’invoquer le rappel. Paramétrer atexit sur false désactivera ce comportement.

$ python3 weakref_finalize_atexit.py 1

on_finalize(('extra argument',))
(Deleting <__main__.ExpensiveObject object at 0x1007b10f0>)

$ python3 weakref_finalize_atexit.py 0

Donner à l’instance finalize une référence vers l’objet à suivre a pour conséquence de le retenir, ainsi l’objet n’est jamais passé au ramasse miettes.

# weakref_finalize_reference.py
import gc
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def on_finalize(*args):
    print('on_finalize({!r})'.format(args))


obj = ExpensiveObject()
obj_id = id(obj)

f = weakref.finalize(obj, on_finalize, obj)
f.atexit = False

del obj

for o in gc.get_objects():
    if id(o) == obj_id:
        print('found uncollected object in gc')

Comme cet exemple le montre, même si la référence explicite vers obj est supprimée, l’objet est retenu et visible par le ramasse miettes au-travers de f.

$ python3 weakref_finalize_reference.py

found uncollected object in gc

Utiliser une méthode liée à un objet suivi en tant rappel peut également empêcher la finalisation correcte de l’objet.

# weakref_finalize_reference_method.py
import gc
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))

    def do_finalize(self):
        print('do_finalize')


obj = ExpensiveObject()
obj_id = id(obj)

f = weakref.finalize(obj, obj.do_finalize)
f.atexit = False

del obj

for o in gc.get_objects():
    if id(o) == obj_id:
        print('found uncollected object in gc')

Puisque le rappel donné à finalize est une méthode liée à l’instance obj, l’objet de finalisation capture une référence vers obj, qui ne peut être supprimé ou collecté par le ramasse miettes.

$ python3 weakref_finalize_reference_method.py

found uncollected object in gc

Mandataires

Il est parfois plus pratique d’utiliser un mandataire, plutôt qu’une référence faible. Les mandataires peuvent être utilisés comme s’il s’agissait de l’objet d’origine et n’ont pas besoin d’être appelés avant que l’objet ne soit accessible. En conséquence, ils peuvent être passés à une bibliothèque qui ne sait pas qu’elle reçoit une référence au lieu d’un objet réel.

# weakref_proxy.py
import weakref


class ExpensiveObject:

    def __init__(self, name):
        self.name = name

    def __del__(self):
        print('(Deleting {})'.format(self))


obj = ExpensiveObject('My Object')
r = weakref.ref(obj)
p = weakref.proxy(obj)

print('via obj:', obj.name)
print('via ref:', r().name)
print('via proxy:', p.name)
del obj
print('via proxy:', p.name)

Si le mandataire est accédé après que l’objet en référence soit supprimé, une erreur d’exception ReferenceError est levée.

$ python3 weakref_proxy.py

via obj: My Object
via ref: My Object
via proxy: My Object
(Deleting <__main__.ExpensiveObject object at 0x1007aa7b8>)
Traceback (most recent call last):
  File "weakref_proxy.py", line 30, in <module>
    print('via proxy:', p.name)
ReferenceError: weakly-referenced object no longer exists

Mettre en Cache des Objets

Les classes ref et obj sont considérés de «bas niveau». Bien qu’elles soient utiles pour maintenir les références faibles d’objets individuels et de permettre des cycles pour être collectés par le ramasse miettes, les classes WeakKeyDictionary et WeakValueDictionary fournissent une API plus appropriée pour créer un cache de plusieurs objets.

La classe WeakValueDictionary utilise les références faibles vers les valeurs qu’elle capture, leur permettant d’être collectées par le ramasse miettes lorsque tout autre code ne les utilise pas. L’utilisation des appels explicites au ramasse miettes illustre la différence entre la gestion de la mémoire avec un dictionnaire standard et WeakValueDictionary :

# weakref_valuedict.py
import gc
from pprint import pprint
import weakref

gc.set_debug(gc.DEBUG_UNCOLLECTABLE)


class ExpensiveObject:

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return 'ExpensiveObject({})'.format(self.name)

    def __del__(self):
        print('    (Deleting {})'.format(self))


def demo(cache_factory):
    # hold objects so any weak references
    # are not removed immediately
    all_refs = {}
    # create the cache using the factory
    print('CACHE TYPE:', cache_factory)
    cache = cache_factory()
    for name in ['one', 'two', 'three']:
        o = ExpensiveObject(name)
        cache[name] = o
        all_refs[name] = o
        del o  # decref

    print('  all_refs =', end=' ')
    pprint(all_refs)
    print('\n  Before, cache contains:', list(cache.keys()))
    for name, value in cache.items():
        print('    {} = {}'.format(name, value))
        del value  # decref

    # remove all references to the objects except the cache
    print('\n  Cleanup:')
    del all_refs
    gc.collect()

    print('\n  After, cache contains:', list(cache.keys()))
    for name, value in cache.items():
        print('    {} = {}'.format(name, value))
    print('  demo returning')
    return


demo(dict)
print()

demo(weakref.WeakValueDictionary)

Toutes variables de boucle faisant référence aux valeurs mises en cache doivent être explicitement effacées pour que le compte de références de l’objet soit décrémenté. Autrement, le ramasse miettes ne supprimera pas les objets, et ils resteront toujours dans le cache. De la même manière, la variable all_refs est utilisée pour capturer les références afin de prévenir que le ramasse miettes les collecte prématurément.

$ python3 weakref_valuedict.py

CACHE TYPE: <class 'dict'>
  all_refs = {'one': ExpensiveObject(one),
 'three': ExpensiveObject(three),
 'two': ExpensiveObject(two)}

  Before, cache contains: ['one', 'three', 'two']
    one = ExpensiveObject(one)
    three = ExpensiveObject(three)
    two = ExpensiveObject(two)

  Cleanup:

  After, cache contains: ['one', 'three', 'two']
    one = ExpensiveObject(one)
    three = ExpensiveObject(three)
    two = ExpensiveObject(two)
  demo returning
    (Deleting ExpensiveObject(one))
    (Deleting ExpensiveObject(three))
    (Deleting ExpensiveObject(two))

CACHE TYPE: <class 'weakref.WeakValueDictionary'>
  all_refs = {'one': ExpensiveObject(one),
 'three': ExpensiveObject(three),
 'two': ExpensiveObject(two)}

  Before, cache contains: ['one', 'three', 'two']
    one = ExpensiveObject(one)
    three = ExpensiveObject(three)
    two = ExpensiveObject(two)

  Cleanup:
    (Deleting ExpensiveObject(one))
    (Deleting ExpensiveObject(three))
    (Deleting ExpensiveObject(two))

  After, cache contains: []
  demo returning

La classe WeakKeyDictionary fonctionne de manière similaire mais utilise les références faibles pour les clés au lieu des valeurs dans le dictionnaire.

Attention La documentation de la bibliothèque pour weakref contient cet avertissement : Mise en garde : Étant donné qu’un WeakValueDictionary est construit sur un dictionnaire Python, sa taille ne doit pas changer lors de son itération. Cela peut être difficile à garantir pour un WeakValueDictionary parce que les actions effectuées par le programme pendant l’itération peuvent entraîner la disparition «par magie» d’éléments du dictionnaire (c’est un effet de bord du ramasse miettes des collections).

Lire aussi

copy - Objets Dupliqués

But : Fournir des fonctions pour dupliquer les objets en utilisant une sémantique de copie superficielle ou profonde

Le module copy inclut deux fonctions, copy() et deepcopy(), pour dupliquer des objets existants.

Copies Superficielles

La copie superficielle créée par copy() est un nouveau conteneur rempli de références vers les contenus de l’objet original. Lors de la copie superficielle d’un objet list, une nouvelle list est construite et les éléments de l’objet original y sont ajoutés.

# copy_shallow.py
import copy
import functools


@functools.total_ordering
class MyClass:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

    def __gt__(self, other):
        return self.name > other.name


a = MyClass('a')
my_list = [a]
dup = copy.copy(my_list)

print('             my_list:', my_list)
print('                 dup:', dup)
print('      dup is my_list:', (dup is my_list))
print('      dup == my_list:', (dup == my_list))
print('dup[0] is my_list[0]:', (dup[0] is my_list[0]))
print('dup[0] == my_list[0]:', (dup[0] == my_list[0]))

Pour une copie superficielle, l’instance MyClass n’est pas dupliquée, ainsi la référence dans la liste dup est le même objet que celui qui est dans my_list.

$ python3 copy_shallow.py

             my_list: [<__main__.MyClass object at 0x101f9c160>]
                 dup: [<__main__.MyClass object at 0x101f9c160>]
      dup is my_list: False
      dup == my_list: True
dup[0] is my_list[0]: True
dup[0] == my_list[0]: True

Copies Profondes

La copie profonde créée par deepcopy() est un nouveau conteneur avec des copies des contenus de l’objet original. Pour faire une copie profonde d’une list, une nouvelle list est construite, les éléments de la liste originale sont copiés, et ensuite ces copies sont ajoutées à la nouvelle liste.

Remplacer l’appel de copy() par deepcopy() fait la différence dans la sortie apparente.

# copy_deep.py
import copy
import functools


@functools.total_ordering
class MyClass:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

    def __gt__(self, other):
        return self.name > other.name


a = MyClass('a')
my_list = [a]
dup = copy.deepcopy(my_list)

print('             my_list:', my_list)
print('                 dup:', dup)
print('      dup is my_list:', (dup is my_list))
print('      dup == my_list:', (dup == my_list))
print('dup[0] is my_list[0]:', (dup[0] is my_list[0]))
print('dup[0] == my_list[0]:', (dup[0] == my_list[0]))

Le premier élément de la liste n’a plus du tout la même référence objet, mais quand les deux objets sont comparés, ils sont toujours évalués comme égaux.

$ python3 copy_deep.py

             my_list: [<__main__.MyClass object at 0x101e9c160>]
                 dup: [<__main__.MyClass object at 0x1044e1f98>]
      dup is my_list: False
      dup == my_list: True
dup[0] is my_list[0]: False
dup[0] == my_list[0]: True

Personnaliser le Comportement des Copies

Il est possible de contrôler comment les copies sont faites en utilisant les méthodes spéciales __copy__() et __deepcopy__().

  • __copy__() est appelée sans aucun argument et devrait retourner une copie superficielle de l’objet.
  • __deepcopy__() est appelée avec un dictionnaire memo et devrait retourner une copie profonde de l’objet. Tous les attributs membres qui ont besoin d’être copiés en profondeur devraient être passé vers copy.deepcopy(), avec le dictionnaire memo, pour contrôler la récursion. (Le dictionnaire memo est expliqué plus tard en détails).

L’exemple suivant illustre comment les méthodes sont appelées.

# copy_hooks.py
import copy
import functools


@functools.total_ordering
class MyClass:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

    def __gt__(self, other):
        return self.name > other.name

    def __copy__(self):
        print('__copy__()')
        return MyClass(self.name)

    def __deepcopy__(self, memo):
        print('__deepcopy__({})'.format(memo))
        return MyClass(copy.deepcopy(self.name, memo))


a = MyClass('a')

sc = copy.copy(a)
dc = copy.deepcopy(a)

Le dictionnaire memo est utilisé pour garder la trace des valeurs qui ont été déjà copiées, évitant ainsi une récursion infinie.

$ python3 copy_hooks.py

__copy__()
__deepcopy__({})

Récursion dans les Copies Profondes

Pour éviter les problèmes lors de la duplication récursive des structures de données, deepcopy() utilise un dictionnaire pour suivre les objets qui ont déjà été copiés. Le dictionnaire est passé à la méthode __deepcopy__() afin qu’il puisse être examiné là aussi.

Le nouvel exemple montre comment une structure de données interconnectée telle qu’un graphe dirigé peut aider à protéger contre la récursivité, en implémentant la méthode __deepcopy__().

# copy_recursion.py
import copy


class Graph:

    def __init__(self, name, connections):
        self.name = name
        self.connections = connections

    def add_connection(self, other):
        self.connections.append(other)

    def __repr__(self):
        return 'Graph(name={}, id={})'.format(
            self.name, id(self))

    def __deepcopy__(self, memo):
        print('\nCalling __deepcopy__ for {!r}'.format(self))
        if self in memo:
            existing = memo.get(self)
            print('  Already copied to {!r}'.format(existing))
            return existing
        print('  Memo dictionary:')
        if memo:
            for k, v in memo.items():
                print('    {}: {}'.format(k, v))
        else:
            print('    (empty)')
        dup = Graph(copy.deepcopy(self.name, memo), [])
        print('  Copying to new object {}'.format(dup))
        memo[self] = dup
        for c in self.connections:
            dup.add_connection(copy.deepcopy(c, memo))
        return dup


root = Graph('root', [])
a = Graph('a', [root])
b = Graph('b', [a, root])
root.add_connection(a)
root.add_connection(b)

dup = copy.deepcopy(root)

La classe Graph inclut certaines méthodes basiques de graphe dirigé. Une instance peut être initialisée avec un nom et une liste de nœuds existants auxquels il est connecté. La méthode add_connection() est utilisée pour paramétrer les connexions bidirectionnelles. Elle est aussi utilisée par l’opérateur de copie profonde.

La méthode __deepcopy__() imprime les messages pour montrer comment elle est appelée, et gère le contenu du dictionnaire memo selon les besoins. Au lieu de copier en gros la liste complète de connexions, il crée une nouvelle liste et y ajoute des copies des connexions individuelles. Cela permet que le dictionnaire memo soit mise à jour à chaque fois qu’un nouveau nœud est dupliqué, et évite les problèmes de récursion ou de copies supplémentaires de nœuds. Comme précédemment, la méthode retourne la copie d’objet quand il est fait.

Copie profonde d’un graphe objet avec des cycles

Le graphe vu dans la figure inclut les nombreux cycles, mais la gestion de la récursion avec le dictionnaire memo empêche la traversée de provoquer une erreur de débordement de pile. Quand le nœud root est copié, il produit la sortie suivante.

$ python3 copy_recursion.py


Calling __deepcopy__ for Graph(name=root, id=4326183824)
  Memo dictionary:
    (empty)
  Copying to new object Graph(name=root, id=4367233208)

Calling __deepcopy__ for Graph(name=a, id=4326186344)
  Memo dictionary:
    Graph(name=root, id=4326183824): Graph(name=root,
id=4367233208)
  Copying to new object Graph(name=a, id=4367234720)

Calling __deepcopy__ for Graph(name=root, id=4326183824)
  Already copied to Graph(name=root, id=4367233208)

Calling __deepcopy__ for Graph(name=b, id=4326183880)
  Memo dictionary:
    Graph(name=root, id=4326183824): Graph(name=root,
id=4367233208)
    Graph(name=a, id=4326186344): Graph(name=a, id=4367234720)
    4326183824: Graph(name=root, id=4367233208)
    4367217936: [Graph(name=root, id=4326183824), Graph(name=a,
id=4326186344)]
    4326186344: Graph(name=a, id=4367234720)
  Copying to new object Graph(name=b, id=4367235000)

La seconde fois où le nœud root est rencontré, pendant que le nœud a soit copié, __deepcopy__() détecte la récursion et réutilise la valeur existante du dictionnaire memo au lieu de créer un nouvel objet.

Lire aussi

pprint - Structures de Données Pretty-Print

But : Structures de données Pretty-print

Le module pprint contient une «jolie imprimante» pour produire des vues esthétiques des structures de données. Le formateur produit des représentations de structures de données qui peuvent être analysées correctement par l’interpréteur, et qui sont aussi faciles à lire pour un humain. La sortie est conservée sur une seule ligne, si possible, et en retrait lorsqu’elle est fractionnée sur plusieurs lignes.

Les exemples dans ce chapitre dépendent tous du script pprint_data.py, qui est montré ici.

# pprint_data.py
data = [
    (1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}),
    (2, {'e': 'E', 'f': 'F', 'g': 'G', 'h': 'H',
         'i': 'I', 'j': 'J', 'k': 'K', 'l': 'L'}),
    (3, ['m', 'n']),
    (4, ['o', 'p', 'q']),
    (5, ['r', 's', 't''u', 'v', 'x', 'y', 'z']),
]

Impression

La manière la plus simple d’utiliser le module est au-travers de la fonction pprint().

# pprint_pprint.py
from pprint import pprint

from pprint_data import data

print('PRINT:')
print(data)
print()
print('PPRINT:')
pprint(data)

pprint() formate un objet et l’écrit dans le flux de données en le passant en argument (ou sys.stdout par défaut).

$ python3 pprint_pprint.py

PRINT:
[(1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}), (2, {'e': 'E', 'f':
'F', 'g': 'G', 'h': 'H', 'i': 'I', 'j': 'J', 'k': 'K', 'l': 'L'}), (
3, ['m', 'n']), (4, ['o', 'p', 'q']), (5, ['r', 's', 'tu', 'v', 'x',
 'y', 'z'])]

PPRINT:
[(1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}),
 (2,
  {'e': 'E',
   'f': 'F',
   'g': 'G',
   'h': 'H',
   'i': 'I',
   'j': 'J',
   'k': 'K',
   'l': 'L'}),
 (3, ['m', 'n']),
 (4, ['o', 'p', 'q']),
 (5, ['r', 's', 'tu', 'v', 'x', 'y', 'z'])]

Formatage

Pour formater une structure de données sans l’écrire directement vers un flux (par exemple, pour la journalisation) utilisez pformat() pour construire une représentation de chaînes.

# pprint_pformat.py
import logging
from pprint import pformat
from pprint_data import data

logging.basicConfig(
    level=logging.DEBUG,
    format='%(levelname)-8s %(message)s',
)

logging.debug('Logging pformatted data')
formatted = pformat(data)
for line in formatted.splitlines():
    logging.debug(line.rstrip())

La chaîne formatée peut alors être imprimée ou journalisée indépendamment.

$ python3 pprint_pformat.py

DEBUG    Logging pformatted data
DEBUG    [(1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}),
DEBUG     (2,
DEBUG      {'e': 'E',
DEBUG       'f': 'F',
DEBUG       'g': 'G',
DEBUG       'h': 'H',
DEBUG       'i': 'I',
DEBUG       'j': 'J',
DEBUG       'k': 'K',
DEBUG       'l': 'L'}),
DEBUG     (3, ['m', 'n']),
DEBUG     (4, ['o', 'p', 'q']),
DEBUG     (5, ['r', 's', 'tu', 'v', 'x', 'y', 'z'])]

Classes Arbitraires

La classe PrettyPrinter utilisée par pprint() peut aussi fonctionner avec des classes personnalisées, si elles définissent une méthode __repr__().

# pprint_arbitrary_object.py
from pprint import pprint


class node:

    def __init__(self, name, contents=[]):
        self.name = name
        self.contents = contents[:]

    def __repr__(self):
        return (
            'node(' + repr(self.name) + ', ' +
            repr(self.contents) + ')'
        )


trees = [
    node('node-1'),
    node('node-2', [node('node-2-1')]),
    node('node-3', [node('node-3-1')]),
]
pprint(trees)

Les représentations de ces objets imbriqués sont combinées par PrettyPrinter pour retourner une représentation complète de chaînes.

$ python3 pprint_arbitrary_object.py

[node('node-1', []),
 node('node-2', [node('node-2-1', [])]),
 node('node-3', [node('node-3-1', [])])]

Récursion

Les structures de données récursives sont représentées avec une référence vers la source originale de données, restituées dans le format <Recursion on typename with id=number>.

# pprint_recursion.py
from pprint import pprint

local_data = ['a', 'b', 1, 2]
local_data.append(local_data)

print('id(local_data) =>', id(local_data))
pprint(local_data)

Dans cet exemple, la liste local_data est ajoutée à elle-même, créant une référence récursive.

$ python3 pprint_recursion.py

id(local_data) => 4358913288
['a', 'b', 1, 2, <Recursion on list with id=4358913288>]

Limiter la Sortie Imbriquée

Pour des structures de données très profondes, il peut ne pas être désirable que la sortie inclut tous les détails. La donnée peut ne pas être formatée proprement, le texte formaté est peut-être trop volumineux pour être géré, ou certaines données peuvent être superflues.

# pprint_depth.py
from pprint import pprint

from pprint_data import data

pprint(data, depth=1)
pprint(data, depth=2)

Utilisez l’argument depth pour contrôler jusqu’où dans la structure de données imbriquée l’imprimante peut faire une récursion. Les niveaux non inclus dans la sortie sont représentés par une ellipse.

$ python3 pprint_depth.py

[(...), (...), (...), (...), (...)]
[(1, {...}), (2, {...}), (3, [...]), (4, [...]), (5, [...])]

Contrôler la Largeur de la Sortie

La sortie par défaut pour le texte formaté est de 80 colonnes. Pour ajuster la largeur, utilisez l’argument width de pprint().

# pprint_width.py
from pprint import pprint

from pprint_data import data

for width in [80, 5]:
    print('WIDTH =', width)
    pprint(data, width=width)
    print()

Quand la largeur est trop petite pour accommoder la structure de données formatée, les lignes ne sont pas tronquées, ni encapsulées au cas où cela introduirait une syntaxe non valide.

$ python3 pprint_width.py

WIDTH = 80
[(1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}),
 (2,
  {'e': 'E',
   'f': 'F',
   'g': 'G',
   'h': 'H',
   'i': 'I',
   'j': 'J',
   'k': 'K',
   'l': 'L'}),
 (3, ['m', 'n']),
 (4, ['o', 'p', 'q']),
 (5, ['r', 's', 'tu', 'v', 'x', 'y', 'z'])]

WIDTH = 5
[(1,
  {'a': 'A',
   'b': 'B',
   'c': 'C',
   'd': 'D'}),
 (2,
  {'e': 'E',
   'f': 'F',
   'g': 'G',
   'h': 'H',
   'i': 'I',
   'j': 'J',
   'k': 'K',
   'l': 'L'}),
 (3,
  ['m',
   'n']),
 (4,
  ['o',
   'p',
   'q']),
 (5,
  ['r',
   's',
   'tu',
   'v',
   'x',
   'y',
   'z'])]

Le drapeau compact demande à pprint() d’essayer de mettre plus de données sur chaque ligne individuelle, plutôt que d’étendre des structures de données complexes sur plusieurs lignes.

# pprint_compact.py
from pprint import pprint

from pprint_data import data

print('DEFAULT:')
pprint(data, compact=False)
print('\nCOMPACT:')
pprint(data, compact=True)

Cet exemple montre que quand une structure de données ne tient pas sur une ligne, elle est scindée (comme pour le deuxième élément de la liste de données). Quand de multiples éléments peuvent tenir sur une ligne, comme avec le troisième ou quatrième membre, ils sont placés de cette façon.

$ python3 pprint_compact.py

DEFAULT:
[(1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}),
 (2,
  {'e': 'E',
   'f': 'F',
   'g': 'G',
   'h': 'H',
   'i': 'I',
   'j': 'J',
   'k': 'K',
   'l': 'L'}),
 (3, ['m', 'n']),
 (4, ['o', 'p', 'q']),
 (5, ['r', 's', 'tu', 'v', 'x', 'y', 'z'])]

COMPACT:
[(1, {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}),
 (2,
  {'e': 'E',
   'f': 'F',
   'g': 'G',
   'h': 'H',
   'i': 'I',
   'j': 'J',
   'k': 'K',
   'l': 'L'}),
 (3, ['m', 'n']), (4, ['o', 'p', 'q']),
 (5, ['r', 's', 'tu', 'v', 'x', 'y', 'z'])]

*[API]: Application Programming Interface - Interface de Programmation Applicative *[PEP]: Python Enhancement Proposals - Propositions d’Amélioration Python *[RSS]: Really Simple Syndication - Syndication de contenu réellement simple *[URL]: Uniform Resource Locator - Repère uniforme de ressource *[p.ex.]: par exemple