¿Cuál es exactamente el punto de memoryview en Python?

84

Comprobando la documentación en memoryview:

Los objetos memoryview permiten que el código Python acceda a los datos internos de un objeto que admita el protocolo de búfer sin copiar.

clase memoryview (obj)

Cree una vista de memoria que haga referencia a obj. obj debe admitir el protocolo de búfer. Los objetos integrados que admiten el protocolo de búfer incluyen bytes y bytearray.

Luego se nos da el código de muestra:

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

Cita terminada, ahora echemos un vistazo más de cerca:

>>> 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
>>> 

Entonces, lo que deduzco de lo anterior:

Creamos un objeto memoryview para exponer los datos internos de un objeto búfer sin copiar, sin embargo, para hacer algo útil con el objeto (llamando a los métodos proporcionados por el objeto), ¡tenemos que crear una copia!

Por lo general, la vista de memoria (o el antiguo objeto de búfer) sería necesaria cuando tenemos un objeto grande, y los cortes también pueden ser grandes. La necesidad de una mayor eficiencia estaría presente si estamos haciendo rodajas grandes, o haciendo rodajas pequeñas pero un gran número de veces.

Con el esquema anterior, no veo cómo puede ser útil para ninguna de las situaciones, a menos que alguien pueda explicarme qué me estoy perdiendo aquí.

Edición 1:

Tenemos una gran cantidad de datos, queremos procesarlos avanzando a través de ellos de principio a fin, por ejemplo, extrayendo tokens desde el inicio de un búfer de cadena hasta que el búfer se consume. búfer, y el puntero se puede pasar a cualquier función que espere el tipo de búfer. ¿Cómo se puede hacer algo similar en Python?

La gente sugiere soluciones alternativas, por ejemplo, muchas funciones de cadena y expresiones regulares toman argumentos de posición que se pueden usar para emular el avance de un puntero. Hay dos problemas con esto: primero es una solución alternativa, se ve obligado a cambiar su estilo de codificación para superar las deficiencias, y segundo: no todas las funciones tienen argumentos de posición, por ejemplo, funciones regex y startswithdo, encode()/ decode()don't.

Otros pueden sugerir cargar los datos en fragmentos o procesar el búfer en segmentos pequeños más grandes que el token máximo. De acuerdo, somos conscientes de estas posibles soluciones, pero se supone que debemos trabajar de una manera más natural en Python sin intentar modificar el estilo de codificación para que se ajuste al lenguaje, ¿no es así?

Edición 2:

Una muestra de código aclararía las cosas. Esto es lo que quiero hacer y lo que asumí que memoryview me permitiría hacer a primera vista. Usemos pmview (vista de memoria adecuada) para la funcionalidad que estoy buscando:

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
Basilea Shishani
fuente
9
La respuesta en la pregunta mencionada no proporciona detalles. La pregunta tampoco aborda problemas potenciales desde el punto de vista del alumno.
Basel Shishani

Respuestas:

81

Una razón por la que memoryviewlos correos electrónicos son útiles es que se pueden dividir sin copiar los datos subyacentes, a diferencia de bytes/ str.

Por ejemplo, tome el siguiente ejemplo de juguete.

import time
for n in (100000, 200000, 300000, 400000):
    data = 'x'*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
    start = time.time()
    b = memoryview(data)
    while b:
        b = b[1:]
    print 'memoryview', n, time.time()-start

En mi computadora, obtengo

bytes 100000 0.200068950653
bytes 200000 0.938908100128
bytes 300000 2.30898690224
bytes 400000 4.27718806267
memoryview 100000 0.0100269317627
memoryview 200000 0.0208270549774
memoryview 300000 0.0303030014038
memoryview 400000 0.0403470993042

Puede ver claramente la complejidad cuadrática del corte repetido de la cuerda. Incluso con solo 400000 iteraciones, ya es inmanejable. Mientras tanto, la versión de memoryview tiene una complejidad lineal y es increíblemente rápida.

Editar: tenga en cuenta que esto se hizo en CPython. Hubo un error en Pypy hasta 4.0.1 que causó que las vistas de memoria tuvieran un rendimiento cuadrático.

Antimonio
fuente
El ejemplo no funciona en Python 3TypeError: memoryview: a bytes-like object is required, not 'str'
Calculus
@Jose printcomo una declaración tampoco funciona en Python 3. Este código fue escrito para Python 2, aunque los cambios requeridos para Python 3 son bastante triviales.
Antimony
@Yumi Tada, stren python3 es totalmente diferente definido en python2.
hcnhcn012
3
Esta respuesta no aborda el hecho de que para hacer algo "útil" como dice el autor de la pregunta, debe usar bytes () que copia el objeto ...
ragardner
1
@ citizen2077 Como muestra mi ejemplo, es útil para realizar manipulaciones intermedias de manera eficiente, incluso si finalmente lo copia en un objeto de bytes.
Antimony
58

memoryviewLos objetos son excelentes cuando necesita subconjuntos de datos binarios que solo necesitan admitir la indexación. En lugar de tener que tomar porciones (y crear nuevos objetos potencialmente grandes) para pasar a otra API , simplemente puede tomar un memoryviewobjeto.

Uno de esos ejemplos de API sería el structmódulo. En lugar de pasar una porción del bytesobjeto grande para analizar los valores C empaquetados, pasa memoryviewsolo una región de la que necesita extraer los valores.

memoryview los objetos, de hecho, apoyan struct desembalaje de forma nativa; puede apuntar a una región del bytesobjeto subyacente con un segmento, luego usar .cast()para 'interpretar' los bytes subyacentes como enteros largos, o valores de punto flotante, o listas n-dimensionales de enteros. Esto hace que las interpretaciones de formatos de archivos binarios sean muy eficientes, sin tener que crear más copias de los bytes.

Martijn Pieters
fuente
1
¿Y qué hace cuando necesita subconjuntos que admitan más que indexación?
Basel Shishani
2
@BaselShishani: no use un memoryview. Se trata de texto, no de datos binarios.
Martijn Pieters
Sí, se trata de texto. Entonces no usamos memoryview, ¿hay alguna alternativa?
Basel Shishani
¿Que problema estas tratando de resolver? ¿Las subcadenas que necesita probar son tan grandes?
Martijn Pieters
6
@BaselShishani: cortar una vista de memoria devuelve una nueva vista de memoria que cubre solo esa región.
Martijn Pieters
4

Permítanme aclarar dónde reside el problema de la comprensión aquí.

El interrogador, como yo, esperaba poder crear una vista de memoria que seleccionara una porción de una matriz existente (por ejemplo, bytes o bytearray). Por lo tanto, esperábamos algo como:

desired_slice_view = memoryview(existing_array, start_index, end_index)

Por desgracia, no existe tal constructor, y los documentos no señalan qué hacer en su lugar.

La clave es que primero debe crear una vista de memoria que cubra toda la matriz existente. Desde esa vista de memoria, puede crear una segunda vista de memoria que cubra una porción de la matriz existente, como esta:

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

En resumen, el propósito de la primera línea es simplemente proporcionar un objeto cuya implementación de corte (dunder-getitem) devuelva una vista de memoria.

Eso puede parecer desordenado, pero se puede racionalizar de dos maneras:

  1. Nuestro resultado deseado es una vista de memoria que es un trozo de algo. Normalmente obtenemos un objeto cortado de un objeto del mismo tipo, usando el operador de corte [10:20] en él. Entonces, hay alguna razón para esperar que necesitemos obtener nuestra vista de corte_deseado de una vista de memoria y, por lo tanto, el primer paso es obtener una vista de memoria de toda la matriz subyacente.

  2. La expectativa ingenua de un constructor de vista de memoria con argumentos de inicio y fin no tiene en cuenta que la especificación de segmento realmente necesita toda la expresividad del operador de segmento habitual (incluidas cosas como [3 :: 2] o [: -4], etc.). No hay forma de usar el operador existente (y entendido) en ese constructor de una sola línea. No puede adjuntarlo al argumento existing_array, ya que eso hará una porción de esa matriz, en lugar de decirle al constructor de memoryview algunos parámetros de la porción. Y no puede usar el operador en sí mismo como argumento, porque es un operador y no un valor u objeto.

Posiblemente, un constructor de memoryview podría tomar un objeto de corte:

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

... pero eso no es muy satisfactorio, ya que los usuarios tendrían que aprender sobre el objeto slice y qué significan los parámetros de su constructor, cuando ya piensan en términos de la notación del operador slice.

gwideman
fuente
4

Aquí está el código 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))
jimaf
fuente
1

Excelente ejemplo de Antimony. En realidad, en Python3, puede reemplazar data = 'x' * n por data = bytes (n) y poner paréntesis para imprimir declaraciones como se muestra a continuación:

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)
usuario2494386
fuente