2.6. טקסט מול בייטים¶
ל-Python יש שני סוגי רצף עבור נתוני תווים גולמיים:
str– רצף של נקודות קוד Unicode. משמש לכל טקסט קריא לבני אדם: נתיבי קבצים, הודעות לוג, מטעני JSON.bytes– רצף של מספרים שלמים בטווח 0 – 255. משמש לנתונים בינאריים גולמיים: פריימים של UART, חוצצי תמונה (image buffers), חבילות רשת, ערכי אוגרים.
לא ניתן לערבב ביניהם ללא המרה מפורשת. העברת str למתודת חומרה write מעלה TypeError, וגם ההפך נדחה.
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" ולהיות מופתעים שהתוצאה היא False – data[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 קטנים, חיתוך פשוט מספיק.