1 điểm bởi GN⁺ 2024-08-23 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 import trong tệp cấu hình đường dẫn .pth, rồi thêm hàm tìm kiếm bằng codecs.register
  • Việc triển khai codec cần decode(data: bytes) -> tuple[str, int]bộ giải mã tăng dần; nếu không xử lý ngoại lệ, bạn có thể chỉ thấy SyntaxError: encoding problem: your_codec thay 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++ qua cppyy, 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òn magic_codec giú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 :
  • 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]+
  • 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ói site sẽ được nạp trong quá trình khởi tạo
  • Các tệp cấu hình đường dẫn .pth trong site-packages sẽ 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 import theo sau là khoảng trắng hoặc tab sẽ được thực thi
    • Ví dụ: nếu thêm import packagename.register_codec vào packagename.pth, mô-đun đó sẽ được import trong lúc khởi tạo
  • Mô-đun được import có thể gọi codecs.register để đăng ký hàm tìm kiếm codec
    • import chỉ chạy một lần nên hàm tìm kiếm cũng chỉ được đăng ký một lần

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 decode có thể dùng codecs.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
  • 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 decode cuối cùng
    • Cách triển khai ví dụ là kế thừa codecs.BufferedIncrementalDecoder và chỉ xử lý trong decode(self, data, final=False) khi finalTrue
  • 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 ++--

    • 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, --x thì hợp lệ về mặt cú pháp nhưng lần lượt trở thành lời gọi x.__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 braces sẽ gây ra SyntaxError: 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 readline của io.StringIO
      • Loại bỏ các token INDENT, DEDENT sẵ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 token INDENT tương ứng với mức thụt lề hiện tại
    • Để 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
      • cppyy dùng cling ở bên trong
    • Kết quả tiền xử lý về cơ bản sẽ thành mã Python như sau
      • import cppyy
      • cppyy.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

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 TOML
    • json để đọ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ảng scores khô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 .pth có 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ế
  • Nếu không muốn tự sửa site-packages hoặc tự viết tệp .pth cùng mã lặp lại, bạn có thể dùng magic_codec
  • Phần mở rộng magic_codec có 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ói magic_foo
    • Sau đó kiểm tra xem gói đó có hàm preprocess hay không
  • Chữ ký preprocess được kỳ vọng như sau
    • def preprocess(data: str) -> str:
  • Có thể xem ví dụ mở rộng tại example/

1 bình luận

 
GN⁺ 2024-08-23
Bình luận trên Hacker News
  • Thông báo lỗi hài hước SyntaxError: not a chance khi chạy from __future__ import braces đã được hardcode trong CPython từ năm 2001
    https://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

    • Làm tôi nhớ đến vụ 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ác
    • Tôi không hiểu vì sao điều đó lại đáng ngạc nhiên. Năm 2001 đâu phải ai cũng có thể thêm thứ gì đó vào Python; khi đó nó là một dự án ngách, và những người đóng góp đều thông minh, tận tâm, nên khả năng cao sẽ có sự nghiệp nhiều ảnh hưởng
      Tô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
    • Đó là thời ngây thơ. Có khi Hylton giờ sẽ tham gia phong trào bỏ phiếu bất tín nhiệm để bảo vệ định nghĩa của Tim Peters
      https://news.ycombinator.com/item?id=41314393
    • Lúc nào nhìn thấy mấy easter egg kiểu này cũng thấy hay. Tiếc là giờ chúng không còn phổ biến như trước
  • 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út

    • Đúng tinh thần một ngôn ngữ tử tế thì cũng nên bắt buộc dùng dấu ngoặc nhọn
  • Có 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ị

    • Python có triết lý là ngôn ngữ dành cho “consenting adults”, nên không có bộ chỉ định phạm vi 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 :)
    • Nếu ngăn người ta làm việc này theo cách dễ và rõ ràng, họ sẽ thử bằng những cách tệ hơn và hacky hơn
  • 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, exec nó rồi chèn thêm exit(), nhưng nếu có preprocessor thì sẽ dễ dùng hơn nhiều
    Trước khi dict luôn đảm bảo thứ tự, tôi chủ yếu dùng rewrite bằng ast để biến list literal thành lời gọi ordered dict, và nó thực sự hữu ích
    Tô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 producer

    • “Thay đổi chuỗi ngay tại chỗ” á, chuỗi là bất biến mà. Bạn đã ghi thẳng vào vị trí bộ nhớ bằng kiểu ctypes sao?
  • 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: pyxl
    print 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.python2 có thể chỉnh mã Python 2 thành Python 3 hợp lệ, hoặc # coding: six.python3 biến mã Python 3 để chạy được trên Python 2
    Có lẽ còn có thể thêm hoặc bỏ các tiền tố như b"..." hay u"..."

    • Nó có thể giúp, nhưng phần nó giúp lại là phần dễ. Khó khăn của việc chuyển từ Python 2 sang 3 nằm ở thay đổi hành vi lúc chạy. Trong Python 2, Unicode chứa ASCII và chuỗi thường hoạt động như cùng một loại chuỗi, nên nếu dùng làm khóa dict thì chúng trỏ đến cùng một mục; còn trong Python 3, bytesstr có cùng nội dung ASCII lại là hai mục khác nhau trong cùng một dict
      Còn có những thay đổi khó chịu hơn. Nhiều thứ built-in như .keys().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 ích six hay 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ần
      Nế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ư str so với bytes
  • Các dependency phát sinh từ chiến lược coding hook này có bị pip freeze hay uv bắ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