Les vues sur des collections en Python 15


Python 3 introduit de nombreux changements qui ont été backportés dans Python 2.7. Parmi eux, les vues, qui sont un concept assez mal expliqué dans la documentation standard.

Dictionary views

Quand on voulait travailler sur les valeurs d’un dictionnaire en Python, on avait deux choix:

  • faire dict.values() et récupérer une liste entière. Créant une liste entière en mémoire.
  • faire dict.itervalues(), et récupérer un générateur. Mais qui ne peut être lu qu’une fois.

Les vues sont une solution intermédiaire: ce sont des objets qui prennent peu de mémoire, mais qui peuvent être lus plusieurs fois.

Exemple:

>>> scores = {'foo': 1, 'bar': 0}
>>> val = scores.viewvalues()
>>> print val
dict_values([1, 0])
>>> 1 in val
True
>>> [x * 2 for x in val]
[2, 0]

Contrairement à une liste, les vues issues d’un dictionnaire ne supportent pas le slicing ou l’assignation et il n’y a aucune garantie d’ordre des éléments. De plus, elles ne peuvent être modifiées.

Bref, une vue ne contient rien, c’est juste un objet qui, quand on accède à son contenu, va le chercher dans le dictionnaire et vous le retourne. C’est ce qu’on appelle un objet proxy: il vous donne l’illusion d’accéder directement aux données pour vous faciliter la vie, généralement en vous les présentant sous une forme différente: ici un itérable.

On peut récupérer des vues pour les valeurs, mais également pour les clés et les couples clés / valeurs. Ces deux types de vues se comportent en plus comme des sets:

>>> scores.viewitems()
dict_items([('foo', 1), ('bar', 0)])
>>> scores.viewkeys() | [3,]
set([3, 'foo', 'bar'])

Puisqu’il est rare d’avoir besoin d’une vraie liste, et comme les vues sont une très bonne alternative aux générateurs, dict.values et consorts retournent des vues en Python 3.

Maintenant vous allez me dire “Mais si les vues sont une si bonne alternative aux générateurs, pourquoi on ne remplace pas tous les générateurs par des vues ?”.

Tout simplement parce que ce n’est pas possible. Un générateur est un mécanisme standard qui permet de produire des valeurs une par une. N’importe qui peut créer un générateur, car c’est un concept portable d’un problème à un autre. On peut l’appliquer à de nombreuses choses: algorithme, flux de données, fichier, etc.

Une vue n’est qu’un proxy qui permet de voir une structure de données sous une autre forme. Il faut coder une vue par type de structure de données, car la vue va chercher les données dans cette structure quand on lui demande. Le code est donc différent à chaque fois.

Python ne permet pas de créer soi-même des vues, mais créer un proxy, c’est à dire un objet qui retourne les valeurs d’un autre objet quand on l’interroge, peut se faire à la main dans tout langage de programmation. Ainsi vous pourriez créer un proxy qui ressemble a une vue des clés d’un dico très simplement:

class keyview(object):
 
    def __init__(self, d):
        self.d = d
 
    def __iter__(self):
        return self.d.iterkeys()
 
>>> view = keyview(scores)
>>> for x in view:
...     print x
...     
foo
bar
>>> list(view)
['foo', 'bar']
>>>

L’implémentation réelle de Python (en C…) ne fait pas vraiment grand chose de plus, juste un travail d’optimisation pour être plus rapide.

memoryview

Les memory views suivent le même principe, mais appliqué à toute structure de données qui supporte le buffer protocole (un certain nombre de méthodes avec un nom et un comportement défini par ce protocole) comme celles trouvées dans le module struct ou array. La structure de données la plus connue qui suit le buffer protocole est la chaîne de caractères.

>>> s = 'Sam & Max eat the road with a Github fork'
>>> ms = memoryview(s)
>>> ms[-1]
'k'
>>> ms[:9]
<memory at 0x25ded60>
>>> ''.join(ms[:9])
'Sam & Max'

Le principal intérêt de la memory view appliquée aux strings, c’est que tout slicing retourne une nouvelle memory view. On peut donc travailler sur des parties de la chaînes sans créer une nouvelle chaîne en mémoire.

En revanche, les chaînes unicodes ne sont pas supportées. Il vous faudra jouer avec encode() et decode().

15 thoughts on “Les vues sur des collections en Python

  • JeromeJ

    Ah tiens j’ignorais … ça à l’air fort intéressant mais ça reste encore assez flou je trouve :/

    Tout le monde s’en fout : dans ton deuxième code, la première ligne (qui est un output) sort de nul part :)

  • Sam Post author

    Corrigé.

    Est-ce que tu peux mettre le doigt sur ce qui est flou ? Comment ça marche ? A quoi ça sert ? Ce que c’est ?

  • JeromeJ

    Je crois avoir ± compris …

    Le terme “proxy” ne m’a pas aidé personnellement.
    Pour le reste, mais encore une fois ce n’est que mon avis personnel, je trouve l’article pas assez “vulgarisé”, dur à comprendre.

    Donc on ne peut pas créer de views soit même ? C’est comme un type “natif” renvoyé par certaines lib ?

    Et le slicing sur une memoryview renvoit une view plutôt que de créer une nouvelle chaine (comme lorsqu’on slice une chaine) c’est ça ?

    J’ai compris pourquoi on ne peut voir des views partout, ceci dit si c’était possible ça serait quand même plus chouette que l’itérateur renvoyé par les générateurs non ? ^^

    Merci quand même sur l’article :p qui éclaire vers des voies fortement intéressantes …

  • Etienne

    @JeromeJ n’a pas tort, c’est un peu technique (allusif?) à partir de “Pourquoi ne remplace-t’on pas les générateurs par des vues?”. Je l’ai relu quatre fois et j’ai toujours pas compris, alors que d’habitude tu te fais comprendre à la première lecture (t’es vraiment fort, y’a pas à dire!). Comme le dit @JeromeJ, “proxy” n’aide pas (c’est quoi un proxy?). Pour moi le moins clair est dans l’histoire de “structure de données”.

    Mais bon, je suis vraiment novice…

    Je viens malgré tout d’utiliser dict.viewkeys et dict.viewitems… Cette partie est tout à fait claire.

  • Sam Post author

    J’ai fais quelques modifs, avec notament une explication courte sur le terme ‘proxy’ et un article qui explique ce qu’est une structure de données.

    Faites moi un feeback sur ce qui est mieux, et ce qui pourrait être plus clair. On va y arriver par itération :-)

  • JeromeJ

    Petite redondance ici :p “Contrairement à une liste, les vues issues d’un dictionnaire ne supportent pas le slicing ou l’assignation et il n’y a aucune garantie d’ordre des éléments. De plus, elles ne peuvent être modifiées.” L’assignation et la modification c’est pas la même chose ? ^^

    “[…] sous une forme différente: ici un itérable.”
    Ça ne serait pas mieux de comparer ça à un tuple ? On peut l’itérer plusieurs fois mais on ne peut le modifier (car l’itérable est à usage unique non ?)

    Merci, c’est globalement plus claire (pour moi en tout cas) :)

  • Sam Post author

    @JeromeJ: l’assignation modifie, mais la modification n’est pas forcément une modification. Par exemple .clear() d’un dicto modifie le dictionnaire, mais ça n’a rien d’une assignation.

    Ensuite, un itérable est tout type sur lequel on peut itérer. Un tuple, un fichier, une liste ou un générateur sont des itérables. On reconnait un itérable au fait qu’on peut lui appliquer une boucle for.

    Ainsi, la vue est un itérable: on peut itérer dessus. Il ne faut pas la comparer à un tuple car un tuple prend autant de place en mémoire qu’il y a d’élement, ce n’est pas le cas d’une vue.

    Un itérateur est un générateur qui itère sur un itérable. Lui n’est lisible qu’une fois.

    Cette subtilité itérable/itérateur se retrouve en anlais: iterable (qui correspond à une caracteristique, celle d’être parcourable un élément à la fois) est différent d’iterator (qui correspond à une nature, celle d’être un parcoureur d’itérable).

    On obtient un itérateur en faisant iter() sur un itérable:

    iterateur = iter([1, 2, 3)
    iterateur.next()
    1

    Je devrais ptêt mettre ça dans un article.

  • Etienne

    Tout ça est parfaitement clair je trouve. Les mystères des proxys ont été dévoilés, et la différence entre un générateur et une vue devient tout à fait claire, même sans savoir ce qu’est exactement une “structure de données” (je vais lire ton post de ce pas).

    Merci Max!

  • Etienne

    Une idée de post, en passant: la question de la copie des objets mutables et donc la question des variables (référence, valeur) en python (je me suis fait avoir avec un dictionnaire hier, j’avais pas réalisé que c’est un mutable).

    Quand j’ai cherché la première fois, je suis tombé sur des trucs du genre: “quand tu copie une liste tu dois faire b = a[:], sinon ça marche pas”. Faut reconnaître que c’est un peu maigre…

  • Kontre

    La toute dernière remarque sur l’unicode, ça ne concerne que python 2.7 ou la branche 3 aussi (puisque les chaînes sont en unicode) ?

  • Sam Post author

    Ca concerne aussi la branche 3. Il n’y pas vraiment tant de différence que ça entre la branche 2 et 3 concernant les strings. Le principal est de se souvenir que les notations littérales ont été échangé:

    – En python 2.7: “string” créé un bytestring, et u”string” créé une chaîne unicode.
    – En python 3: “string” créé une chaîne unicode, et b”string” créé un bytestring.

    Mais la problématique reste la même.

  • OPi

    Intéressant ces articles sur Python.

    Petit conseil, moi j’utiliserais la notation
    print(val)
    plutôt que
    print val
    afin que les codes tournent aussi sous Python 3.

Leave a comment

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.