Bộ tiền xử lý của Python
(pydong.org)- Python có thể dùng chú thích ma thuật mã hóa nguồn ở hai dòng đầu và codec tự định nghĩa để thay đổi nội dung tệp trước khi thực thi, hoặc thậm chí thay thế bằng mã hoàn toàn khác
- Codec tự định nghĩa có thể được đăng ký trong lúc khởi tạo trình thông dịch thông qua việc thực thi
importtrong tệp cấu hình đường dẫn.pth, rồi thêm hàm tìm kiếm bằngcodecs.register - Việc triển khai codec cần
decode(data: bytes) -> tuple[str, int]và bộ giải mã tăng dần; nếu không xử lý ngoại lệ, bạn có thể chỉ thấySyntaxError: encoding problem: your_codecthay vì nguyên nhân thực sự - Cùng một điểm vào này có thể dùng để triển khai toán tử tăng/giảm
++/--, Python dùng dấu ngoặc nhọn, thực thi C/C++ quacppyy, hay cả cách xác thực TOML bằng JSON Schema - Ngoài các ví dụ mang tính đùa vui, nó còn có thể dùng cho các phần mở rộng và backport của Python như
pythonql,future-typing,future-fstrings,future-annotations, cònmagic_codecgiúp giảm bớt công việc lặp lại
Dùng mã hóa nguồn như điểm vào tiền xử lý
- Theo PEP-0263, bạn có thể chỉ định mã hóa mã nguồn ở một trong hai dòng đầu của tệp Python
- Ví dụ:
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- Ví dụ:
- Dòng ma thuật phải khớp với regex
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)- Tên codec phải khớp với
[-_.a-zA-Z0-9]+
- Tên codec phải khớp với
- Codec tự định nghĩa không chỉ dừng ở việc giải mã mã nguồn mà còn có thể sửa đổi chuỗi nguồn rồi chuyển cho trình thông dịch Python
Đăng ký codec bằng tệp .pth
- Khi trình thông dịch Python khởi động mà không có tùy chọn
-S, góisitesẽ được nạp trong quá trình khởi tạo - Các tệp cấu hình đường dẫn
.pthtrongsite-packagessẽ thêm nội dung vào đường dẫn tìm kiếm mô-đun, ngoại trừ dòng trống và dòng bắt đầu bằng# - Theo tài liệu Python, những dòng bắt đầu bằng
importtheo sau là khoảng trắng hoặc tab sẽ được thực thi- Ví dụ: nếu thêm
import packagename.register_codecvàopackagename.pth, mô-đun đó sẽ được import trong lúc khởi tạo
- Ví dụ: nếu thêm
- Mô-đun được import có thể gọi
codecs.registerđể đăng ký hàm tìm kiếm codec- Vì
importchỉ chạy một lần nên hàm tìm kiếm cũng chỉ được đăng ký một lần
- Vì
Triển khai codec tự định nghĩa
- Một codec tự định nghĩa cần hai thành phần
decode(data: bytes) -> tuple[str, int]- lớp bộ giải mã tăng dần
- Hàm
decodecó thể dùngcodecs.utf_8_decodeđể thực hiện giải mã UTF-8 thực tế, rồi chuyển chuỗi kết quả cho hàm tiền xử lý - Nếu ngoại lệ bên trong codec không được bắt lại, thay vì traceback thông thường bạn có thể chỉ thấy
SyntaxError: encoding problem: your_codec- Tốt hơn là in trực tiếp ngoại lệ trong hàm tiền xử lý bằng
traceback.print_exc()rồi raise lại
- Tốt hơn là in trực tiếp ngoại lệ trong hàm tiền xử lý bằng
- Bộ giải mã tăng dần có thể gom toàn bộ tệp vào bộ đệm và chỉ tiền xử lý một lần ở lần gọi
decodecuối cùng- Cách triển khai ví dụ là kế thừa
codecs.BufferedIncrementalDecodervà chỉ xử lý trongdecode(self, data, final=False)khifinallàTrue
- Cách triển khai ví dụ là kế thừa
- Kết quả tiền xử lý không nhất thiết phải dùng nội dung tệp gốc, mà có thể trả về mã Python hoàn toàn tùy ý
- Tuy nhiên, dòng đầu tiên sẽ bị loại bỏ vì được xem là dòng ma thuật, và kết quả phải là Python hợp lệ
Ví dụ mở rộng cú pháp Python
-
Toán tử tăng/giảm
++và--- Python không có toán tử tăng giảm một ngôi
x++,x--không hợp lệ về mặt cú pháp++x,--xthì hợp lệ về mặt cú pháp nhưng lần lượt trở thành lời gọix.__pos__().__pos__(),x.__neg__().__neg__()- Bộ tiền xử lý có thể thay đổi luồng token để khiến chúng hoạt động như toán tử tăng/giảm
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- Phép biến đổi này dùng toán tử hải mã (walrus operator), tức biểu thức gán của Python
- Chỉ thay token đơn giản có thể thất bại với biểu thức như
x++ - -y; thêm ngoặc nhưx++ - (-y)có thể giảm mơ hồ - incdec.py thực hiện thay bằng regex nhưng vẫn có thể mong manh ngay cả khi cố tránh thay thế bên trong chuỗi literal
- Một cách triển khai sửa trực tiếp luồng token có trong magic.incdec
-
Python dùng dấu ngoặc nhọn
from __future__ import bracessẽ gây raSyntaxError: not a chance- Bộ tiền xử lý có thể sửa luồng token để biến phạm vi dùng dấu ngoặc nhọn thành Python dựa trên thụt lề
- Luồng triển khai như sau
- Tạo token bằng
tokenize.generate_tokens - Chuỗi đầu vào được cung cấp từng dòng qua
readlinecủaio.StringIO - Loại bỏ các token
INDENT,DEDENTsẵn có - Khi gặp
{, tăng mức thụt lề và xuất: - Khi gặp
}, giảm mức thụt lề - Sau
NL, thêm tokenINDENTtương ứng với mức thụt lề hiện tại
- Tạo token bằng
- Để giảm xung đột với literal dictionary của Python, chỉ nên điều chỉnh mức thụt lề khi sau
{là newline, và chỉ coi}là đóng phạm vi khi trước nó là newline - Dictionary nhiều dòng vẫn có thể dùng nếu thêm dấu gạch chéo ngược vì khi đó sẽ không phát sinh token newline bên trong dấu ngoặc nhọn
Chạy ngôn ngữ khác bằng Python
-
C và C++
- Các ngôn ngữ dùng
#để viết chú thích như shell script, script CMake, PHP, Ruby rất dễ thêm dòng ma thuật mã hóa cùng với shebang - C và C++ dùng chú thích
/* ... */hoặc// ..., nhưng chỉ thị tiền xử lý lại bắt đầu bằng#, nên có thể khớp với regex mã hóa - Dòng ma thuật ví dụ sau vừa hợp lệ trong mã nguồn C/C++ vừa khớp với mẫu mã hóa của Python
#define CODEC "coding:magic.cpp"
- Dùng cppyy, bạn có thể phân tích và tạo binding cho mã C/C++ từ Python
cppyydùngclingở bên trong
- Kết quả tiền xử lý về cơ bản sẽ thành mã Python như sau
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main- gọi
main()của C/C++ khi__name__ == "__main__"
- Có thể xem ví dụ triển khai tại magic.cpp
- Các ngôn ngữ dùng
Dùng như công cụ xác thực TOML
- TOML bắt đầu chú thích bằng
#, nên có thể thêm dòng ma thuật mã hóa như# coding: magic.toml - Nếu biến kết quả tiền xử lý thành script xác thực Python, bạn có thể dùng trình thông dịch Python như một công cụ xác thực TOML
- Ví dụ xác thực dùng các mô-đun sau
tomllibđể đọc tệp TOMLjsonđể đọc tệp JSON Schema- jsonschema để xác thực
- Cách chạy:
python tests/toml/data_valid.toml -s tests/toml/schema.json- Nếu hợp lệ, sẽ in
Successfully validated.
- Với ví dụ TOML không hợp lệ, sẽ xuất lỗi xác thực rằng chuỗi
'20'trong mảngscoreskhông phải là số - Có thể xem ví dụ triển khai tại magic.toml
Ứng dụng thực tế và magic_codec
- Kết hợp codec tự định nghĩa với tệp
.pthcó thể thay đổi mạnh cách hoạt động của trình thông dịch Python - Phần lớn ví dụ mang tính vui vẻ, nhưng cũng có các trường hợp sử dụng thực tế
- pythonql: phần mở rộng ngôn ngữ truy vấn cho Python
- future-typing: backport generic type hint và cú pháp union
|cho Python 3.6+ - future-fstrings
- future-annotations
- Nếu không muốn tự sửa
site-packageshoặc tự viết tệp.pthcùng mã lặp lại, bạn có thể dùng magic_codec - Phần mở rộng
magic_codeccó thể được tạo thành các gói Python mang tiền tốmagic_- Nếu đặt codec của tệp là
magic_foo, nó sẽ nạp góimagic_foo - Sau đó kiểm tra xem gói đó có hàm
preprocesshay không
- Nếu đặt codec của tệp là
- Chữ ký
preprocessđược kỳ vọng như saudef preprocess(data: str) -> str:
- Có thể xem ví dụ mở rộng tại example/
1 bình luận
Bình luận trên Hacker News
Thông báo lỗi hài hước
SyntaxError: not a chancekhi chạyfrom __future__ import bracesđã được hardcode trong CPython từ năm 2001https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
Jeremy Hylton, người viết đoạn đó, hiện là Principal Engineer phụ trách chất lượng tìm kiếm AI tại Google; khá ấn tượng khi trong 24 năm, sự nghiệp của ông đi từ việc đùa vui để kỷ niệm một cú pháp bị cấm sang xây dựng hệ thống truy vấn phổ quát không cần cú pháp chuyên biệt
break rust;từng gây ra lỗi nội bộ của trình biên dịch trong Rust. Không biết còn bao nhiêu easter egg tương tự trong các ngôn ngữ khácTôi nghĩ việc xem kiểu hack vui vẻ, không chính thức, như một thế giới tách biệt với phát triển phần mềm chuyên nghiệp là một hiểu lầm
https://news.ycombinator.com/item?id=41314393
Tôi từng nghĩ nghịch import hook là cách sáng tạo nhất để bị sa thải, nhưng hóa ra suy nghĩ đó vẫn còn ngây thơ. Hơi tiếc là do regex của codec nên chắc không troll tử tế được bằng kiểu
μtf8, vậy giờ chỉ còn cách dùng import hook, preprocessor vàsys.settraceđể monkey patch mọi hàm thành hàm vừa được gọi ngay trước đó, rồi đảo stdout và stderr mỗi 17 phútCó lý do chính đáng để Python không cố tình phơi bày preprocessor hook, và đây là thứ mà một người lớn lý trí nên tránh xa
Nhưng mặt khác, tôi cũng chẳng muốn dính dáng đến những người lớn lý trí. Có vẻ sẽ làm được rất nhiều trò thực sự thú vị
public/private, và chắc vì vậy cũng mở ra đủ loại ma thuật metaprogramming. Nếu họ thực sự lo về “người lớn lý trí” thì những quyết định thiết kế này sẽ hơi kỳ cục :)Cái này trông tiện và thực sự hữu ích. Khi cần làm mấy trò import hack vô lý, tôi thường import module, sửa lại mã bằng module
ast,execnó rồi chèn thêmexit(), nhưng nếu có preprocessor thì sẽ dễ dùng hơn nhiềuTrước khi
dictluôn đảm bảo thứ tự, tôi chủ yếu dùng rewrite bằngastđể biến list literal thành lời gọi ordered dict, và nó thực sự hữu íchTôi thích sự linh hoạt của Python. Trò bị nguyền rủa nhất tôi từng làm là thay đổi chuỗi ngay tại chỗ, rồi cuối cùng còn lạm dụng cả
mmapđể viết script tự sửa chính nó. Giờ có cảm giác như tiếp theo nên viết một trình thông dịch Lisp bằng producerctypessao?Trường hợp sử dụng hay nhất tôi tìm được đến giờ là pyxl lấy cảm hứng từ JSX: https://github.com/dropbox/pyxl
Bạn có thể viết mã như thế này
# coding: pyxlprint Hello World!Tôi tự hỏi liệu nó có thể đã được dùng để xử lý tốt hơn quá trình chuyển từ Python 2 sang 3 hay không. Ví dụ
# coding: six.python2có thể chỉnh mã Python 2 thành Python 3 hợp lệ, hoặc# coding: six.python3biến mã Python 3 để chạy được trên Python 2Có lẽ còn có thể thêm hoặc bỏ các tiền tố như
b"..."hayu"..."dictthì chúng trỏ đến cùng một mục; còn trong Python 3,bytesvàstrcó cùng nội dung ASCII lại là hai mục khác nhau trong cùng mộtdictCòn có những thay đổi khó chịu hơn. Nhiều thứ built-in như
.keys()và.values()trả về list trong Python 2 nhưng trả về iterator trong Python 3. Nếu cố dịch mã một cách an toàn bằng các tiện íchsixhay mẹo lách khác, mã sẽ trở nên rất dài dòng. Đa số trường hợp chỉ dùng một lần, nhưng đôi khi lại dùng hai lầnNếu đã có công cụ có thể sửa mã tại thời điểm import, tôi nghĩ tốt hơn là commit luôn mã đã được chuyển đổi rồi dọn dẹp dần. Phần khó là những thay đổi hành vi ảnh hưởng đến cả các đoạn mã ở xa, như
strso vớibytesCác dependency phát sinh từ chiến lược coding hook này có bị
pip freezehayuvbắt được không?Nếu không thì chắc sẽ vui lắm :). Nếu ai đó từng nhét thứ này vào thì gần như có thể chắc còn những cái bẫy khác nữa, nên thay vì chiến đấu với kiểu rồng đó, có lẽ viết lại cả thư viện còn dễ hơn
Sẽ khá buồn cười nếu tạo ra một Python giả mã rồi để LLM giải mã nó. Đương nhiên sẽ rất kinh khủng, nhưng có lẽ cũng vui