125 votes

Quel est l'intérêt de memoryview en Python ?

Vérification de la documentation sur memoryview :

Les objets memoryviews permettent au code Python d'accéder aux données internes d'un qui supporte le protocole de tampon, sans avoir à le copier.

classe memoryview (obj)

Créez un memoryview qui fait référence à obj. obj doit supporter le protocole tampon. Objets intégrés qui prennent en charge le protocole des tampons incluent bytes et bytearray.

Ensuite, on nous donne l'exemple de code :

>>> v = memoryview(b'abcefg')
>>> v[1]
98
>>> v[-1]
103
>>> v[1:4]
<memory at 0x7f3ddc9f4350>
>>> bytes(v[1:4])
b'bce'

Citation terminée, regardons maintenant de plus près :

>>> b = b'long bytes stream'
>>> b.startswith(b'long')
True
>>> v = memoryview(b)
>>> vsub = v[5:]
>>> vsub.startswith(b'bytes')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'memoryview' object has no attribute 'startswith'
>>> bytes(vsub).startswith(b'bytes')
True
>>> 

D'après ce que je comprends de ce qui précède :

Nous créons un objet memoryview pour exposer les données internes d'un objet tampon sans les copier. Toutefois, pour faire quoi que ce soit d'utile avec l'objet (en appelant les méthodes fournies par l'objet), nous devons créer une copie !

Habituellement, memoryview (ou l'ancien objet tampon) est nécessaire lorsque nous avons un objet de grande taille, et les tranches peuvent être grandes aussi. Le besoin d'une meilleure efficacité serait présent si nous faisons de grandes tranches, ou si nous faisons de petites tranches mais un grand nombre de fois.

Avec le schéma ci-dessus, je ne vois pas comment il peut être utile dans l'une ou l'autre situation, à moins que quelqu'un puisse m'expliquer ce que je manque ici.

Edit1 :

Nous avons un gros morceau de données, nous voulons le traiter en le parcourant du début à la fin. par exemple, extraire des tokens depuis le début d'un tampon de chaînes de caractères jusqu'à ce que le tampon soit épuisé. En C, cela revient à faire avancer un pointeur dans le tampon, et le pointeur peut être transmis à toute fonction qui attend le type de tampon. Comment peut-on faire quelque chose de similaire en Python ?

Les gens suggèrent des solutions de contournement, par exemple, de nombreuses fonctions de chaînes de caractères et de regex prennent des arguments de position. qui peuvent être utilisés pour émuler l'avancement d'un pointeur. Il y a deux problèmes avec cela : premièrement il s'agit d'une solution de contournement, vous êtes obligé de modifier votre style de codage pour pallier les inconvénients, et deuxièmement, toutes les fonctions n'ont pas d'arguments de position, par exemple les fonctions regex et les fonctions startswith faire, encode() / decode() ne le faites pas.

D'autres pourraient suggérer de charger les données par morceaux, ou de traiter la mémoire tampon en petits morceaux. segments plus grands que le jeton maximum. Ok, donc nous sommes conscients de ces possibles possibles, mais nous sommes censés travailler de façon plus naturelle en Python sans essayer de sans essayer d'adapter le style de codage au langage, n'est-ce pas ?

Edit2 :

Un exemple de code rendrait les choses plus claires. Voici ce que je veux faire, et ce que je pensais que memoryview me permettrait de faire à première vue. Utilisons pmview (proper memory view) pour la fonctionnalité que je recherche :

tokens = []
xlarge_str = get_string()
xlarge_str_view =  pmview(xlarge_str)

while True:
    token =  get_token(xlarge_str_view)
    if token: 
        xlarge_str_view = xlarge_str_view.vslice(len(token)) 
        # vslice: view slice: default stop paramter at end of buffer
        tokens.append(token)
    else:   
        break

116voto

Antimony Points 13190

Une raison memoryview sont utiles car ils peuvent être découpés sans copier les données sous-jacentes, contrairement aux bytes / str .

Prenons l'exemple du jouet suivant.

import time
for n in (100000, 200000, 300000, 400000):
    data = b'x'*n
    start = time.time()
    b = data
    while b:
        b = b[1:]
    print(f'     bytes {n} {time.time() - start:0.3f}')

for n in (100000, 200000, 300000, 400000):
    data = b'x'*n
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print(f'memoryview {n} {time.time() - start:0.3f}')

Sur mon ordinateur, j'obtiens

     bytes 100000 0.211
     bytes 200000 0.826
     bytes 300000 1.953
     bytes 400000 3.514
memoryview 100000 0.021
memoryview 200000 0.052
memoryview 300000 0.043
memoryview 400000 0.077

Vous pouvez clairement voir la complexité quadratique du découpage répété de la chaîne de caractères. Même avec seulement 400000 itérations, c'est déjà ingérable. Pendant ce temps, le memoryview a une complexité linéaire et est rapide comme l'éclair.

Edit : Notez que cela a été fait en CPython. Il y avait un bogue dans Pypy jusqu'à la version 4.0.1 qui provoquait des performances quadratiques pour les memoryviews.

82voto

Martijn Pieters Points 271458

memoryview sont parfaits lorsque vous avez besoin de sous-ensembles de données binaires qui ne doivent supporter que l'indexation. Au lieu de devoir prendre des tranches (et créer de nouveaux objets, potentiellement volumineux) pour les passer à la fonction une autre API vous pouvez juste prendre un memoryview objet.

Un tel exemple d'API serait le struct module. Au lieu de transmettre une tranche du grand module bytes pour analyser les valeurs C emballées, vous passez dans un objet memoryview de la seule région dont vous devez extraire les valeurs.

memoryview les objets, en fait, soutiennent struct déballage natif ; vous pouvez cibler une région de l'architecture sous-jacente. bytes avec une tranche, puis utiliser .cast() pour "interpréter" les octets sous-jacents comme des entiers longs, des valeurs à virgule flottante ou des listes d'entiers à n dimensions. Cela permet d'interpréter très efficacement les formats de fichiers binaires, sans avoir à créer davantage de copies des octets.

9voto

gwideman Points 657

Permettez-moi de préciser où se situe le problème de compréhension ici.

L'auteur de la question, comme moi, s'attendait à pouvoir créer un memoryview qui sélectionne une tranche d'un tableau existant (par exemple un bytes ou bytearray). Nous nous attendions donc à quelque chose comme :

desired_slice_view = memoryview(existing_array, start_index, end_index)

Hélas, ce constructeur n'existe pas, et la documentation ne précise pas ce qu'il faut faire à la place.

La clé est que vous devez d'abord créer une vue mémoire qui couvre l'ensemble du tableau existant. À partir de ce memoryview, vous pouvez créer un second memoryview qui couvre une tranche du tableau existant, comme ceci :

whole_view = memoryview(existing_array)
desired_slice_view = whole_view[10:20]

En bref, le but de la première ligne est simplement de fournir un objet dont l'implémentation de la tranche (dunder-getitem) retourne un memoryview.

Cela peut sembler désordonné, mais on peut le rationaliser de plusieurs façons :

  1. La sortie souhaitée est une vue mémoire qui est une tranche de quelque chose. Normalement, nous obtenons un objet découpé à partir d'un objet du même type, en utilisant l'opérateur de découpage [10:20]. Il y a donc une raison de s'attendre à ce que nous devions obtenir notre vue_slice_ désirée à partir d'une vue mémoire, et que la première étape consiste donc à obtenir une vue mémoire de l'ensemble du tableau sous-jacent.

  2. L'attente naïve d'un constructeur de vue mémoire avec des arguments de début et de fin ne prend pas en compte le fait que la spécification de la tranche nécessite vraiment toute l'expressivité de l'opérateur de tranche habituel (y compris des choses comme [3::2] ou [:-4], etc.) Il n'y a aucun moyen d'utiliser simplement l'opérateur existant (et compris) dans ce constructeur en une ligne. Vous ne pouvez pas l'attacher à l'argument existing_array, car cela fera une tranche de ce tableau, au lieu d'indiquer au constructeur memoryview des paramètres de tranche. Et vous ne pouvez pas utiliser l'opérateur lui-même comme argument, car c'est un opérateur et non une valeur ou un objet.

On pourrait imaginer qu'un constructeur de memoryview prenne un objet slice :

desired_slice_view = memoryview(existing_array, slice(1, 5, 2) )

... mais ce n'est pas très satisfaisant, car les utilisateurs devraient apprendre l'objet tranche et la signification des paramètres de son constructeur, alors qu'ils pensent déjà en termes de notation de l'opérateur tranche.

3voto

jimaf Points 49

Voici le code python3.

#!/usr/bin/env python3

import time
for n in (100000, 200000, 300000, 400000):
    data = b'x'*n
    start = time.time()
    b = data
    while b:
        b = b[1:]
    print ('bytes {:d} {:f}'.format(n,time.time()-start))

for n in (100000, 200000, 300000, 400000):
    data = b'x'*n
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print ('memview {:d} {:f}'.format(n,time.time()-start))

2voto

user2494386 Points 41

Excellent exemple d'Antimoine. En fait, dans Python3, vous pouvez remplacer data = 'x'*n par data = bytes(n) et mettre des parenthèses aux instructions d'impression comme ci-dessous :

import time
for n in (100000, 200000, 300000, 400000):
    #data = 'x'*n
    data = bytes(n)
    start = time.time()
    b = data
    while b:
        b = b[1:]
    print('bytes', n, time.time()-start)

for n in (100000, 200000, 300000, 400000):
    #data = 'x'*n
    data = bytes(n)
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print('memoryview', n, time.time()-start)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X