Python
Care este diferența dintre `str` și `bytes` în Python și când convertești între ele
Pe scurt
str este o secvență de puncte de cod Unicode — text, fără nicio codificare atașată. bytes este o secvență de întregi între 0 și 255 — date brute, fără niciun înțeles textual. Trecerea de la text la octeți se face cu encode(codificare), invers cu decode(codificare), iar codificarea trebuie știută: nu este stocată nicăieri în valoarea bytes. Cele două tipuri nu se amestecă niciodată implicit, iar regula practică este să decodifici imediat ce datele intră în program și să codifici doar la ieșire — tot ce este în mijloc lucrează cu str.
Cum funcționează
Un str stochează puncte de cod, nu caractere tipărite și nu octeți. CPython alege intern reprezentarea cea mai compactă care acoperă cel mai mare punct de cod din șir — un octet, doi sau patru per poziție — dar aceasta este o optimizare invizibilă. len numără puncte de cod, s[i] returnează un str de lungime 1, iar comparațiile și dispersia operează pe puncte de cod.
Un bytes stochează octeți. len numără octeți, iar b[i] returnează un int, nu un bytes de lungime 1 — asimetria față de str este deliberată și surprinde constant. Feliile, în schimb, returnează bytes, deci b[0:1] și b[0] au tipuri diferite.
encode aplică un algoritm de codificare punctelor de cod și produce octeți; decode face drumul invers. Rezultatul lui encode nu poartă nicio informație despre codificarea folosită. Un fișier, un socket sau un câmp de bază de date care conține octeți este ambiguu până când cineva declară cum trebuie citit — de aceea codificarea aparține protocolului sau convenției, nu datelor.
Nu orice text intră în orice codificare, și nu orice secvență de octeți este validă în orice codificare. latin-1 acoperă doar primele 256 de puncte de cod, deci ș ridică UnicodeEncodeError. Invers, octeții UTF-8 ai lui ș nu formează o secvență ASCII validă, deci decode("ascii") ridică UnicodeDecodeError. Periculoasă este combinația care nu eșuează: latin-1 decodifică orice secvență de octeți, pentru că mapează fiecare octet la punctul de cod cu același număr — produce text greșit fără nicio eroare.
Parametrul errors controlează ce se întâmplă la eșec, iar alegerea lui determină dacă informația se pierde. replace și ignore distrug octeții problematici definitiv; surrogateescape îi ascunde în puncte de cod din zona substituenților, de unde pot fi recuperați exact la recodificare. Invariantul de reținut: str este text, bytes este transport; conversia este singurul loc unde codificarea contează, deci ține-o la marginile programului.
Cum arată la rulare
Creează un fișier:
touch depot_payload.py
Rulează-l cu:
python3 depot_payload.py
Două tipuri, două unități de măsură
Cel mai mic caz: același text în ambele reprezentări, măsurat și indexat.
label = "Iași depot"
raw = label.encode("utf-8")
print("str type :", type(label).__name__, "len:", len(label))
print("bytes type:", type(raw).__name__, "len:", len(raw))
print("raw :", raw)
print("raw[0] :", raw[0], type(raw[0]).__name__)
print("label[0] :", label[0], type(label[0]).__name__)
print("round trip:", raw.decode("utf-8") == label)
try:
label + raw
except TypeError as exc:
print("TypeError:", exc)
str type : str len: 10
bytes type: bytes len: 11
raw : b'Ia\xc8\x99i depot'
raw[0] : 73 int
label[0] : I str
round trip: True
TypeError: can only concatenate str (not "bytes") to str
- Cele două lungimi diferă cu unu, exact octetul suplimentar al lui
șîn UTF-8. Textul are 10 puncte de cod, transportul are 11 octeți. raw[0]este73, unint— indexarea unuibytesdă numărul octetului.label[0]este'I', unstrde lungime 1.TypeError-ul final este garanția centrală a modelului: nu există conversie implicită între text și octeți, deci un bug de codificare apare ca eroare de tip, nu ca text corupt.
Reprezentarea b'Ia\xc8\x99i depot' arată octeții ASCII ca litere și pe ceilalți ca escape-uri. Este o comoditate de afișare, nu o proprietate a valorii — toți sunt octeți.
Codificarea trebuie să se potrivească textului
Următorul nivel adaugă mai multe codificări și ambele feluri de eșec. Înlocuiește fișierul cu:
label = "Iași depot"
for encoding in ("utf-8", "utf-16-le", "latin-1"):
try:
encoded = label.encode(encoding)
print(f"{encoding:10} -> {len(encoded):2} bytes {encoded[:8]!r}...")
except UnicodeEncodeError as exc:
print(f"{encoding:10} -> UnicodeEncodeError: {exc.reason} at position {exc.start}")
utf8 = label.encode("utf-8")
try:
utf8.decode("ascii")
except UnicodeDecodeError as exc:
print("decoding utf-8 as ascii:", exc.reason, "at position", exc.start)
print("decoded as latin-1 silently:", repr(utf8.decode("latin-1")))
utf-8 -> 11 bytes b'Ia\xc8\x99i de'...
utf-16-le -> 20 bytes b'I\x00a\x00\x19\x02i\x00'...
latin-1 -> UnicodeEncodeError: ordinal not in range(256) at position 2
decoding utf-8 as ascii: ordinal not in range(128) at position 2
decoded as latin-1 silently: 'IaÈ\x99i depot'
utf-16-leproduce aproape dublu, pentru că folosește cel puțin doi octeți per punct de cod chiar și pentru ASCII. Aceeași informație, alt transport.latin-1refuzășla codificare: punctul de cod 537 nu încape în intervalul acoperit.- Decodificarea ca ASCII eșuează la aceeași poziție, din partea cealaltă.
- Ultima linie este cea care produce bug-uri reale.
latin-1decodifică orice octet, deci cei doi octeți ai luișdevin două caractere separate:Èși un caracter de control invizibil, vizibil aici doar pentru căreprîl scrie ca\x99. Nicio excepție, doar text greșit — clasiculédin bazele de date prost configurate.
Când o codificare nu poate eșua, absența erorii nu este o confirmare că a fost aleasă corect.
Ce faci când datele sunt deja stricate
Al treilea nivel adaugă parametrul errors și distincția între pierdere și recuperare. Înlocuiește fișierul cu:
utf8 = "Iași depot".encode("utf-8")
print("replace :", utf8.decode("ascii", errors="replace"))
print("ignore :", utf8.decode("ascii", errors="ignore"))
print("backslash:", utf8.decode("ascii", errors="backslashreplace"))
print("replace round trips:", utf8.decode("ascii", errors="replace").encode("utf-8") == utf8)
broken = b"scan\xffLEG-8842"
try:
broken.decode("utf-8")
except UnicodeDecodeError as exc:
print("strict :", exc.reason, "at position", exc.start)
escaped = broken.decode("utf-8", errors="surrogateescape")
print("surrogateescape text:", escaped.encode("unicode_escape").decode("ascii"))
print("surrogateescape round trips:", escaped.encode("utf-8", errors="surrogateescape") == broken)
replace : Ia��i depot
ignore : Iai depot
backslash: Ia\xc8\x99i depot
replace round trips: False
strict : invalid start byte at position 4
surrogateescape text: scan\udcffLEG-8842
surrogateescape round trips: True
replacepune caracterul de înlocuire pentru fiecare octet nedecodabil,ignoreîi șterge,backslashreplaceîi scrie ca text vizibil. Toate trei produc unstrvalid și niciuna nu poate reface originalul — linia de round-trip confirmă.\xffnu este un octet de start valid în UTF-8, deci decodificarea strictă eșuează. Poziția 4 este exact octetul problematic.surrogateescapemapează fiecare octet nedecodabil la un punct de cod din zonaU+DC80–U+DCFF. Textul rezultat conține\udcff, iar recodificarea cu același handler reface octetul original — de aceea round-trip-ul reușește.
surrogateescape este handlerul folosit de Python pentru numele de fișiere din sistemul de operare, tocmai pentru că un nume de fișier trebuie să poată fi redat exact, chiar dacă nu este text valid. Pentru date care doar se afișează, replace este suficient; pentru date care se scriu înapoi, este singura alegere corectă.
Unde se face conversia în cod real
Ultimul nivel arată granițele tipice — fișier, JSON, buffer în memorie. Înlocuiește fișierul cu:
import io
import json
import pathlib
payload = {"depot": "Iași", "scans": 3}
as_text = json.dumps(payload, ensure_ascii=False)
as_bytes = as_text.encode("utf-8")
print("json text :", as_text)
print("json bytes :", as_bytes)
print("ensure_ascii=True:", json.dumps(payload))
path = pathlib.Path("depot.json")
path.write_text(as_text, encoding="utf-8")
print("read_text :", path.read_text(encoding="utf-8"))
print("read_bytes :", path.read_bytes())
buffer = io.BytesIO()
buffer.write(as_bytes)
print("BytesIO holds bytes:", buffer.getvalue() == as_bytes)
print("json.loads accepts both:", json.loads(as_text) == json.loads(as_bytes))
path.unlink()
json text : {"depot": "Iași", "scans": 3}
json bytes : b'{"depot": "Ia\xc8\x99i", "scans": 3}'
ensure_ascii=True: {"depot": "Ia\u0219i", "scans": 3}
read_text : {"depot": "Iași", "scans": 3}
read_bytes : b'{"depot": "Ia\xc8\x99i", "scans": 3}'
BytesIO holds bytes: True
json.loads accepts both: True
json.dumpsreturneazăstr, nubytes— serializarea JSON este o transformare text-la-text; codificarea este un pas separat.ensure_ascii=True, valoarea implicită, evită complet problema scriindșîn ASCII pur. Rezultatul este mai lung, dar trece prin orice canal care presupune ASCII.write_textcuencodingexplicit face codificarea în locul tău;read_bytesarată ce a ajuns efectiv pe disc.json.loadsacceptă șistr, șibytes, detectând codificarea din primii octeți — una dintre puținele funcții din biblioteca standard care fac asta.
Tiparul vizibil aici este cel corect în general: conversia are loc la granița cu fișierul sau cu rețeaua, iar payload rămâne un dicționar cu str pe toată durata prelucrării.
Cazuri limită și capcane
- Codificarea implicită la deschiderea unui fișier depinde de sistem.
open(path)fărăencodingfolosește codificarea locală, care diferă între mașini. Scrie mereuencoding="utf-8"explicit. b[i]este unint, darb[i:i+1]este unbytes. Codul care iterează unbytesși compară cub"x"nu se potrivește niciodată; compară cuord("x")sau iterează pe felii.bytearrayeste varianta mutabilă a luibytesși este alegerea corectă când construiești un tampon incremental; concatenarea repetată debytesrealocă la fiecare pas.str(b)nu decodifică. Produce"b'...'", reprezentarea textuală a obiectului. Python avertizează doar dacă ceri explicit, deci greșeala ajunge tăcut în loguri.latin-1este util deliberat ca transport reversibil octet-cu-octet, dar niciodată ca presupunere despre text real.- Comparațiile între
strșibytesnu ridică excepție, returneazăFalse."LEG-8842" == b"LEG-8842"este fals, iar o cheie de dicționar de tipul greșit pur și simplu nu se găsește.
Rezumat
str conține puncte de cod Unicode și reprezintă text; bytes conține octeți și reprezintă transport. Conversia trece obligatoriu printr-o codificare declarată explicit, pentru că valoarea bytes nu o poartă cu sine. Cele două tipuri nu se convertesc implicit niciodată, ceea ce transformă erorile de codificare în erori de tip vizibile devreme.
lennumără puncte de cod pestrși octeți pebytes;b[i]returnează unint,s[i]unstr.- Codificarea nu este stocată în date — este o convenție de protocol, iar
decodefără codificare corectă produce text greșit sau o excepție. latin-1decodifică orice secvență de octeți fără să eșueze, deci absența unei erori nu confirmă alegerea corectă.errors="replace"și"ignore"pierd informația definitiv;"surrogateescape"o păstrează și permite recodificarea exactă.- Decodifică la intrarea în program și codifică la ieșire; între ele lucrează exclusiv cu
str. - Specifică
encoding="utf-8"explicit la orice operațiune cu fișiere, pentru că valoarea implicită depinde de sistem.