2.6. Text vs. Bytes

Python hat zwei Sequenztypen für rohe Zeichendaten:

  • str – eine Folge von Unicode-Codepunkten. Wird für jeglichen menschenlesbaren Text verwendet: Dateipfade, Logmeldungen, JSON-Nutzdaten.

  • bytes – eine Folge von Ganzzahlen im Bereich 0 – 255. Wird für rohe Binärdaten verwendet: UART-Frames, Bildpuffer, Netzwerkpakete, Registerwerte.

Sie können nicht ohne eine explizite Konvertierung gemischt werden. Das Übergeben eines str an eine Hardware-write-Methode löst einen TypeError aus, und der umgekehrte Fall wird ebenfalls zurückgewiesen.

Ein str aus Unicode-Codepunkten links und eine bytes-Folge aus rohen Oktetts rechts, mit Encode- und Decode-Pfeilen dazwischen.

Ein str speichert Unicode-Zeichen; ein bytes speichert rohe Oktetts. Der Übergang zwischen ihnen ist Encoding (str → bytes) und Decoding (bytes → str).

2.6.1. bytes-Literale

Ein bytes-Literal ist ein string-ähnliches Literal mit vorangestelltem b:

header  = b"OMV"
crlf    = b"\r\n"
payload = b"\x01\x02\x03"

Innerhalb eines bytes-Literals sind direkt nur ASCII-Zeichen erlaubt; Nicht-ASCII-Werte müssen als \xHH-Hex-Escapes geschrieben werden.

2.6.2. Encoding und Decoding

  • str.encode() konvertiert einen String mithilfe einer benannten Kodierung (Standard "utf-8") in bytes.

  • bytes.decode() macht das Umgekehrte.

>>> "hello".encode()
b'hello'
>>> "héllo".encode()
b'h\xc3\xa9llo'              # é is two bytes in UTF-8
>>> b"hello".decode()
'hello'

UTF-8 ist der Standard und die richtige Wahl für alles, was Nicht-ASCII-Zeichen enthalten könnte. Verwenden Sie "ascii" nur dann, wenn garantiert ist, dass die Daten reines ASCII sind; auf diese Weise löst ein verirrtes Nicht-ASCII-Byte einen UnicodeError aus, statt stillschweigend durchzulaufen.

2.6.3. Indizierung und Slicing

Ein bytes-Wert verhält sich beim Indizieren wie eine Folge von Ganzzahlen, nicht wie eine Folge von Ein-Byte-Strings:

>>> data = b"abc"
>>> data[0]
97                           # the int 97, not 'a'
>>> data[0:1]
b'a'                         # slicing returns bytes

Ein häufiger Fehler ist der Vergleich data[0] == "a" mit der überraschenden Feststellung, dass er False ergibt – data[0] ist eine Ganzzahl, kein einzeichiger String, sodass die beiden Werte niemals übereinstimmen können.

2.6.4. ord und chr – Brücke zwischen Zeichen und Ganzzahlen

Da die Indizierung eines bytes eine Ganzzahl zurückgibt, der Rest des Programms aber wahrscheinlich in Zeichen denkt, stellt Python zwei eingebaute Funktionen zum Wechseln zwischen ihnen bereit:

  • ord() – nimmt einen einzeichigen String entgegen und gibt dessen ganzzahligen Codepunkt zurück.

  • chr() – das Gegenstück: gibt zu einer Ganzzahl den einzeichigen String für diesen Codepunkt zurück.

>>> ord("a")
97
>>> chr(97)
'a'
>>> ord("A"), chr(0x41)
(65, 'A')

Für ASCII-Zeichen entspricht der Codepunkt dem Byte-Wert, sodass ord("a") und b"a"[0] beide 97 ergeben. Dadurch lassen sich Byte-Vergleiche in Bezug auf das Zeichen lesen, das Sie tatsächlich interessiert:

>>> data = b"abc"
>>> data[0] == ord("a")          # instead of the magic number 97
True

Und chr() ist praktisch zum Loggen oder Debuggen, wenn Sie die druckbare Form eines Bytes sehen möchten:

>>> chr(data[0])
'a'

Für Nicht-ASCII-Zeichen gibt ord() den Unicode-Codepunkt zurück, der nicht mit irgendeinem einzelnen Byte in der kodierten Form übereinstimmt; die Byte-Darstellung hängt von der Kodierung ab.

2.6.5. bytearray für veränderbare Puffer

bytes ist unveränderlich – jede „Modifikation“ gibt ein neues Objekt zurück und lässt das Original unangetastet. Für Daten, die Sie verändern, erweitern oder Stück für Stück befüllen möchten, verwenden Sie bytearray. Es enthält denselben Inhalt wie bytes, unterstützt aber Veränderung an Ort und Stelle:

>>> s = b"hello"
>>> s[0] = ord("H")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

>>> s = bytearray(b"hello")
>>> s[0] = ord("H")
>>> s
bytearray(b'Hello')

2.6.5.1. Ein bytearray erstellen

Der bytearray-Konstruktor akzeptiert mehrere Eingaben:

  • bytearray(8) – ein Puffer aus 8 Null-Bytes.

  • bytearray(b"hello") – eine veränderbare Kopie eines bytes-Werts.

  • bytearray("hello", "utf-8") – ein bytearray aus einem String unter Verwendung der angegebenen Kodierung.

  • bytearray([72, 73, 74]) – ein bytearray aus einer Folge von Ganzzahlen im Bereich 0 – 255 (hier b"HIJ").

>>> bytearray(4)
bytearray(b'\x00\x00\x00\x00')
>>> bytearray(b"abc")
bytearray(b'abc')
>>> bytearray("café", "utf-8")
bytearray(b'caf\xc3\xa9')

2.6.5.2. Ein bytearray verändern

Indizierte und Slice-Zuweisung funktionieren genauso wie bei einer list:

>>> buf = bytearray(8)        # 8 zero bytes
>>> buf[0] = 0xFF             # one byte at a time
>>> buf[1:4] = b"ABC"         # replace a slice
>>> buf
bytearray(b'\xffABC\x00\x00\x00\x00')

Einzelne Bytes müssen Ganzzahlen im Bereich 0 – 255 sein; das Zuweisen eines anderen Typs löst einen TypeError oder ValueError aus.

Slice-Zuweisung kann die Länge des Puffers ändern. Das Ersetzen eines Slice durch einen längeren Wert vergrößert das bytearray; das Ersetzen durch einen kürzeren Wert verkleinert es. Das Ersetzen durch b"" löscht den Slice vollständig:

>>> buf = bytearray(b"abcdef")
>>> buf[1:3] = b"XYZ"         # 2 bytes replaced with 3
>>> buf
bytearray(b'aXYZdef')
>>> buf[1:4] = b""            # delete the inserted run
>>> buf
bytearray(b'adef')

Die Methoden bytearray.append() und bytearray.extend() fügen Bytes am Ende hinzu, ohne den gesamten Puffer jedes Mal neu zu allozieren:

>>> buf = bytearray()
>>> buf.append(0x01)
>>> buf.extend(b"abc")
>>> buf
bytearray(b'\x01abc')

2.6.5.3. Aus einem bytearray lesen

Indizierung, Slicing, Iteration und die bytes-Inspektionsmethoden (bytes.startswith(), bytes.find(), bytes.strip() usw.) funktionieren alle genauso wie bei einem bytes-Wert. Die Indizierung gibt eine Ganzzahl zurück; das Slicing gibt ein weiteres bytearray zurück:

>>> buf = bytearray(b"OpenMV")
>>> buf[0]
79
>>> buf[0:4]
bytearray(b'Open')
>>> buf.startswith(b"Open")
True

2.6.5.4. Zwischen bytes und bytearray konvertieren

bytes und bytearray lassen sich mit ihren Konstruktoren ineinander konvertieren. Verwenden Sie dies, wenn eine API gezielt eine bestimmte Form erfordert:

>>> ba = bytearray(b"hello")
>>> snapshot = bytes(ba)      # immutable copy
>>> ba[0] = ord("H")
>>> ba, snapshot
(bytearray(b'Hello'), b'hello')

2.6.5.5. memoryview für Slicing ohne Kopieren

Das Slicing eines bytes oder bytearray kopiert die Bytes normalerweise in einen neuen Puffer. memoryview legt dieselben Bytes ohne Kopieren offen:

>>> buf = bytearray(b"OpenMV Cam")
>>> view = memoryview(buf)
>>> view[0:6]                 # shares storage with buf
<memoryview ...>
>>> bytes(view[0:6])          # materialise as bytes when needed
b'OpenMV'

Eine View über ein bytearray ist außerdem beschreibbar – das Verändern der View verändert den zugrunde liegenden Puffer:

>>> view[0] = ord("o")
>>> buf
bytearray(b'openMV Cam')

Greifen Sie zu memoryview, wenn das Kopieren eines Slice verschwenderisch wäre – typischerweise wenn derselbe große Puffer herumgereicht oder in Teilen verarbeitet wird. Für alltägliche string-artige Arbeit an kleinen bytes ist einfaches Slicing völlig in Ordnung.