PEP 686: Bật mặc định chế độ UTF-8 trong Python 3.15
(peps.python.org)- 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=0hoặc-X utf8=0; để kiểm tra tương thích có thể dùngEncodingWarning,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 sinhEncodingWarningcủa PEP 597 giống nhưopen() - PEP 597 đã thêm tùy chọn
encoding="locale"choTextIOWrapper, cho phép chỉ định rõ mã hóa locale - Trước đây, trong chế độ UTF-8, dù chỉ định
encoding="locale",TextIOWrappervẫ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
- Tắt chế độ UTF-8
- Dùng
EncodingWarningcủa PEP 597 để tìm các vị trí bị chế độ UTF-8 ảnh hưởng- Nếu tùy chọn
encodingbị bỏ qua, cân nhắc dùngencoding="utf-8"hoặcencoding="locale" - Nếu dùng
locale.getpreferredencoding(), cân nhắc dùng"utf-8"hoặclocale.getencoding()
- Nếu tùy chọn
- 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_encodingmặ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ư
EncodingWarningcủ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ềuDeprecationWarningcó 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
PYTHONIOENCODINGlàm mã hóa mặc định cho pipe của mô-đunsubprocesscũ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=Truethànhencoding="utf-8"hoặcencoding="locale"
- Cách này cho phép
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
Ý 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
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ư
TextOutAdù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 98Nó 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-1từng rất phổ biến, và sau khi đồng euro ra đời, người ta thường chuyển sangiso-8859-15có 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 EtchTrê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ý
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ộ raNgà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
.gitignoređược tạo bởi hàmtouchđặ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
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àmCó 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
charsetmà không phải UTF-8 thì nó saiPython 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-8thì đó là Python 3, và nếu chỉ chạy được trong localeC.UTF-8thì đó là Python 3. Tôi hoan nghênh thay đổi này vì hiểu nó như việc “sửa chữa” Python 3Tôi cứ tưởng từ Python 3 nó đã là mặc định rồi
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ùngopen('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')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ủatokenize.open,sys.getfilesystemencoding()củaos.fsencode, vàlocale.getpreferredencoding()củaopenCâ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
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ớpStringban đầ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ựJava chưa từng dùng UTF-16 làm giá trị mặc định cho mã hóa đọc/ghi
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
strlà đối tượngPyUnicodeKhi các byte UTF-8 được yêu cầu, một đối tượng
bytessẽ được tạo khi cần, được cache như một phần củaPyUnicode, và được giải phóng cùng khiPyUnicodebị 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ạoPyUnicode, 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 byteNế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
etheo 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 PythonTô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 đó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ý
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
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ãiThà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
bytes.decodevàstr.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ùngsys.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,opendùng trực tiếplocale.getencoding()