2.6. טקסט מול בייטים

ל-Python יש שני סוגי רצף עבור נתוני תווים גולמיים:

  • str – רצף של נקודות קוד Unicode. משמש לכל טקסט קריא לבני אדם: נתיבי קבצים, הודעות לוג, מטעני JSON.

  • bytes – רצף של מספרים שלמים בטווח 0 – 255. משמש לנתונים בינאריים גולמיים: פריימים של UART, חוצצי תמונה (image buffers), חבילות רשת, ערכי אוגרים.

לא ניתן לערבב ביניהם ללא המרה מפורשת. העברת str למתודת חומרה write מעלה TypeError, וגם ההפך נדחה.

A str of Unicode codepoints on the left and a bytes sequence of raw octets on the right, with encode and decode arrows between them.

str שומר תווי Unicode; bytes שומר אוקטטות גולמיות. המעבר ביניהם הוא קידוד (str ← bytes) ופענוח (bytes ← str).

2.6.1. ליטרלים של bytes

ליטרל bytes הוא ליטרל דמוי-מחרוזת עם התחילית b:

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

רק תווי ASCII מותרים ישירות בתוך ליטרל bytes; ערכים שאינם ASCII חייבים להיכתב כבריחות הקסדצימליות \xHH.

2.6.2. קידוד ופענוח

  • str.encode() ממירה מחרוזת ל-bytes באמצעות קידוד נקוב (ברירת המחדל "utf-8").

  • bytes.decode() עושה את ההפך.

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

UTF-8 הוא ברירת המחדל והבחירה הנכונה לכל דבר שעשוי להכיל תווים שאינם ASCII. השתמשו ב-"ascii" רק כאשר מובטח שהנתונים הם ASCII פשוט; כך בייט תועה שאינו ASCII מעלה UnicodeError במקום לעבור בשקט.

2.6.3. אינדוקס וחיתוך

ערך bytes מתנהג כרצף של מספרים שלמים בעת אינדוקס, ולא כרצף של מחרוזות בנות בייט אחד:

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

טעות נפוצה היא להשוות data[0] == "a" ולהיות מופתעים שהתוצאה היא Falsedata[0] הוא מספר שלם, ולא מחרוזת בת תו אחד, ולכן שני הערכים לעולם לא יכולים להתאים.

2.6.4. ord ו-chr – גישור בין תווים למספרים שלמים

מכיוון שאינדוקס של bytes מחזיר מספר שלם אך שאר התוכנית כנראה חושבת במונחי תווים, Python מספקת שתי פונקציות מובנות למעבר ביניהם:

  • ord() – מקבלת מחרוזת בת תו אחד ומחזירה את נקודת הקוד השלמה שלה.

  • chr() – ההפך: בהינתן מספר שלם, מחזירה את המחרוזת בת התו האחד עבור אותה נקודת קוד.

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

עבור תווי ASCII נקודת הקוד שווה לערך הבייט, כך ש-ord("a") ו-b"a"[0] שניהם נותנים 97. הדבר מאפשר לקרוא השוואות בייטים במונחי התו שבאמת מעניין אתכם:

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

ו-chr() שימושית ללוג או לניפוי באגים כשאתם רוצים לראות את הצורה הניתנת להדפסה של בייט:

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

עבור תווים שאינם ASCII, ord() מחזירה את נקודת הקוד של Unicode, שאינה זהה לאף בייט בודד בצורה המקודדת; ייצוג הבייטים תלוי בקידוד.

2.6.5. bytearray לחוצצים ניתנים לשינוי

bytes אינו ניתן לשינוי – כל ”שינוי“ מחזיר אובייקט חדש ומשאיר את המקורי ללא שינוי. עבור נתונים שבכוונתכם לשנות, להוסיף אליהם, או למלא חלק אחר חלק, השתמשו ב-bytearray. הוא מחזיק את אותו תוכן כמו bytes אך תומך בשינוי במקום:

>>> 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. יצירת bytearray

הבנאי של bytearray מקבל כמה קלטים:

  • bytearray(8) – חוצץ של 8 בייטים אפס.

  • bytearray(b"hello") – עותק ניתן לשינוי של ערך bytes.

  • bytearray("hello", "utf-8") – bytearray ממחרוזת, באמצעות הקידוד הנתון.

  • bytearray([72, 73, 74]) – bytearray מרצף של מספרים שלמים בטווח 0 – 255 (כאן, 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. שינוי של bytearray

השמה לפי אינדקס ולפי פרוסה עובדת בדיוק כמו 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')

בייטים בודדים חייבים להיות מספרים שלמים בטווח 0 – 255; השמת כל סוג אחר מעלה TypeError או ValueError.

השמה לפרוסה יכולה לשנות את אורך החוצץ. החלפת פרוסה בערך ארוך יותר מגדילה את ה-bytearray; החלפה בערך קצר יותר מכווצת אותו. החלפה ב-b"" מוחקת את הפרוסה כליל:

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

המתודות bytearray.append() ו-bytearray.extend() מוסיפות בייטים בסוף מבלי להקצות מחדש את כל החוצץ בכל פעם:

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

2.6.5.3. קריאה מ-bytearray

אינדוקס, חיתוך, איטרציה, ומתודות הבדיקה של bytes (bytes.startswith(), bytes.find(), bytes.strip() וכו«) כולן עובדות באותו אופן כמו על ערך bytes. אינדוקס מחזיר מספר שלם; חיתוך מחזיר bytearray נוסף:

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

2.6.5.4. המרה בין bytes ל-bytearray

bytes ו-bytearray ממירים זה לזה באמצעות הבנאים שלהם. השתמשו בכך כאשר API דורש צורה אחת ספציפית:

>>> 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 לחיתוך ללא העתקה (zero-copy)

חיתוך של bytes או bytearray בדרך כלל מעתיק את הבייטים לחוצץ חדש. memoryview חושף את אותם בייטים ללא העתקה:

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

מבט (view) על bytearray הוא גם בר-כתיבה – שינוי המבט משנה את החוצץ הבסיסי:

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

פנו אל memoryview כאשר העתקת פרוסה תהיה בזבזנית – בדרך כלל כשאותו חוצץ גדול מועבר הלוך ושוב או מעובד בחלקים. לעבודה יומיומית בסגנון מחרוזות על bytes קטנים, חיתוך פשוט מספיק.