Was genau ist der Punkt der Speicheransicht in Python

86

Überprüfen der Dokumentation zur Speicheransicht:

Mit Memoryview-Objekten kann Python-Code ohne Kopieren auf die internen Daten eines Objekts zugreifen, das das Pufferprotokoll unterstützt.

Klasse memoryview (obj)

Erstellen Sie eine Speicheransicht, die auf obj verweist. obj muss das Pufferprotokoll unterstützen. Zu den integrierten Objekten, die das Pufferprotokoll unterstützen, gehören Bytes und Bytearray.

Dann erhalten wir den Beispielcode:

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

Zitat vorbei, jetzt schauen wir uns das genauer an:

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

Also, was ich aus dem Obigen sammle:

Wir erstellen ein Memoryview-Objekt, um die internen Daten eines Pufferobjekts ohne Kopieren verfügbar zu machen. Um jedoch etwas Nützliches mit dem Objekt zu tun (indem wir die vom Objekt bereitgestellten Methoden aufrufen), müssen wir eine Kopie erstellen!

Normalerweise wird eine Speicheransicht (oder das alte Pufferobjekt) benötigt, wenn wir ein großes Objekt haben, und die Slices können auch groß sein. Die Notwendigkeit einer besseren Effizienz wäre vorhanden, wenn wir große Scheiben oder kleine Scheiben nur sehr oft herstellen.

Mit dem obigen Schema sehe ich nicht, wie es für beide Situationen nützlich sein kann, es sei denn, jemand kann mir erklären, was mir hier fehlt.

Edit1:

Wir haben einen großen Datenblock, den wir verarbeiten möchten, indem wir ihn von Anfang bis Ende durchlaufen, z. B. Token vom Anfang eines Zeichenfolgenpuffers extrahieren, bis der Puffer verbraucht ist. In C bedeutet dies, dass ein Zeiger durch den Code verschoben wird Puffer, und der Zeiger kann an jede Funktion übergeben werden, die den Puffertyp erwartet. Wie kann etwas Ähnliches in Python gemacht werden?

Die Leute schlagen Problemumgehungen vor, zum Beispiel verwenden viele String- und Regex-Funktionen Positionsargumente, mit denen das Vorrücken eines Zeigers emuliert werden kann. Hierbei gibt es zwei Probleme: Erstens ist es eine Problemumgehung, Sie müssen Ihren Codierungsstil ändern, um die Mängel zu beheben, und zweitens: Nicht alle Funktionen haben Positionsargumente, z. B. Regex-Funktionen und startswithdo, encode()/ decode()do.

Andere schlagen möglicherweise vor, die Daten in Blöcken zu laden oder den Puffer in kleinen Segmenten zu verarbeiten, die größer als das maximale Token sind. Okay, wir sind uns dieser möglichen Problemumgehungen bewusst, aber wir sollten in Python auf natürlichere Weise arbeiten, ohne zu versuchen, den Codierungsstil an die Sprache anzupassen - nicht wahr?

Edit2:

Ein Codebeispiel würde die Dinge klarer machen. Dies ist, was ich tun möchte und was ich angenommen habe, dass Memoryview es mir auf den ersten Blick ermöglichen würde. Verwenden wir pmview (richtige Speicheransicht) für die gesuchte Funktionalität:

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
Basel Shishani
quelle
9
Die Antwort in der Frage, auf die verwiesen wird, enthält keine Details. Die Frage berührt auch keine potenziellen Probleme aus der Sicht eines Lernenden.
Basel Shishani

Antworten:

83

Ein Grund, warum memoryviews nützlich sind, ist, dass sie im Gegensatz zu bytes/ in Scheiben geschnitten werden können, ohne die zugrunde liegenden Daten zu kopieren str.

Nehmen Sie zum Beispiel das folgende Spielzeugbeispiel.

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

Auf meinem Computer bekomme ich

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

Sie können die quadratische Komplexität des wiederholten Schnitts deutlich erkennen. Selbst mit nur 400000 Iterationen ist es bereits unveränderlich. Mittlerweile ist die Memoryview-Version linear komplex und blitzschnell.

Bearbeiten: Beachten Sie, dass dies in CPython durchgeführt wurde. In Pypy bis 4.0.1 gab es einen Fehler, der dazu führte, dass Speicheransichten eine quadratische Leistung zeigten.

Antimon
quelle
Das Beispiel funktioniert nicht in Python 3TypeError: memoryview: a bytes-like object is required, not 'str'
Calculus
@Jose printals Anweisung funktioniert auch in Python 3 nicht. Dieser Code wurde für Python 2 geschrieben, obwohl die für Python 3 erforderlichen Änderungen ziemlich trivial sind.
Antimon
@ Yumi Tada, strin Python3 ist völlig anders in Python2 definiert.
hcnhcn012
5
Diese Antwort geht nicht auf die Tatsache ein, dass Sie, um etwas "Nützliches" zu tun, wie der Fragesteller angibt, Bytes () verwenden müssen, die das Objekt
kopieren
1
@ Citizen2077 Wie mein Beispiel zeigt, ist es nützlich, Zwischenmanipulationen effizient durchzuführen, selbst wenn Sie es letztendlich in ein Byte-Objekt kopieren.
Antimon
59

memoryviewObjekte eignen sich hervorragend, wenn Sie Teilmengen von Binärdaten benötigen, die nur die Indizierung unterstützen müssen. Anstatt Slices zu erstellen (und neue, möglicherweise große Objekte zu erstellen), um sie an eine andere API zu übergeben , können Sie einfach ein memoryviewObjekt nehmen.

Ein solches API-Beispiel wäre das structModul. Anstatt einen Slice des großen bytesObjekts zu übergeben, um gepackte C-Werte zu analysieren, übergeben Sie memoryviewnur einen Bereich, aus dem Sie Werte extrahieren müssen.

memoryviewObjekte unterstützen tatsächlich das structnative Auspacken; Sie können auf einen Bereich des zugrunde liegenden bytesObjekts mit einem Slice abzielen und dann .cast()die zugrunde liegenden Bytes als lange Ganzzahlen, Gleitkommawerte oder n-dimensionale Listen von Ganzzahlen interpretieren. Dies ermöglicht eine sehr effiziente Interpretation des binären Dateiformats, ohne dass weitere Kopien der Bytes erstellt werden müssen.

Martijn Pieters
quelle
1
Und was machen Sie, wenn Sie Teilmengen benötigen, die mehr als nur die Indizierung unterstützen?!
Basel Shishani
2
@ BaselShishani: nicht verwenden a memoryview. Sie haben es dann mit Text zu tun, nicht mit Binärdaten.
Martijn Pieters
Ja, mit Text umgehen. Wir verwenden also keine Speicheransicht. Gibt es eine Alternative?
Basel Shishani
Welches Problem versuchen Sie zu lösen? Sind die Teilzeichenfolgen, die Sie zum Testen benötigen, so groß?
Martijn Pieters
6
@BaselShishani: Wenn Sie eine Speicheransicht aufteilen, wird eine neue Speicheransicht zurückgegeben, die nur diese Region abdeckt.
Martijn Pieters
5

Lassen Sie mich klarstellen, wo der Fehler beim Verstehen liegt.

Der Fragesteller erwartete, wie ich, in der Lage zu sein, eine Speicheransicht zu erstellen, die ein Segment eines vorhandenen Arrays auswählt (z. B. ein Byte oder ein Bytearray). Wir haben daher so etwas erwartet wie:

desired_slice_view = memoryview(existing_array, start_index, end_index)

Leider gibt es keinen solchen Konstruktor, und in den Dokumenten wird nicht klargestellt, was stattdessen zu tun ist.

Der Schlüssel ist, dass Sie zuerst eine Speicheransicht erstellen müssen, die das gesamte vorhandene Array abdeckt. Aus dieser Speicheransicht können Sie eine zweite Speicheransicht erstellen, die einen Teil des vorhandenen Arrays wie folgt abdeckt:

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

Kurz gesagt, der Zweck der ersten Zeile besteht einfach darin, ein Objekt bereitzustellen, dessen Slice-Implementierung (dunder-getitem) eine Speicheransicht zurückgibt.

Das mag unordentlich erscheinen, aber man kann es auf verschiedene Arten rationalisieren:

  1. Unsere gewünschte Ausgabe ist eine Speicheransicht, die ein Stück von etwas ist. Normalerweise erhalten wir ein geschnittenes Objekt von einem Objekt desselben Typs, indem wir den Slice-Operator [10:20] verwenden. Es gibt also einen Grund zu der Annahme, dass wir unsere gewünschte_Slice_view aus einer Speicheransicht abrufen müssen und dass der erste Schritt daher darin besteht, eine Speicheransicht des gesamten zugrunde liegenden Arrays zu erhalten.

  2. Die naive Erwartung eines Memoryview-Konstruktors mit Start- und Endargumenten berücksichtigt nicht, dass die Slice-Spezifikation wirklich die gesamte Ausdruckskraft des üblichen Slice-Operators benötigt (einschließlich Dinge wie [3 :: 2] oder [: -4] usw.). Es gibt keine Möglichkeit, nur den vorhandenen (und verstandenen) Operator in diesem einzeiligen Konstruktor zu verwenden. Sie können es nicht an das Argument exist_array anhängen, da dadurch ein Slice dieses Arrays erstellt wird, anstatt dem Memoryview-Konstruktor einige Slice-Parameter mitzuteilen. Und Sie können den Operator selbst nicht als Argument verwenden, da es sich um einen Operator und nicht um einen Wert oder ein Objekt handelt.

Möglicherweise könnte ein Memoryview-Konstruktor ein Slice-Objekt aufnehmen:

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

... aber das ist nicht sehr zufriedenstellend, da Benutzer etwas über das Slice-Objekt und die Bedeutung seiner Konstruktorparameter lernen müssten, wenn sie bereits in der Notation des Slice-Operators denken.

gwideman
quelle
4

Hier ist Python3-Code.

#!/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
quelle
1

Exzellentes Beispiel von Antimon. Tatsächlich können Sie in Python3 data = 'x' * n durch data = bytes (n) ersetzen und die folgenden Anweisungen in Klammern setzen, um Anweisungen zu drucken:

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)
user2494386
quelle