2.6. Văn bản và bytes

Python có hai kiểu dãy cho dữ liệu ký tự thô:

  • str -- dãy các điểm mã Unicode. Dùng cho tất cả văn bản đọc được: đường dẫn tệp, thông điệp nhật ký, dữ liệu JSON.

  • bytes -- dãy các số nguyên trong phạm vi 0 -- 255. Dùng cho dữ liệu nhị phân thô: khung UART, bộ đệm khung hình ảnh, gói mạng, giá trị thanh ghi.

Chúng không thể được kết hợp mà không có chuyển đổi rõ ràng. Truyền str cho phương thức write phần cứng sẽ gây ra TypeError, và ngược lại cũng bị từ chối.

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 lưu các ký tự Unicode; bytes lưu các octet thô. Chuyển đổi giữa chúng là mã hóa (str → bytes) và giải mã (bytes → str).

2.6.1. Literal bytes

Literal bytes là một literal giống chuỗi có tiền tố b:

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

Chỉ các ký tự ASCII được phép viết trực tiếp trong literal bytes; các giá trị không phải ASCII phải được viết dưới dạng thoát hex \xHH.

2.6.2. Mã hóa và giải mã

  • str.encode() chuyển đổi chuỗi thành bytes bằng một mã hóa được đặt tên (mặc định là "utf-8").

  • bytes.decode() thực hiện điều ngược lại.

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

UTF-8 là mặc định và là lựa chọn đúng cho bất cứ thứ gì có thể chứa ký tự không phải ASCII. Chỉ dùng "ascii" khi dữ liệu được đảm bảo là ASCII thuần túy; theo cách đó, một byte không phải ASCII lọt vào sẽ gây ra UnicodeError thay vì lặng lẽ đi qua.

2.6.3. Chỉ mục và cắt lát

Một giá trị bytes hoạt động như một dãy số nguyên khi dùng chỉ mục, không phải dãy chuỗi một byte:

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

Một lỗi phổ biến là so sánh data[0] == "a" và ngạc nhiên vì kết quả là False -- data[0] là một số nguyên chứ không phải chuỗi một ký tự, nên hai giá trị không bao giờ khớp được.

2.6.4. ord và chr -- kết nối ký tự và số nguyên

Vì dùng chỉ mục trên bytes trả về số nguyên nhưng phần còn lại của chương trình thường nghĩ theo ký tự, Python cung cấp hai hàm tích hợp để chuyển đổi giữa chúng:

  • ord() -- nhận một chuỗi gồm một ký tự và trả về điểm mã số nguyên của nó.

  • chr() -- hàm nghịch đảo: nhận một số nguyên, trả về chuỗi một ký tự tương ứng với điểm mã đó.

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

Với các ký tự ASCII, điểm mã bằng giá trị byte, vì vậy ord("a")b"a"[0] đều cho giá trị 97. Điều này giúp các phép so sánh byte đọc được theo ký tự bạn thực sự quan tâm:

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

chr() rất tiện khi ghi nhật ký hoặc gỡ lỗi khi bạn muốn xem dạng in được của một byte:

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

Với các ký tự không phải ASCII, ord() trả về điểm mã Unicode, khác với bất kỳ byte đơn nào trong dạng mã hóa; biểu diễn byte phụ thuộc vào bảng mã.

2.6.5. bytearray cho bộ đệm có thể thay đổi

bytes là bất biến -- mỗi "sửa đổi" đều trả về một đối tượng mới và giữ nguyên bản gốc. Với dữ liệu bạn muốn sửa đổi, nối thêm hoặc điền dần từng phần, hãy dùng bytearray. Nó giữ nội dung giống bytes nhưng hỗ trợ thay đổi tại chỗ:

>>> 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. Tạo một bytearray

Hàm tạo bytearray chấp nhận nhiều đầu vào:

  • bytearray(8) -- một bộ đệm gồm 8 byte zero.

  • bytearray(b"hello") -- bản sao có thể thay đổi của một giá trị bytes.

  • bytearray("hello", "utf-8") -- một bytearray từ chuỗi, dùng mã hóa đã cho.

  • bytearray([72, 73, 74]) -- một bytearray từ dãy các số nguyên trong 0 -- 255 (ở đây là 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. Sửa đổi một bytearray

Gán theo chỉ mục và theo lát cắt hoạt động giống như 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')

Các byte riêng lẻ phải là số nguyên trong 0 -- 255; gán bất kỳ kiểu nào khác sẽ gây ra TypeError hoặc ValueError.

Gán theo lát cắt có thể thay đổi độ dài bộ đệm. Thay thế một lát cắt bằng giá trị dài hơn sẽ làm bytearray tăng kích thước; thay bằng giá trị ngắn hơn sẽ giảm kích thước. Thay bằng b"" sẽ xóa hoàn toàn lát cắt đó:

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

Các phương thức bytearray.append()bytearray.extend() thêm byte vào cuối mà không cấp phát lại toàn bộ bộ đệm mỗi lần:

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

2.6.5.3. Đọc từ một bytearray

Chỉ mục, cắt lát, duyệt và các phương thức kiểm tra bytes (bytes.startswith(), bytes.find(), bytes.strip(), v.v.) đều hoạt động giống như trên giá trị bytes. Chỉ mục trả về số nguyên; cắt lát trả về một bytearray khác:

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

2.6.5.4. Chuyển đổi giữa bytes và bytearray

bytesbytearray chuyển đổi lẫn nhau bằng hàm tạo của chúng. Dùng cách này khi một API yêu cầu cụ thể một dạng:

>>> 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 để cắt lát không sao chép

Cắt lát một bytes hoặc bytearray thông thường sẽ sao chép các byte vào một bộ đệm mới. memoryview hiển thị cùng các byte đó mà không sao chép:

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

Một view trên bytearray cũng có thể ghi -- thay đổi view sẽ thay đổi bộ đệm bên dưới:

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

Dùng memoryview khi sao chép một lát cắt là lãng phí -- thường là khi cùng một bộ đệm lớn được truyền đi hoặc xử lý từng phần. Với các thao tác kiểu chuỗi thông thường trên bytes nhỏ, cắt lát thông thường là đủ.