พรีโปรเซสเซอร์ของ Python
(pydong.org)- 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 ต้องตรงกับ
- 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 อีกครั้ง
- ควรให้ exception ที่เกิดในฟังก์ชันพรีโปรเซสพิมพ์เองด้วย
- incremental decoder สามารถเก็บไฟล์ทั้งหมดไว้ในบัฟเฟอร์ แล้วพรีโปรเซสเพียงครั้งเดียวในการเรียก
decodeครั้งสุดท้ายได้- ตัวอย่างการ implement คือสืบทอดจาก
codecs.BufferedIncrementalDecoderและประมวลผลเฉพาะเมื่อเป็นfinalในdecode(self, data, final=False)
- ตัวอย่างการ implement คือสืบทอดจาก
- ผลลัพธ์จากพรีโปรเซสไม่จำเป็นต้องใช้เนื้อหาไฟล์ต้นฉบับ และจะคืนโค้ด 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ให้เพิ่ม tokenINDENTที่ตรงกับระดับ indentation ปัจจุบัน
- สร้าง token ด้วย
- เพื่อลดการชนกับ 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 cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main- เรียก
main()ของ C/C++ เมื่อ__name__ == "__main__"
- ตัวอย่าง implementation อยู่ที่ magic.cpp
- ภาษาอย่าง shell script, CMake script, PHP, Ruby ที่ใช้
ใช้เป็นเครื่องมือตรวจสอบ TOML
- TOML เริ่มคอมเมนต์ด้วย
#จึงสามารถใส่บรรทัดมหัศจรรย์สำหรับการเข้ารหัสอย่าง# coding: magic.tomlได้ - หากเปลี่ยนผลลัพธ์จากพรีโปรเซสเป็นสคริปต์ตรวจสอบด้วย Python ก็สามารถใช้อินเทอร์พรีเตอร์ Python เหมือนเป็น เครื่องมือตรวจสอบ TOML ได้
- ตัวอย่างการตรวจสอบใช้โมดูลต่อไปนี้
- อ่านไฟล์ TOML ด้วย
tomllib - อ่านไฟล์ JSON Schema ด้วย
json - ตรวจสอบด้วย jsonschema
- อ่านไฟล์ TOML ด้วย
- ตัวอย่างการรัน:
python tests/toml/data_valid.toml -s tests/toml/schema.json- หากถูกต้อง จะพิมพ์
Successfully validated.
- ในตัวอย่าง TOML ที่ไม่ถูกต้อง จะแสดงข้อผิดพลาดการตรวจสอบว่าสตริง
'20'ในอาร์เรย์scoresไม่ใช่ตัวเลข - ตัวอย่าง implementation อยู่ที่ magic.toml
การใช้งานจริงและ magic_codec
- เมื่อใช้ codec ที่ผู้ใช้กำหนดเองร่วมกับไฟล์
.pthจะสามารถเปลี่ยนพฤติกรรมของอินเทอร์พรีเตอร์ Python ได้อย่างมาก - ตัวอย่างส่วนใหญ่ทำขึ้นเพื่อความสนุก แต่ก็มีกรณีใช้งานจริงด้วย
- pythonql: ส่วนขยายภาษา query สำหรับ Python
- future-typing: แบ็กพอร์ต generic type hint และไวยากรณ์ union แบบ
|ไปยัง Python 3.6+ - future-fstrings
- future-annotations
- หากไม่ต้องการแก้
site-packagesเอง หรือเขียนไฟล์.pthและโค้ดซ้ำ ๆ เอง สามารถใช้ magic_codec ได้ - ส่วนขยาย
magic_codecสามารถสร้างเป็นแพ็กเกจ Python ที่มีคำนำหน้าmagic_- หากตั้งค่าไฟล์ codec เป็น
magic_fooจะโหลดแพ็กเกจmagic_foo - ตรวจสอบว่าแพ็กเกจนั้นมีฟังก์ชัน
preprocessหรือไม่
- หากตั้งค่าไฟล์ codec เป็น
- signature ของ
preprocessที่คาดหวังคือdef preprocess(data: str) -> str:
- ตัวอย่างส่วนขยายอยู่ที่ example/
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ข้อความ error ตลก ๆ ที่เมื่อรัน
from __future__ import bracesแล้วได้SyntaxError: not a chanceนั้น ถูกฮาร์ดโค้ดอยู่ใน CPython มาตั้งแต่ปี 2001https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
Jeremy Hylton ผู้เขียน ตอนนี้ทำงานที่ Google ในตำแหน่ง Principal Engineer ดูแลคุณภาพ AI Search อยู่ ซึ่งค่อนข้างน่าประทับใจที่ในช่วง 24 ปี เส้นทางอาชีพของเขาเหมือนต่อเนื่องจากการล้อเล่นเพื่อรำลึกถึงไวยากรณ์ที่ถูกห้าม ไปสู่ระบบ query แบบสากลที่ไม่ต้องใช้ไวยากรณ์เฉพาะ
break rust;ที่เคยทำให้คอมไพเลอร์ Rust เกิด internal compiler error ขึ้นมา สงสัยเหมือนกันว่าภาษาอื่น ๆ จะมี easter egg แบบนี้อีกมากแค่ไหนผมคิดว่าเป็นความเข้าใจผิดที่มองว่า hobby hacking แบบเล่น ๆ ไม่เป็นทางการ เป็นคนละโลกกับการพัฒนาซอฟต์แวร์มืออาชีพจริง ๆ
https://news.ycombinator.com/item?id=41314393
เคยคิดว่าการเล่นกับ import hook คือวิธีสร้างสรรค์ที่สุดที่จะโดนไล่ออก แต่นั่นเป็นความคิดที่ใสซื่อไปหน่อย เสียดายที่ regex ของ codec น่าจะทำให้ troll ด้วยอะไรอย่าง
μtf8แบบเต็มที่ไม่ได้ ตอนนี้คงเหลือแค่ใช้ import hook, preprocessor และsys.settraceเพื่อ monkey patch ทุกฟังก์ชันให้กลายเป็นฟังก์ชันที่ถูกเรียกก่อนหน้าทันที และ สลับ stdout กับ stderr ทุก ๆ 17 นาที แล้วล่ะมีเหตุผลที่ดีที่ Python จงใจไม่เปิดเผย preprocessor hook ออกมา และผมคิดว่านี่เป็นส่วนที่ผู้ใหญ่ที่มีเหตุผลควรอยู่ให้ห่าง
แต่อีกด้านหนึ่ง ผมก็ไม่อยากไปเกี่ยวข้องกับผู้ใหญ่ที่มีเหตุผลเท่าไรนัก น่าจะทำเรื่องสนุก ๆ ได้เยอะมาก
public/privateและดูเหมือนจะเปิดทางให้ เวทมนตร์ metaprogramming สารพัดด้วย ถ้าเขากังวลเรื่อง “ผู้ใหญ่ที่มีเหตุผล” จริง ๆ การตัดสินใจออกแบบเหล่านี้ก็คงดูแปลกอยู่สักหน่อย :)อันนี้ดูสะดวกและน่าจะมีประโยชน์จริง ๆ เวลาทำ import hack บ้า ๆ ปกติผมจะ import โมดูล แล้วใช้โมดูล
astแก้โค้ด จากนั้นexecแล้วแทรกexit()เข้าไป แต่ถ้ามี preprocessor ก็น่าจะใช้ง่ายกว่ามากก่อนที่
dictทั้งหมดจะรับประกันลำดับ ผมใช้การ rewrite ด้วยastเป็นหลักเพื่อเปลี่ยน list literal ให้เป็นการเรียก ordered dict และมันมีประโยชน์จริง ๆผมชอบความยืดหยุ่นของ Python สิ่งที่ถูกสาปที่สุดที่ผมเคยทำคือแก้ string แบบ in-place และสุดท้ายก็ถึงขั้น abuse
mmapเพื่อเขียนสคริปต์ที่แก้ไขตัวเอง ตอนนี้รู้สึกเหมือนควรจะเขียน Lisp interpreter เป็น producer แล้วctypesอะไรพวกนั้นเขียนตรงตำแหน่งหน่วยความจำหรือเปล่า?use case ที่ดีที่สุดที่เจอมาจนถึงตอนนี้คือ pyxl ที่ได้แรงบันดาลใจจาก JSX: https://github.com/dropbox/pyxl
เขียนโค้ดแบบนี้ได้
# coding: pyxlprint Hello World!สงสัยว่าสิ่งนี้อาจถูกใช้เพื่อจัดการการเปลี่ยนผ่านจาก Python 2 ไป 3 ได้ดีกว่านี้ไหม เช่น
# coding: six.python2ช่วยปรับโค้ด Python 2 ให้เป็นโค้ด Python 3 ที่ valid หรือ# coding: six.python3แปลงโค้ด Python 3 ให้รันบน Python 2 ได้การเพิ่มหรือลบ prefix อย่าง
b"..."หรือu"..."ก็น่าจะทำได้เหมือนกันdictก็จะชี้ไปยังรายการเดียวกัน แต่ใน Python 3 นั้นbytesกับstrที่มีเนื้อหา ASCII เหมือนกัน จะชี้ไปยังคนละรายการในdictเดียวกันยังมีการเปลี่ยนแปลงที่ยุ่งยากกว่านั้นด้วย builtin หลายอย่างอย่าง
.keys()และ.values()ใน Python 2 คืนค่าเป็น list แต่ใน Python 3 คืนค่าเป็น iterator ถ้าพยายามแปลโค้ดให้ปลอดภัยด้วย utility ของsixหรือ workaround อื่น ๆ โค้ดจะยืดยาวมาก เพราะส่วนใหญ่ถูกใช้เพียงครั้งเดียว แต่บางครั้งก็ถูกใช้สองครั้งถ้ามีเครื่องมือที่แก้โค้ดได้ตอน import ผมคิดว่าสู้ commit โค้ดที่แปลงแล้ว แล้วค่อย ๆ เก็บกวาดทีละส่วนยังดีกว่า ส่วนที่ยากคือการเปลี่ยนพฤติกรรมที่ส่งผลไปถึงโค้ดที่อยู่ห่างไกลกัน เช่น
strกับbytesdependency ที่เกิดจาก กลยุทธ์ coding hook แบบนี้
pip freezeหรือuvจับได้ไหม?ถ้าไม่ ก็คงสนุกน่าดู :). ถ้ามีใครใส่อะไรแบบนี้ไว้ ก็แทบรับประกันได้เลยว่ายังมีกับดักอื่นอยู่ด้วย สู้เขียนไลบรารีใหม่เองน่าจะง่ายกว่าการไปสู้กับมังกรพวกนั้น
ถ้าทำ Python แบบ pseudocode แล้วให้ LLM decode มัน ก็น่าจะฮาดี แน่นอนว่าคงเลวร้ายมาก แต่ก็น่าจะสนุก