2.6. النص مقابل البايتات¶
تمتلك Python نوعَي تسلسل لبيانات المحارف الخام:
str-- تسلسل من نقاط رمز Unicode. يُستخدم لكل النصوص المقروءة من البشر: مسارات الملفات، ورسائل السجلات، وحمولات JSON.
bytes-- تسلسل من الأعداد الصحيحة في المدى 0 -- 255. يُستخدم للبيانات الثنائية الخام: إطارات UART، ومخازن الصور، وحزم الشبكة، وقيم السجلات.
لا يمكن خلطهما من دون تحويل صريح. تمرير 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 إلى أحدهما الآخر عبر مُنشئَيهما. استخدم هذا عندما تتطلب واجهة برمجية صيغة محددة منهما:
>>> 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 للتقطيع دون نسخ¶
تقطيع 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'
العرض على bytearray قابل للكتابة أيضاً -- فتغيير العرض يُغيّر المخزن المؤقت الأساسي:
>>> view[0] = ord("o")
>>> buf
bytearray(b'openMV Cam')
لجأ إلى memoryview عندما يكون نسخ مقطع تبديداً للموارد -- عادةً عندما يُمرَّر المخزن المؤقت الكبير نفسه أو يُعالَج قطعةً قطعة. أما للأعمال اليومية على نمط السلاسل النصية على بايتات صغيرة، فالتقطيع العادي مناسب.