1 คะแนน โดย GN⁺ 2024-08-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Python สามารถใช้ คอมเมนต์มหัศจรรย์สำหรับการเข้ารหัสซอร์ส ในสองบรรทัดแรก และ codec ที่ผู้ใช้กำหนดเอง เพื่อเปลี่ยนเนื้อหาไฟล์ก่อนรัน หรือแทนที่ด้วยโค้ดที่ต่างออกไปโดยสิ้นเชิงได้
  • codec ที่ผู้ใช้กำหนดเองสามารถลงทะเบียนระหว่างการเริ่มต้นอินเทอร์พรีเตอร์ผ่านการรัน import ใน ไฟล์ตั้งค่าพาธ .pth และเพิ่มฟังก์ชันค้นหาด้วย codecs.register
  • การ implement codec ต้องมี decode(data: bytes) -> tuple[str, int] และ incremental decoder และหากไม่จัดการ exception อาจเห็นเพียง SyntaxError: encoding problem: your_codec แทนสาเหตุจริง
  • สามารถ implement ผ่านจุดเริ่มต้นเดียวกันได้ตั้งแต่โอเปอเรเตอร์เพิ่ม/ลด ++/--, Python แบบใช้วงเล็บปีกกา, การรัน C/C++ ผ่าน cppyy ไปจนถึงวิธีตรวจสอบ TOML ด้วย JSON Schema
  • นอกจากตัวอย่างเล่น ๆ แล้ว ยังใช้กับส่วนขยายและแบ็กพอร์ตของ Python เช่น pythonql, future-typing, future-fstrings, future-annotations ได้ด้วย และ magic_codec ช่วยลดงานซ้ำ ๆ

ใช้การเข้ารหัสซอร์สเป็นจุดเริ่มต้นของพรีโปรเซส

  • ตาม PEP-0263 สามารถระบุ การเข้ารหัสซอร์สโค้ด ไว้ในหนึ่งในสองบรรทัดแรกของไฟล์ Python ได้
    • ตัวอย่าง: # coding=utf8, # -*- coding: utf8 -*-, # vim: set fileencoding=utf8 :
  • บรรทัดมหัศจรรย์ต้องตรงกับ regular expression ^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)
    • ชื่อ codec ต้องตรงกับ [-_.a-zA-Z0-9]+
  • codec ที่ผู้ใช้กำหนดเองไม่ได้จำกัดอยู่แค่การถอดรหัสซอร์ส แต่สามารถเปลี่ยนสตริงซอร์สแล้วส่งต่อให้อินเทอร์พรีเตอร์ Python ได้

ลงทะเบียน codec ด้วยไฟล์ .pth

  • เมื่ออินเทอร์พรีเตอร์ Python เริ่มทำงานโดยไม่มีตัวเลือก -S แพ็กเกจ site จะถูกโหลดระหว่างการเริ่มต้น
  • ไฟล์ตั้งค่าพาธ .pth ใน site-packages จะเพิ่มเนื้อหาที่ไม่ใช่บรรทัดว่างและไม่ได้ขึ้นต้นด้วย # เข้าไปในพาธค้นหาโมดูล
  • ตามเอกสาร Python บรรทัดที่มีช่องว่างหรือแท็บตามหลัง import จะถูก execute
    • ตัวอย่าง: หากใส่ import packagename.register_codec ใน packagename.pth โมดูลนั้นจะถูก import ระหว่างการเริ่มต้น
  • โมดูลที่ถูก import สามารถเรียก codecs.register เพื่อลงทะเบียนฟังก์ชันค้นหา codec ได้
    • import จะถูกรันเพียงครั้งเดียว ดังนั้นฟังก์ชันค้นหาก็จะถูกลงทะเบียนเพียงครั้งเดียวเช่นกัน

การ implement codec ที่ผู้ใช้กำหนดเอง

  • codec ที่ผู้ใช้กำหนดเองต้องมีสองอย่าง
    • decode(data: bytes) -> tuple[str, int]
    • คลาส incremental decoder
  • ฟังก์ชัน decode สามารถใช้ codecs.utf_8_decode เพื่อถอดรหัส UTF-8 จริง ๆ แล้วส่งสตริงผลลัพธ์ไปยังฟังก์ชันพรีโปรเซสได้
  • หาก exception ภายใน codec ไม่ถูกจับ อาจแสดงเพียง SyntaxError: encoding problem: your_codec แทน traceback ทั่วไป
    • ควรให้ exception ที่เกิดในฟังก์ชันพรีโปรเซสพิมพ์เองด้วย traceback.print_exc() แล้วค่อย raise อีกครั้ง
  • incremental decoder สามารถเก็บไฟล์ทั้งหมดไว้ในบัฟเฟอร์ แล้วพรีโปรเซสเพียงครั้งเดียวในการเรียก decode ครั้งสุดท้ายได้
    • ตัวอย่างการ implement คือสืบทอดจาก codecs.BufferedIncrementalDecoder และประมวลผลเฉพาะเมื่อเป็น final ใน decode(self, data, final=False)
  • ผลลัพธ์จากพรีโปรเซสไม่จำเป็นต้องใช้เนื้อหาไฟล์ต้นฉบับ และจะคืนโค้ด Python ใด ๆ ก็ได้
    • อย่างไรก็ตาม บรรทัดแรกจะถูกคาดหวังว่าเป็นบรรทัดมหัศจรรย์จึงถูกลบออก และผลลัพธ์ต้องเป็น Python ที่ถูกต้อง

ตัวอย่างการขยายไวยากรณ์ Python

  • โอเปอเรเตอร์เพิ่ม/ลด ++ และ --

    • Python ไม่มี โอเปอเรเตอร์เพิ่ม/ลด แบบ unary
    • x++, x-- ไม่ถูกต้องตามไวยากรณ์
    • ++x, --x ถูกต้องตามไวยากรณ์ แต่จะกลายเป็นการเรียก x.__pos__().__pos__() และ x.__neg__().__neg__() ตามลำดับ
    • พรีโปรเซสเซอร์สามารถเปลี่ยน token stream ให้ทำงานเหมือนโอเปอเรเตอร์เพิ่ม/ลดได้
      • x++(x, x := x + 1)[0]
      • x--(x, x := x - 1)[0]
      • ++x(x, x := x + 1)[1]
      • --x(x, x := x - 1)[1]
    • การแปลงนี้ใช้ walrus operator ซึ่งเป็น assignment expression ของ Python
    • การแทนที่ token แบบง่าย ๆ อาจล้มเหลวในนิพจน์อย่าง x++ - -y และสามารถลดความกำกวมได้ด้วยวงเล็บ เช่น x++ - (-y)
    • incdec.py แทนที่ด้วย regular expression แต่แม้จะพยายามหลีกเลี่ยงการแทนที่ภายใน string literal ก็ยังอาจเปราะบางได้
    • implementation ที่แก้ไข token stream โดยตรงอยู่ที่ magic.incdec
  • Python แบบใช้วงเล็บปีกกา

    • from __future__ import braces จะทำให้เกิด SyntaxError: not a chance
    • พรีโปรเซสเซอร์สามารถแก้ไข token stream เพื่อแปลง scope แบบวงเล็บปีกกา เป็น Python ที่อิง indentation ได้
    • ลำดับการ implement เป็นดังนี้
      • สร้าง token ด้วย tokenize.generate_tokens
      • ส่งสตริงอินพุตทีละบรรทัดด้วย readline ของ io.StringIO
      • ลบ token INDENT, DEDENT เดิมออก
      • เมื่อพบ { ให้เพิ่มระดับ indentation และพิมพ์ :
      • เมื่อพบ } ให้ลดระดับ indentation
      • หลัง NL ให้เพิ่ม token INDENT ที่ตรงกับระดับ indentation ปัจจุบัน
    • เพื่อลดการชนกับ dictionary literal ของ Python สามารถปรับระดับ indentation เฉพาะเมื่อหลัง { เป็น newline และถือว่าเป็นการปิด scope เฉพาะเมื่อก่อน } เป็น newline ได้
    • dictionary หลายบรรทัดสามารถใช้ backslash ได้ เพราะจะไม่เกิด token newline ภายในวงเล็บปีกกา

รันภาษาอื่นเป็น Python

  • C และ C++

    • ภาษาอย่าง shell script, CMake script, PHP, Ruby ที่ใช้ # เป็นคอมเมนต์ สามารถใส่ shebang พร้อมบรรทัดมหัศจรรย์สำหรับการเข้ารหัสได้ง่าย
    • C และ C++ ใช้คอมเมนต์แบบ /* ... */ หรือ // ... แต่ preprocessor directive เริ่มด้วย # จึงสามารถทำให้ตรงกับ regular expression ของการเข้ารหัสได้
    • ตัวอย่างบรรทัดมหัศจรรย์นี้ถูกต้องทั้งในซอร์ส C/C++ และตรงกับแพตเทิร์นการเข้ารหัสของ Python
      • #define CODEC "coding:magic.cpp"
    • เมื่อใช้ cppyy จะสามารถตีความโค้ด C/C++ และสร้าง binding จาก Python ได้
      • cppyy ใช้ cling ภายใน
    • ผลลัพธ์จากพรีโปรเซสโดยคร่าว ๆ จะเป็นโค้ด Python ต่อไปนี้
      • import cppyy
      • cppyy.cppdef("<input source file content>")
      • from cppyy.gbl import main
      • เรียก main() ของ C/C++ เมื่อ __name__ == "__main__"
    • ตัวอย่าง implementation อยู่ที่ magic.cpp

ใช้เป็นเครื่องมือตรวจสอบ TOML

  • TOML เริ่มคอมเมนต์ด้วย # จึงสามารถใส่บรรทัดมหัศจรรย์สำหรับการเข้ารหัสอย่าง # coding: magic.toml ได้
  • หากเปลี่ยนผลลัพธ์จากพรีโปรเซสเป็นสคริปต์ตรวจสอบด้วย Python ก็สามารถใช้อินเทอร์พรีเตอร์ Python เหมือนเป็น เครื่องมือตรวจสอบ TOML ได้
  • ตัวอย่างการตรวจสอบใช้โมดูลต่อไปนี้
    • อ่านไฟล์ TOML ด้วย tomllib
    • อ่านไฟล์ JSON Schema ด้วย json
    • ตรวจสอบด้วย jsonschema
  • ตัวอย่างการรัน:
    • python tests/toml/data_valid.toml -s tests/toml/schema.json
    • หากถูกต้อง จะพิมพ์ Successfully validated.
  • ในตัวอย่าง TOML ที่ไม่ถูกต้อง จะแสดงข้อผิดพลาดการตรวจสอบว่าสตริง '20' ในอาร์เรย์ scores ไม่ใช่ตัวเลข
  • ตัวอย่าง implementation อยู่ที่ magic.toml

การใช้งานจริงและ magic_codec

  • เมื่อใช้ codec ที่ผู้ใช้กำหนดเองร่วมกับไฟล์ .pth จะสามารถเปลี่ยนพฤติกรรมของอินเทอร์พรีเตอร์ Python ได้อย่างมาก
  • ตัวอย่างส่วนใหญ่ทำขึ้นเพื่อความสนุก แต่ก็มีกรณีใช้งานจริงด้วย
  • หากไม่ต้องการแก้ site-packages เอง หรือเขียนไฟล์ .pth และโค้ดซ้ำ ๆ เอง สามารถใช้ magic_codec ได้
  • ส่วนขยาย magic_codec สามารถสร้างเป็นแพ็กเกจ Python ที่มีคำนำหน้า magic_
    • หากตั้งค่าไฟล์ codec เป็น magic_foo จะโหลดแพ็กเกจ magic_foo
    • ตรวจสอบว่าแพ็กเกจนั้นมีฟังก์ชัน preprocess หรือไม่
  • signature ของ preprocess ที่คาดหวังคือ
    • def preprocess(data: str) -> str:
  • ตัวอย่างส่วนขยายอยู่ที่ example/

1 ความคิดเห็น

 
GN⁺ 2024-08-23
ความคิดเห็นบน Hacker News
  • ข้อความ error ตลก ๆ ที่เมื่อรัน from __future__ import braces แล้วได้ SyntaxError: not a chance นั้น ถูกฮาร์ดโค้ดอยู่ใน CPython มาตั้งแต่ปี 2001
    https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
    Jeremy Hylton ผู้เขียน ตอนนี้ทำงานที่ Google ในตำแหน่ง Principal Engineer ดูแลคุณภาพ AI Search อยู่ ซึ่งค่อนข้างน่าประทับใจที่ในช่วง 24 ปี เส้นทางอาชีพของเขาเหมือนต่อเนื่องจากการล้อเล่นเพื่อรำลึกถึงไวยากรณ์ที่ถูกห้าม ไปสู่ระบบ query แบบสากลที่ไม่ต้องใช้ไวยากรณ์เฉพาะ

    • นึกถึงกรณี break rust; ที่เคยทำให้คอมไพเลอร์ Rust เกิด internal compiler error ขึ้นมา สงสัยเหมือนกันว่าภาษาอื่น ๆ จะมี easter egg แบบนี้อีกมากแค่ไหน
    • ไม่เห็นว่ามันน่าประหลาดใจตรงไหนเลย ปี 2001 ไม่ใช่ว่าใครก็ใส่อะไรลงใน Python ได้ ตอนนั้นมันยังเป็นโปรเจ็กต์เฉพาะกลุ่ม และคนที่ contribute ก็น่าจะฉลาดและทุ่มเทพอที่จะมีโอกาสได้เส้นทางอาชีพที่มีอิทธิพลสูงอยู่แล้ว
      ผมคิดว่าเป็นความเข้าใจผิดที่มองว่า hobby hacking แบบเล่น ๆ ไม่เป็นทางการ เป็นคนละโลกกับการพัฒนาซอฟต์แวร์มืออาชีพจริง ๆ
    • เป็นยุคที่ไร้เดียงสาจริง ๆ Hylton อาจเข้าร่วมกระแส ลงมติไม่ไว้วางใจ เพื่อความยุติธรรมของ Tim Peters ก็ได้
      https://news.ycombinator.com/item?id=41314393
    • การได้เห็น easter egg แบบนี้เท่เสมอ น่าเสียดายที่เดี๋ยวนี้ไม่ค่อยเจอบ่อยเหมือนเมื่อก่อน
  • เคยคิดว่าการเล่นกับ import hook คือวิธีสร้างสรรค์ที่สุดที่จะโดนไล่ออก แต่นั่นเป็นความคิดที่ใสซื่อไปหน่อย เสียดายที่ regex ของ codec น่าจะทำให้ troll ด้วยอะไรอย่าง μtf8 แบบเต็มที่ไม่ได้ ตอนนี้คงเหลือแค่ใช้ import hook, preprocessor และ sys.settrace เพื่อ monkey patch ทุกฟังก์ชันให้กลายเป็นฟังก์ชันที่ถูกเรียกก่อนหน้าทันที และ สลับ stdout กับ stderr ทุก ๆ 17 นาที แล้วล่ะ

    • ในฐานะภาษาที่ดี ก็ควรบังคับให้ ใช้วงเล็บปีกกา ด้วย
  • มีเหตุผลที่ดีที่ Python จงใจไม่เปิดเผย preprocessor hook ออกมา และผมคิดว่านี่เป็นส่วนที่ผู้ใหญ่ที่มีเหตุผลควรอยู่ให้ห่าง
    แต่อีกด้านหนึ่ง ผมก็ไม่อยากไปเกี่ยวข้องกับผู้ใหญ่ที่มีเหตุผลเท่าไรนัก น่าจะทำเรื่องสนุก ๆ ได้เยอะมาก

    • Python มีปรัชญาว่าเป็นภาษาสำหรับ “ผู้ใหญ่ที่ยินยอมพร้อมใจกัน (consenting adults)” ดังนั้นจึงไม่มีตัวกำหนด visibility อย่าง public/private และดูเหมือนจะเปิดทางให้ เวทมนตร์ metaprogramming สารพัดด้วย ถ้าเขากังวลเรื่อง “ผู้ใหญ่ที่มีเหตุผล” จริง ๆ การตัดสินใจออกแบบเหล่านี้ก็คงดูแปลกอยู่สักหน่อย :)
    • ถ้าห้ามไม่ให้คนทำสิ่งนี้ด้วยวิธีที่ง่ายและชัดเจน พวกเขาก็จะลองทำด้วยวิธีที่แย่กว่าและ hacky กว่า
  • อันนี้ดูสะดวกและน่าจะมีประโยชน์จริง ๆ เวลาทำ import hack บ้า ๆ ปกติผมจะ import โมดูล แล้วใช้โมดูล ast แก้โค้ด จากนั้น exec แล้วแทรก exit() เข้าไป แต่ถ้ามี preprocessor ก็น่าจะใช้ง่ายกว่ามาก
    ก่อนที่ dict ทั้งหมดจะรับประกันลำดับ ผมใช้การ rewrite ด้วย ast เป็นหลักเพื่อเปลี่ยน list literal ให้เป็นการเรียก ordered dict และมันมีประโยชน์จริง ๆ
    ผมชอบความยืดหยุ่นของ Python สิ่งที่ถูกสาปที่สุดที่ผมเคยทำคือแก้ string แบบ in-place และสุดท้ายก็ถึงขั้น abuse mmap เพื่อเขียนสคริปต์ที่แก้ไขตัวเอง ตอนนี้รู้สึกเหมือนควรจะเขียน Lisp interpreter เป็น producer แล้ว

    • “แก้ string แบบ in-place” งั้นเหรอ string เป็น immutable นะ ใช้ ctypes อะไรพวกนั้นเขียนตรงตำแหน่งหน่วยความจำหรือเปล่า?
  • use case ที่ดีที่สุดที่เจอมาจนถึงตอนนี้คือ pyxl ที่ได้แรงบันดาลใจจาก JSX: https://github.com/dropbox/pyxl
    เขียนโค้ดแบบนี้ได้
    # coding: pyxl
    print Hello World!

  • สงสัยว่าสิ่งนี้อาจถูกใช้เพื่อจัดการการเปลี่ยนผ่านจาก Python 2 ไป 3 ได้ดีกว่านี้ไหม เช่น # coding: six.python2 ช่วยปรับโค้ด Python 2 ให้เป็นโค้ด Python 3 ที่ valid หรือ # coding: six.python3 แปลงโค้ด Python 3 ให้รันบน Python 2 ได้
    การเพิ่มหรือลบ prefix อย่าง b"..." หรือ u"..." ก็น่าจะทำได้เหมือนกัน

    • อาจช่วยได้ แต่ส่วนที่ช่วยได้คือส่วนที่ง่าย ส่วนที่ ยากของการย้ายจาก Python 2 ไป 3 คือการเปลี่ยนแปลงพฤติกรรมตอน runtime ใน Python 2 นั้น Unicode ที่มี ASCII กับ string ปกติทำตัวเหมือนเป็น string เดียวกัน ถ้าใช้เป็น key ของ dict ก็จะชี้ไปยังรายการเดียวกัน แต่ใน Python 3 นั้น bytes กับ str ที่มีเนื้อหา ASCII เหมือนกัน จะชี้ไปยังคนละรายการใน dict เดียวกัน
      ยังมีการเปลี่ยนแปลงที่ยุ่งยากกว่านั้นด้วย builtin หลายอย่างอย่าง .keys() และ .values() ใน Python 2 คืนค่าเป็น list แต่ใน Python 3 คืนค่าเป็น iterator ถ้าพยายามแปลโค้ดให้ปลอดภัยด้วย utility ของ six หรือ workaround อื่น ๆ โค้ดจะยืดยาวมาก เพราะส่วนใหญ่ถูกใช้เพียงครั้งเดียว แต่บางครั้งก็ถูกใช้สองครั้ง
      ถ้ามีเครื่องมือที่แก้โค้ดได้ตอน import ผมคิดว่าสู้ commit โค้ดที่แปลงแล้ว แล้วค่อย ๆ เก็บกวาดทีละส่วนยังดีกว่า ส่วนที่ยากคือการเปลี่ยนพฤติกรรมที่ส่งผลไปถึงโค้ดที่อยู่ห่างไกลกัน เช่น str กับ bytes
  • dependency ที่เกิดจาก กลยุทธ์ coding hook แบบนี้ pip freeze หรือ uv จับได้ไหม?
    ถ้าไม่ ก็คงสนุกน่าดู :). ถ้ามีใครใส่อะไรแบบนี้ไว้ ก็แทบรับประกันได้เลยว่ายังมีกับดักอื่นอยู่ด้วย สู้เขียนไลบรารีใหม่เองน่าจะง่ายกว่าการไปสู้กับมังกรพวกนั้น

  • ถ้าทำ Python แบบ pseudocode แล้วให้ LLM decode มัน ก็น่าจะฮาดี แน่นอนว่าคงเลวร้ายมาก แต่ก็น่าจะสนุก