3 điểm bởi GN⁺ 2024-04-28 | 1 bình luận | Chia sẻ qua WhatsApp
  • Python 3.15 sẽ bật chế độ UTF-8 theo mặc định, thống nhất mã hóa mặc định của tệp, đầu vào/đầu ra chuẩn và pipe thành UTF-8
  • UTF-8 đang được dùng như mã hóa tiêu chuẩn trên thực tế trong tệp nguồn, JSON·TOML·YAML, các trình soạn thảo phổ biến, dữ liệu web, Node.js·Go·Rust·Java, v.v., giúp tăng khả năng tương tác
  • Mã hóa mặc định hiện tại khác nhau tùy nền tảng; nếu nhà phát triển Unix bỏ qua encoding="utf-8", có thể phát sinh lỗi không nhất quán trên Windows và các môi trường khác
  • Nếu cần, có thể tắt bằng PYTHONUTF8=0 hoặc -X utf8=0; để kiểm tra tương thích có thể dùng EncodingWarning, encoding="utf-8", encoding="locale", locale.getencoding()
  • Các chương trình phụ thuộc vào mã hóa mặc định, chủ yếu trên Windows, có thể gặp UnicodeError, mojibake hoặc hỏng dữ liệu âm thầm, nên cần kiểm tra trước

Mã hóa mặc định thay đổi trong Python 3.15

  • PEP 686 là thay đổi bật chế độ UTF-8 của PEP 540 làm mặc định
  • Với tệp, stdio và pipe cần mã hóa mặc định, Python sẽ nhất quán dùng UTF-8
  • Từ Python 3.15, chế độ này được bật mặc định; người dùng có thể tắt bằng các cách sau
    • PYTHONUTF8=0
    • -X utf8=0

Vì sao chọn UTF-8 làm mặc định

  • UTF-8 đã trở thành mã hóa văn bản tiêu chuẩn trong nhiều môi trường
    • Mã hóa mặc định của tệp nguồn Python là UTF-8
    • JSON, TOML, YAML dùng UTF-8
    • Hầu hết trình soạn thảo văn bản, gồm Visual Studio Code và Windows Notepad, dùng UTF-8 làm mặc định
    • Phần lớn website và dữ liệu văn bản trên Internet dùng UTF-8
    • Nhiều ngôn ngữ lập trình phổ biến, gồm Node.js, Go, Rust, Java, dùng UTF-8 làm mặc định
  • Khi mã hóa mặc định của Python chuyển sang UTF-8, khả năng tương tác với các công cụ, ngôn ngữ và định dạng dữ liệu khác sẽ tốt hơn
  • Nhiều nhà phát triển Python trên Unix quên rằng mã hóa mặc định phụ thuộc nền tảng, và bỏ qua encoding="utf-8" khi đọc văn bản UTF-8 như JSON·TOML·Markdown·tệp nguồn Python
  • Khác biệt về mã hóa mặc định giữa các nền tảng trở thành nguyên nhân gây lỗi khiến mã như vậy bị hỏng ở môi trường khác

Sửa locale API và encoding="locale"

  • Chế độ UTF-8 ảnh hưởng đến locale.getpreferredencoding(False), nên cần API để lấy mã hóa locale độc lập với chế độ UTF-8
  • locale.getencoding() được thêm vào cho mục đích này; nó trả về mã hóa locale nhưng bỏ qua chế độ UTF-8
    • API này được thêm trong Python 3.11
  • Khi tùy chọn warn_default_encoding được chỉ định, locale.getpreferredencoding() sẽ phát sinh EncodingWarning của PEP 597 giống như open()
  • PEP 597 đã thêm tùy chọn encoding="locale" cho TextIOWrapper, cho phép chỉ định rõ mã hóa locale
  • Trước đây, trong chế độ UTF-8, dù chỉ định encoding="locale", TextIOWrapper vẫn dùng "UTF-8"
    • Điều này không phù hợp với động cơ của PEP 597
    • Vì khi mã hóa văn bản mặc định của Python thay đổi, người ta đã không lường trước tình huống chế độ UTF-8 trở thành mặc định
  • Sự không nhất quán này đã được sửa trong Python 3.11; ngay cả trong chế độ UTF-8, nếu truyền encoding="locale" thì sẽ dùng mã hóa locale

Tương thích ngược và quy trình di trú

  • Hầu hết hệ thống Unix dùng locale UTF-8, và Python bật chế độ UTF-8 khi locale là C hoặc POSIX, nên tác động của thay đổi chủ yếu tập trung vào người dùng Windows
  • Các chương trình Python phụ thuộc vào mã hóa mặc định có thể gặp các vấn đề sau
    • UnicodeError
    • mojibake
    • hỏng dữ liệu âm thầm
  • Quy trình được khuyến nghị để sửa vấn đề tương thích ngược như sau
    1. Tắt chế độ UTF-8
    2. Dùng EncodingWarning của PEP 597 để tìm các vị trí bị chế độ UTF-8 ảnh hưởng
      • Nếu tùy chọn encoding bị bỏ qua, cân nhắc dùng encoding="utf-8" hoặc encoding="locale"
      • Nếu dùng locale.getpreferredencoding(), cân nhắc dùng "utf-8" hoặc locale.getencoding()
    3. Kiểm thử ứng dụng trong chế độ UTF-8

Tiền lệ Ruby·Java và các phương án bị từ chối

  • Ruby đã thay đổi external_encoding mặc định trên Windows sang UTF-8 trong Ruby 3.0, năm 2020
  • Java đã thay đổi mã hóa văn bản mặc định sang UTF-8 trong JDK 18, năm 2022
  • Cả Ruby và Java đều cung cấp tùy chọn cho tương thích ngược, nhưng không cung cấp cảnh báo khi dùng mã hóa mặc định như EncodingWarning của Python
  • Phương án loại bỏ hẳn việc dùng mã hóa mặc định đã bị từ chối
    • Có nhiều trường hợp dùng mã hóa mặc định chỉ để đọc và ghi văn bản ASCII
    • Với các ứng dụng không đa nền tảng chỉ chạy trên Unix, cảnh báo như vậy không hữu ích
    • Nếu bắt buộc encoding ở mọi nơi, gánh nặng cho người dùng sẽ lớn; nhiều DeprecationWarning có thể khiến người dùng phớt lờ cảnh báo
    • PEP 387 yêu cầu thêm cảnh báo cho thay đổi phá vỡ tương thích ngược, nhưng không nhất thiết yêu cầu DeprecationWarning
  • Phương án dùng PYTHONIOENCODING làm mã hóa mặc định cho pipe của mô-đun subprocess cũng bị từ chối
    • Cách này cho phép subprocess.Popen(text=True) dùng mã hóa legacy ngay cả trong chế độ UTF-8
    • Tuy nhiên, nó làm “mã hóa mặc định” trở nên phức tạp và bản thân cách này cũng là thay đổi phá vỡ tương thích ngược
    • Người dùng có thể tắt chế độ UTF-8 cho đến khi đổi text=True thành encoding="utf-8" hoặc encoding="locale"

Góc nhìn giáo dục người dùng

  • Người dùng mới sẽ ít cần học về mã hóa văn bản hơn trong năm đầu tiên
  • Họ chỉ cần học về mã hóa khi phải xử lý tệp văn bản không phải UTF-8
  • Người dùng hiện tại cần kiểm tra các điểm bị ảnh hưởng theo quy trình tương thích ngược

1 bình luận

 
GN⁺ 2024-04-28
Ý kiến trên Hacker News
  • Việc mã hóa mặc định của tệp văn bản thay đổi tùy theo nền tảng lúc nào cũng gây khó chịu, nên thay đổi lần này rất đáng hoan nghênh
    Cũng tốt là họ không cố đụng tới cả mã hóa của hệ thống tệp. Đó là một vấn đề riêng, và bản thân nó cũng rắc rối

    • Code page mặc định của hệ thống trên Windows không chỉ phụ thuộc vào nền tảng mà còn phụ thuộc vào locale hệ thống
      Việc Windows trong một thời gian dài không cung cấp cách đơn giản để chọn cho các hàm ANSI như TextOutA dùng code page UTF-8 là một sai lầm lớn. Đến khoảng giữa quá trình phát triển Windows 10 mới có thể làm được bằng tệp manifest, nhưng tính năng kiểu này lẽ ra phải có từ thời NT4 hoặc Windows 98
    • Xét về lịch sử thì điều đó có lý. Vì phần lớn phần mềm chỉ dùng cục bộ, và tệp văn bản cũng được kỳ vọng là ở mã hóa cục bộ
      Nó không chỉ phụ thuộc vào nền tảng mà còn vào locale ưa thích của người dùng, và thư viện chuẩn C cũng hoạt động theo cách tương tự. Chẳng hạn trên Unix/Linux, với các ngôn ngữ Tây Âu thì iso-8859-1 từng rất phổ biến, và sau khi đồng euro ra đời, người ta thường chuyển sang iso-8859-15 có ký hiệu . UTF-8 bắt đầu hoạt động ổn thỏa vào khoảng cuối thập niên 2000, và Debian đã đổi mặc định sang UTF-8 trong bản phát hành Etch
    • Vài ngày trước tôi cũng bị dính chuyện ngầm thay đổi ký tự xuống dòng
      Trên laptop công ty, tất cả kiểm thử cục bộ đều ổn, nhưng khi triển khai lên host Linux thì ứng dụng con yêu cầu CRLF nên không đọc được. Đây là một trong những vấn đề nhỏ nhặt và ngớ ngẩn đôi khi phải nhớ. Tuy nhiên, việc vì sao phần mềm mới viết lại yêu cầu một kiểu ký tự kết thúc dòng cụ thể cũng là một câu hỏi hợp lý
    • Khi ai đó bắt đầu viết code trên Windows, họ đã nhiều lần gặp phải vấn đề này
  • Không dựa vào các giá trị mặc định hệ thống không ổn định là điều tốt
    Những giá trị như vậy thường đến một lúc nào đó trả về khác với giả định của mình. Vài năm trước, khi làm với Ubuntu và script init.d, một script khởi chạy Java đang chạy dưới quyền root, lại là thời trước Docker nên càng như vậy, và nó chạy trong một shell không thiết lập mặc định UTF-8 bình thường cho người dùng thông thường. Kết quả là việc dùng API tệ của Java, vốn lấy mặc định từ OS, bị lộ ra
    Ngày nay hầu hết đều có biến thể API cho phép chỉ định mã hóa, và các công cụ kiểm tra mã tĩnh cũng cảnh báo nếu dùng sai. Nhưng chỉ cần sót một chỗ là nội dung bắt đầu hỏng. Giờ đây, việc dùng mã hóa không phải UTF-8 rất có khả năng là ngoài ý muốn trong đa số trường hợp; nếu là có chủ đích thì nên chỉ định rõ, thay vì dựa vào một thiết lập gián tiếp kỳ quặc của OS. Vì vậy đây là thay đổi tốt, và những đoạn code bị hỏng vì chuyện này tốt hơn là nên được sửa bằng một bản vá đơn giản

    • Tôi từng dùng một tệp .gitignore được tạo bởi hàm touch đặt alias trong PowerShell, nhưng dù làm thế nào Git cũng không tôn trọng nó
      Kiểm tra thì thấy tệp văn bản được tạo là UTF-16, nên về cơ bản nó bị bỏ qua. Tôi rút ra bài học và đổi mặc định hệ thống sang UTF-8, nhưng giờ thì chỉ dựa vào trình soạn thảo văn bản
    • Locale toàn cục không chỉ là sai lầm ở phần mã hóa, mà nói chung là một sai lầm
      Nếu printf("%f", 4.2) tự động in ra chuỗi khác nhau tùy theo môi trường thì nó tạo ra nhiều vấn đề hơn là giải quyết. Khi muốn hành vi phụ thuộc locale, nên truyền rõ thông tin locale hoặc phần liên quan vào hàm
  • Có một heuristic ngày càng đúng hơn trong vài thập kỷ qua: nếu ở đâu đó có thiết lập charsetkhông phải UTF-8 thì nó sai
    Python 2 không phụ thuộc vào bộ ký tự nên lúc nào cũng chạy được, nhưng cải tiến của Python 3 không chỉ đơn giản là cải tiến. Cách phân biệt script Python 3 với script Python 2 là thế này: nếu có chuỗi utf-8 thì đó là Python 3, và nếu chỉ chạy được trong locale C.UTF-8 thì đó là Python 3. Tôi hoan nghênh thay đổi này vì hiểu nó như việc “sửa chữa” Python 3

  • Tôi cứ tưởng từ Python 3 nó đã là mặc định rồi

    • Có lẽ bạn đang nghĩ tới chuỗi trong Python 3 không còn cần tiền tố u""
      Tôi vừa thử nhập "éķů" trong Python 2.7 thì nó in ra các byte UTF-8 của những ký tự đó, nên tôi cũng không rõ chính xác tiền tố u đã làm gì, nhưng một trong những thay đổi lớn khi chuyển từ Python 2 sang 3 là chuỗi có mã hóa, còn chuỗi byte trở thành chuỗi byte không có mã hóa. Thay đổi lần này có vẻ chủ yếu liên quan đến vấn đề khi dùng open('filename', mode='r') trong môi trường có mã hóa mặc định không phải UTF-8 như Windows thì trước đây phải chỉ định rõ open('filename', mode='r', encoding='UTF-8')
    • Trong Python 3, mã nguồn Python mặc định là UTF-8. Nhưng điều đó không nói gì về mã hóa ký tự dùng khi lưu vào tệp, và giá trị mặc định thì phụ thuộc vào locale
      Như Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding"), literal chuỗi, tên tệp và mã hóa nội dung tệp đều khác nhau. Các mã hóa tương ứng là UTF-8 của tokenize.open, sys.getfilesystemencoding() của os.fsencode, và locale.getpreferredencoding() của open
  • Câu “các ngôn ngữ lập trình phổ biến khác, bao gồm Node.js, Go, Rust, Java, cũng dùng UTF-8 làm mặc định” làm tôi bỏ lỡ chuyện Java đã chuyển từ UTF-16 sang UTF-8

    • Mã hóa mặc định khi chuyển byte thành chuỗi trong Java vốn phụ thuộc vào nền tảng, và hiện nay là UTF-8
      Bên trong lớp String, mã hóa UTF-16 và latin-1 vẫn được dùng, còn JVM vẫn dùng mã hóa modified UTF-8 như trước. Lớp String ban đầu chỉ dùng UTF-16, nhưng từ Java 9, khi có thể thì cũng dùng mã hóa latin-1 1 byte mỗi ký tự
    • Có vẻ đang lẫn lộn giữa biểu diễn chuỗi nội bộ và mã hóa đọc/ghi
      Java chưa từng dùng UTF-16 làm giá trị mặc định cho mã hóa đọc/ghi
    • Có vẻ nó đã thay đổi 2 năm trước trong Java 18
  • Mã hóa nội bộ của CPython giờ là UTF-8 à?
    Chuỗi Python có thể được lập chỉ mục bằng chỉ số dưới, nhưng truy cập ngẫu nhiên đủ hiếm nên có lẽ khi cần thì lập chỉ mục lười cũng ổn. Nếu chỉ tiến hoặc lùi một ô thì không cần chỉ mục, nên việc dùng UTF-8 làm biểu diễn nội bộ cũng hoàn toàn khả thi

    • Thứ biểu diễn str là đối tượng PyUnicode
      Khi các byte UTF-8 được yêu cầu, một đối tượng bytes sẽ được tạo khi cần, được cache như một phần của PyUnicode, và được giải phóng cùng khi PyUnicode bị giải phóng. Riêng các code point cấu thành chuỗi được lưu trong một mảng đơn giản để có thể truy cập ngẫu nhiên. Kích thước của mỗi code point có thể là 1, 2 hoặc 4 byte; khi tạo PyUnicode, nếu chỉ định giá trị code point lớn nhất thì nó sẽ được làm tròn lên thành một trong các giá trị 127, 255, 65535, 1.114.111 để quyết định dùng 1/2/4 byte
      Nếu giá trị code point lớn nhất là 127 thì biểu diễn dạng mảng đó có thể được dùng trực tiếp như UTF-8. Vì vậy câu trả lời cho câu hỏi là: nhiều chuỗi mà tất cả code point đều từ 127 trở xuống được lưu dưới dạng UTF-8. Tuy nhiên, khi duyệt chuỗi thì không nên duyệt theo đơn vị code point. Ký tự mà người dùng nhận biết, tức cụm grapheme, gồm một hoặc nhiều code point. Ví dụ chữ e có dấu có thể là code point e theo sau là code point dấu kết hợp, còn emoji phượng hoàng được cấu thành từ emoji chim, ký tự nối không độ rộng và emoji lửa. Một số hệ chữ được hàng trăm triệu người dùng cũng tương tự, với các ký hiệu kết hợp biểu thị nguyên âm gắn vào phụ âm. Chuỗi - - này có 5 code point, và có một bài viết hay bàn về cách nhiều ngôn ngữ báo cáo “độ dài” của nó: https://hsivonen.fi/string-length/. Nội dung này đến từ trải nghiệm vừa mới triển khai Unicode TR29, phần xử lý vấn đề này, dưới dạng extension C cho Python
  • Tôi thắc mắc vì sao không phải utf-8-sig. Nó xử lý BOM tùy chọn, và tuần trước tôi còn phải sửa script vì chuyện đó

    • Giờ không thứ gì nên đưa BOM vào UTF-8 nữa
      Việc đó không còn được khuyến nghị, và ngày nay hành vi lỗi khi gặp BOM cũng có thể xem là hợp lý
    • Thay đổi để Python âm thầm gắn một BOM vô hình trước mọi đầu vào/đầu ra không phải là ý hay
  • Nếu nói về UTF-8, framebuffer của Linux đáng lẽ đã phải có hỗ trợ UTF-8 đúng nghĩa từ lâu rồi
    Ý tôi là hỗ trợ thật sự, không phải kiểu 256/512 glyph. Ngay cả GNU Hurd khoảng từ năm 2007 đã có console terminal tốt hơn hỗ trợ UTF-8, vậy mà giờ đã là năm 2024

  • Tốt. Giờ chỉ còn JS chuyển sang UTF-8 nữa là xong
    Dĩ nhiên JS không thể được cải thiện. Vì khác với bất kỳ ngôn ngữ lập trình nào khác, nó phải tương thích với mã được viết từ năm 1995

    • Chuyện này nói về việc Python sẽ dùng encoding nào làm mặc định khi bạn yêu cầu mở file “dưới dạng văn bản”
      Biểu diễn nội bộ của chuỗi là vấn đề riêng; giống như JavaScript, Python cũng không dùng “chỉ UTF-8” ở bên trong
  • Trong câu “nhiều lập trình viên Python dùng Unix quên mất rằng encoding mặc định phụ thuộc vào nền tảng, và bỏ qua encoding="utf-8" khi đọc các file văn bản được mã hóa UTF-8”, chuyện này có thể không hẳn là quên mà là chưa được biết đến đủ rộng rãi
    Thành thật mà nói, tôi cứ nghĩ Python chỉ dùng UTF-8 ở mọi nơi, trừ khi yêu cầu rõ ràng cách khác

    • Thực tế thì còn tùy trường hợp
      bytes.decodestr.encode ít nhất từ Python 3 đã dùng UTF-8 làm mặc định. Trong khi đó, encoding mặc định khi decode tên file dùng sys.getfilesystemencoding(), và trên Windows cũng như macOS thì cái này cũng là UTF-8, nhưng trên Linux thì phụ thuộc vào locale, cụ thể là CODESET. Cuối cùng, open dùng trực tiếp locale.getencoding()