2.6. Tekst versus bytes

Python heeft twee reekstypen voor ruwe tekengegevens:

  • str – een reeks Unicode-codepunten. Wordt gebruikt voor alle voor mensen leesbare tekst: bestandspaden, logberichten, JSON-payloads.

  • bytes – een reeks gehele getallen in het bereik 0 – 255. Wordt gebruikt voor ruwe binaire gegevens: UART-frames, afbeeldingsbuffers, netwerkpakketten, registerwaarden.

Ze kunnen niet zonder expliciete conversie worden gemengd. Het doorgeven van een str aan een hardware-write-methode veroorzaakt een TypeError, en het omgekeerde wordt ook geweigerd.

Een str van Unicode-codepunten aan de linkerkant en een bytes- reeks van ruwe octetten aan de rechterkant, met encode- en decode-pijlen ertussen.

Een str slaat Unicode-tekens op; een bytes slaat ruwe octetten op. Het overgaan tussen beide is encoderen (str → bytes) en decoderen (bytes → str).

2.6.1. bytes-literals

Een bytes-literal is een string-achtige literal met b als voorvoegsel:

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

Alleen ASCII-tekens zijn rechtstreeks binnen een bytes-literal toegestaan; niet-ASCII-waarden moeten worden geschreven als \xHH-hexescapes.

2.6.2. Encoderen en decoderen

  • str.encode() converteert een string naar bytes met behulp van een benoemde codering (standaard "utf-8").

  • bytes.decode() doet het omgekeerde.

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

UTF-8 is de standaard en de juiste keuze voor alles wat niet-ASCII-tekens kan bevatten. Gebruik "ascii" alleen wanneer de gegevens gegarandeerd platte ASCII zijn; op die manier veroorzaakt een afdwalende niet-ASCII-byte een UnicodeError in plaats van er stilzwijgend doorheen te glippen.

2.6.3. Indexering en slicen

Een bytes-waarde gedraagt zich bij indexering als een reeks gehele getallen, niet als een reeks strings van één byte:

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

Een veelgemaakte fout is het vergelijken van data[0] == "a" en verrast zijn dat het False is – data[0] is een geheel getal, geen string van één teken, dus de twee waarden kunnen nooit overeenkomen.

2.6.4. ord en chr – de brug tussen tekens en gehele getallen

Omdat het indexeren van een bytes een geheel getal teruggeeft maar de rest van het programma waarschijnlijk in tekens denkt, biedt Python twee ingebouwde functies om ertussen te schakelen:

  • ord() – neemt een string van één teken en geeft het bijbehorende gehele codepunt terug.

  • chr() – het omgekeerde: gegeven een geheel getal, geeft het de string van één teken voor dat codepunt terug.

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

Voor ASCII-tekens is het codepunt gelijk aan de bytewaarde, dus ord("a") en b"a"[0] geven beide 97. Daardoor zijn bytevergelijkingen leesbaar in termen van het teken waar het je werkelijk om gaat:

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

En chr() is handig voor het loggen of debuggen wanneer je de afdrukbare vorm van een byte wilt zien:

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

Voor niet-ASCII-tekens geeft ord() het Unicode-codepunt terug, dat niet hetzelfde is als een enkele byte in de gecodeerde vorm; de byterepresentatie hangt af van de codering.

2.6.5. bytearray voor muteerbare buffers

bytes is onveranderlijk – elke “wijziging” geeft een nieuw object terug en laat het origineel ongemoeid. Gebruik voor gegevens die je van plan bent te wijzigen, eraan toe te voegen of stuk voor stuk in te vullen, bytearray. Deze bevat dezelfde inhoud als bytes maar ondersteunt mutatie ter plekke:

>>> 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. Een bytearray maken

De bytearray-constructor accepteert verschillende invoeren:

  • bytearray(8) – een buffer van 8 nul-bytes.

  • bytearray(b"hello") – een muteerbare kopie van een bytes-waarde.

  • bytearray("hello", "utf-8") – een bytearray uit een string, met behulp van de opgegeven codering.

  • bytearray([72, 73, 74]) – een bytearray uit een reeks gehele getallen in 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. Een bytearray wijzigen

Toewijzing via index en slice werkt net als bij een 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')

Afzonderlijke bytes moeten gehele getallen in 0 – 255 zijn; het toewijzen van een ander type veroorzaakt een TypeError of ValueError.

Slice-toewijzing kan de lengte van de buffer veranderen. Een slice vervangen door een langere waarde laat de bytearray groeien; vervangen door een kortere waarde laat hem krimpen. Vervangen door b"" verwijdert de slice volledig:

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

De methoden bytearray.append() en bytearray.extend() voegen bytes aan het einde toe zonder de hele buffer elke keer opnieuw te reserveren:

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

2.6.5.3. Lezen uit een bytearray

Indexering, slicen, iteratie en de inspectiemethoden van bytes (bytes.startswith(), bytes.find(), bytes.strip(), enz.) werken allemaal hetzelfde als bij een bytes-waarde. Indexering geeft een geheel getal terug; slicen geeft weer een bytearray terug:

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

2.6.5.4. Converteren tussen bytes en bytearray

bytes en bytearray converteren naar elkaar met hun constructors. Gebruik dit wanneer een API specifiek één vorm vereist:

>>> 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 voor slicen zonder kopiëren

Het slicen van een bytes of bytearray kopieert normaal gesproken de bytes naar een nieuwe buffer. memoryview stelt dezelfde bytes beschikbaar zonder te kopiëren:

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

Een view over een bytearray is ook beschrijfbaar – het muteren van de view muteert de onderliggende buffer:

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

Grijp naar memoryview wanneer het kopiëren van een slice verspillend zou zijn – doorgaans wanneer dezelfde grote buffer wordt rondgegeven of in stukken wordt verwerkt. Voor alledaags string-achtig werk op kleine bytes is gewoon slicen prima.