- แม้แต่สคริปต์เล็ก ๆ สำหรับทดสอบ API ก็ยังต้องจัดสภาพแวดล้อมรันให้ตรงกันเพราะมี dependency แต่ถ้าใช้ uv execution shebang ก็ทำให้เป็นรูปแบบที่รันได้ทันทีโดยไม่ต้องมีขั้นตอนติดตั้ง
- ตัวอย่าง
jam_users.py ใช้ httpx, IPython, loguru เพื่อทดสอบ endpoint /users ของ Go API โดยลบและสร้างผู้ใช้ แล้วเข้าสู่ REPL
- วิธีเดิมต้องติดตั้งแพ็กเกจแบบ global ลงในระบบ Python หรือเตรียม virtual environment เอง ทำให้ทั้งการแชร์สคริปต์และการรันซ้ำทำได้ยุ่งยาก
- หากประกาศ
dependencies = ["ipython", "httpx", "loguru"] ในส่วนหัว # /// script แล้วรัน uv run jam_users.py uv จะจัดการทั้งสภาพแวดล้อมแบบแยกและการติดตั้ง dependency ให้
- ถ้าเพิ่ม shebang
#!/usr/bin/env -S uv run --script และสิทธิ์ execute ก็จะสามารถรันแบบ ./jam_users.py ได้บนระบบ Unix ในสภาพที่ ติดตั้งแค่ uv
ประกาศ dependency ภายในสคริปต์
- ตัวอย่าง
jam_users.py เตรียมข้อมูลผู้ใช้ทดสอบสำหรับ local API http://localhost:4000/v1/users
- ส่งคำขอ API ด้วย httpx
- เข้า REPL ของ IPython เพื่อตรวจดู response และทดสอบต่อ
- ใช้
loguru บันทึก log การลบและการสร้าง
- สคริปต์พื้นฐานมีลำดับการทำงานเป็นการรีเซ็ตรายการผู้ใช้ก่อน แล้วเติมข้อมูลทดสอบกลับเข้าไป
- ดึงรายการผู้ใช้เดิมด้วย
GET /v1/users
- เรียก
DELETE /v1/users/{id} สำหรับผู้ใช้แต่ละคน
- ส่งรายการผู้ใช้ที่เตรียมไว้ไปยัง
POST /v1/users ในรูปแบบ JSON
- จากนั้นเปิด REPL ด้วย
IPython.embed()
- หากจะรันด้วย
python jam_users.py จำเป็นต้องติดตั้ง httpx, IPython, loguru ไว้ในสภาพแวดล้อมรันล่วงหน้าแล้ว
- จะติดตั้งแบบ global ลงในระบบ Python หรือสร้าง virtual environment แยกต่างหากก็ได้ แต่ทั้งสองวิธีต้องมีการเตรียมพร้อมก่อนรัน
- การติดตั้งแบบ global อาจทำให้ระบบ Python มีแพ็กเกจปะปนจนรก
- วิธี virtual environment ต้องจัดการขั้นตอนสร้าง เปิดใช้งาน ติดตั้ง และรันด้วยตัวเอง
- ทั้งสองวิธียังต้องมีระบบ Python ที่เข้ากันได้กับแพ็กเกจที่ต้องใช้ด้วย
สร้างสคริปต์ที่รันได้โดยตรงด้วย uv
- ถ้าใส่แท็ก
# /// script ของ uv ไว้ที่ด้านบนของสคริปต์ ก็สามารถประกาศ dependency ไว้ในไฟล์ได้
# /// script
# dependencies = ["ipython", "httpx", "loguru"]
# ///
- เมื่อมีส่วนหัวนี้ ก็สามารถรันด้วย
uv run jam_users.py ได้
- uv จะสร้าง virtual environment แบบแยก สำหรับสคริปต์
- ดาวน์โหลดและติดตั้ง dependency ที่จำเป็น
- รันสคริปต์ภายใต้บริบทของ virtual environment นั้น
- shebang Python ทั่วไปอย่าง
#!/usr/bin/env python ใช้งานส่วนหัวสคริปต์ของ uv ไม่ได้ เพราะ Python จะมองข้ามคอมเมนต์ # /// script
- หากใส่การเรียก uv ลงใน shebang โดยตรง ก็จะทำให้สคริปต์ถูกใช้งานเหมือนไฟล์ executable ได้
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["ipython", "httpx", "loguru"]
# ///
- แฟลก
-S ของ env ใช้แยกสตริงที่ตามมาออกเป็นอาร์กิวเมนต์ต่างหากแล้วส่งต่อให้ env
- หลังให้สิทธิ์ execute ด้วย
chmod +x jam_users.py แล้ว ก็สามารถรันได้โดยตรงแบบนี้
./jam_users.py
- วิธีนี้ทำให้บนระบบ Unix ที่ ติดตั้ง uv อยู่แล้ว สามารถรันสคริปต์ได้โดยไม่ต้องติดตั้ง dependency เพิ่มหรือจัดการ virtual environment เอง
- เมื่อต้องส่งต่อสคริปต์ Python ที่ซับซ้อนให้ผู้ใช้อื่น ก็ช่วยลดภาระในการอธิบายขั้นตอนเตรียมระบบก่อนรันที่มักจะยาวและยุ่งยาก
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ไม่ใช่แค่ตัว UV เอง แต่โดยรวมแล้วรู้สึกขัดใจกับวิธีที่ ใช้คอมเมนต์ควบคุมการรันโค้ด
การใช้คอมเมนต์สำหรับคำสั่งให้ linter หรือบันทึกสำหรับนักพัฒนานั้นโอเค แต่ถ้าเป็นข้อมูลเกี่ยวกับการตั้งค่าหรือการรัน ผมมองว่ารูปแบบอย่าง
UV_ENV = { "dependencies": { "requests": "2.32.3", "pandas": "2.2.3" } }ดีกว่ามากวิธีนี้เป็นไวยากรณ์ Python ที่ถูกต้อง ใช้โครงสร้างข้อมูลมาตรฐานแทนการ parse คอมเมนต์ตามอำเภอใจ จึงสร้างและตรวจสอบได้ง่ายกว่า และที่สำคัญที่สุดคือยังรักษาหลักการที่ว่า แม้ลบคอมเมนต์ทั้งหมดออก โค้ดก็ควรรันได้เหมือนเดิม
วิธีที่เสนอมาก็ยังเป็น ค่าคงที่แบบเมจิก ที่ตอน runtime ไม่ได้ทำอะไรเลย และจะถูก parse ด้วย static analysis เท่านั้น แถมเครื่องมืออื่นอาจมองว่าเป็นโค้ดที่ไม่ได้ใช้แล้วลบทิ้งไป แทนที่ uv จะได้อ่าน
น่าจะให้เรียก uv โดยตรงว่าต้องทำอะไรไปเลย เช่น
import uvแล้วตามด้วยuv.exec(dependencies=["clown"], python=">=3.10")การรันครั้งแรกให้ทำบน Python runtime ใดก็ได้ที่หาแพ็กเกจ
uvสมมตินี้เจอ จากนั้นแพ็กเกจuvก็เตรียม virtual environment และ Python runtime แล้วติด flag เช่น environment variable ก่อนre-exec(3)ใน runtime รอบที่สอง
uv.execที่ตรวจพบ flag แล้วก็ไม่ต้องทำอะไรhttps://peps.python.org/pep-0723/
อีกอย่าง บรรทัด shebang เองก็ทำงานเป็นคอมเมนต์อยู่แล้วในความเป็นจริง
แค่ฝังรากลึกมา 45 ปีจนคนไม่ค่อยตระหนักว่ามันคือคอมเมนต์ของ shell เท่านั้นเอง
ถ้าติดตั้ง dependency ชุดเดียวกัน โค้ดก็รันเหมือนเดิม
มันไม่ได้เปลี่ยนความหมายของตัวโค้ดโดยตรง แต่เปลี่ยน สภาพแวดล้อม ที่โค้ดถูกรัน และในแง่นั้นก็ไม่ต่างจากคอมเมนต์
#!/bin/bashที่อยู่บรรทัดแรกของ shell scriptอย่างไรก็ตาม uv น่าจะไม่อยากรันโค้ดเพื่อค้นหา dependency ดังนั้นมันคงต้องเป็น subset ที่จำกัดมากของไวยากรณ์ Python
ความจริงที่ว่าเราต้องมีสิ่งแบบนี้ตั้งแต่แรก สะท้อนจุดอ่อนของภาษา
คำสั่ง
importเองควรจะสื่อข้อมูลทั้งหมดเกี่ยวกับ dependency ได้ช่วงไม่กี่เดือนที่ผ่านมา หัวข้อนี้โผล่บน HN บ่อยมาก และตัวอย่างล่าสุดมีดังนี้
https://news.ycombinator.com/item?id=43500124
https://news.ycombinator.com/item?id=42463975
ผมชอบ uv แต่ไม่ค่อยเห็นด้วยกับคำว่า self-contained ด้วยเหตุผลสองข้อ
ข้อแรก ถ้าจะรันสคริปต์ uv ต้องถูกติดตั้งไว้ก่อนแล้ว
จะทำเป็น shell script ให้ตรวจว่ามี uv ติดตั้งอยู่ไหม และถ้าไม่มีก็ให้ติดตั้งด้วย curlpipe ก็ได้ แต่ boilerplate จะเพิ่มขึ้นพอสมควร และวิธี curlpipe เองก็ไม่ดีด้วย
ข้อสอง การสร้าง virtual environment อัตโนมัติไว้ที่ไหนสักแห่งใน home directory ไม่ใช่ self-contained จริง ๆ
ถ้ารันครั้งเดียวแล้วลบสคริปต์ทิ้ง virtual environment นั้นก็ยังคงอยู่และกินพื้นที่ และผมหาหลักประกันจากเอกสารของ uv ไม่เจอว่ามันจะล้าง virtual environment ชั่วคราวแบบนี้ให้อัตโนมัติ
curl | shนั้นสมเหตุสมผลแต่เมื่อ package manager เริ่มรวม uv ไว้ใน repository ปัญหานี้ก็จะค่อย ๆ ลดลง
ตัวอย่างเช่น uv มีให้ใช้แล้วใน Alpine Linux และ Homebrew: https://repology.org/project/uv/versions
อีกอย่าง inline script metadata เป็นมาตรฐานของ Python
ถ้าในระบบไม่มี uv และไม่ได้ถูกแพ็กเกจไว้ แต่มี Python เวอร์ชันที่ตรงกับสคริปต์ ก็สามารถรันด้วย pipx ได้: https://pipx.pypa.io/stable/examples/#pipx-run-examples
pipx ถูกแพ็กเกจไว้แพร่หลายกว่ามาก: https://repology.org/project/pipx/versions
บนเครื่องนักพัฒนาทั่วไป Docker อาจมีอยู่บ่อยกว่า uv และต้นฉบับก็พูดถึงโปรเจกต์ของบริษัท จึงดูมีเหตุผล
แต่ตอนนี้ยังไม่ cache อะไรเลย ทำให้ต้องดาวน์โหลดทุกครั้งที่รัน ซึ่งดูไม่ค่อยเหมาะ และน่าจะแก้ได้ด้วย volume
ประมาณนี้: https://hugojosefson.github.io/docker-shebang/#python
แต่ถ้าตอนรันต้องดาวน์โหลดของที่ไม่รู้แน่ชัดจากอินเทอร์เน็ต ก็เรียกว่า self-contained ได้ยาก และ self-contained แบบสมบูรณ์จริง ๆ น่าจะใกล้เคียงกับ AppImage มากกว่า
ถ้าใช้ของอย่าง py2exe ก็สามารถทำ “สคริปต์ Python” แบบ self-contained ได้
สำหรับนักพัฒนาจะมีปัญหาเกิดขึ้นเยอะ แต่สำหรับผู้ใช้ปัญหาจะน้อยที่สุด
Nix ก็ใช้วิธีเดียวกัน โดยบรรทัด shebang จะหน้าตาแบบนี้
#! nix-shell -i python3 -p "python312.withPackages (pkgs: [ pkgs.boto3 pkgs.click ])"ถ้าทำแบบนี้ สิ่งที่ระบบต้องมีมีแค่ Nix เท่านั้น ไม่จำเป็นต้องติดตั้ง Python ไว้ด้วย
ตัวอย่างที่ obvious ที่สุดคือ bash ที่ระบุ dependency ทั้งหมดไว้ และเคยลองทำสคริปต์ Rust แบบไฟล์เดียวที่รันได้เร็วด้วย Nix shebang ด้วย
https://nixos.wiki/wiki/Nix-shell_shebang
nix shellแทนnix-shellเช่นเดียวกับคอมเมนต์อื่น ๆ คำกล่าวว่า “self-contained” นั้นพึ่งพาการที่ติดตั้ง
uvไว้แล้วถ้าต้องการสคริปต์ Python ที่ self-contained จริง ๆ ก็น่าลองดู คอมไพเลอร์ Nuitka
ใช้อยู่ในโปรดักชันของบริการ gRPC โดยไม่มีปัญหา และแค่รัน
nuitka --onefile run.pyก็จบเพราะเป็นคอมไพเลอร์ ไบนารีที่ได้จึงอาจเร็วกว่าตัวโปรแกรม Python เดิมที่แพ็กด้วย PyInstaller ด้วย
หน้า GitHub ของผู้เขียนมีข้อความว่า “ภารกิจชีวิตของผมคือการพยายามจนแก่ตายเพื่อสร้าง Python Compiler ที่ดีที่สุดเท่าที่เป็นไปได้”
https://nuitka.net/
https://github.com/kayhayen
ชอบแพตเทิร์นนี้มากจริง ๆ แต่น่าเสียดายที่ยังทำให้ทำงานกับ LSP ได้อย่างถูกต้องไม่ได้
ใช้ pyright ใน Helix อยู่ และแม้จะเปิดเอดิเตอร์ด้วย
uv run hx script.pyก็ยังไม่ได้จะทำแบบ
uv run --with whatever-it-is-i-need hx script.pyก็ได้ แต่จะเริ่มมีความซ้ำซ้อนมากขึ้นเรื่อย ๆuveหน้าตาไม่สวยที่ทำเองอยู่$ cat ~/.local/bin/uve#!/bin/bashtemp=$(mktemp)uv export --script $1 --no-hashes > $tempuv run --with-requirements $temp vim $1unlink $tempหวังว่าเอดิเตอร์จะรองรับ
uv python find --scriptในเร็ว ๆ นี้ดูมีประโยชน์ทีเดียว
สงสัยว่า uv จะเป็นตัวเลือกที่ปลอดภัยกว่าสำหรับการเผยแพร่โปรเจกต์ที่ใช้ Python ในระยะยาวหรือไม่
นึกถึง rug pull ตอนที่เคยใช้ Anaconda จัดการ dependency เมื่อราว 5 ปีก่อน แล้วต่อมามีกฎเปลี่ยนไป ทำให้ลูกค้าองค์กรที่มีพนักงานเกิน 200 คนไม่สามารถใช้ Anaconda ฟรีได้อีกต่อไป และต้องจ่ายไลเซนส์เชิงพาณิชย์
อาจหยุดพัฒนา หรือย้ายงานในอนาคตไปยัง fork ที่ใช้ไลเซนส์อื่นได้ แต่ไม่สามารถเปลี่ยนไลเซนส์ย้อนหลังได้ ดังนั้นสิ่งที่มีอยู่ตอนนี้จึงรับประกันได้ว่าเป็นโอเพนซอร์ส
ถ้ากังวลจริง ๆ ก็ fork แล้วคอย sync ต่อไปได้
จริง ๆ แล้วเรื่องนี้ใช้ได้กับโปรเจกต์ OSS อื่น ๆ แทบเหมือนกัน จึงไม่คิดว่าน่ากังวลมาก
เท่าที่รู้ conda ไม่เคยเป็นโอเพนซอร์ส และแจกจ่ายเป็นไบนารีมาตลอด
https://github.com/astral-sh/uv?tab=readme-ov-file#license
แพ็กเกจของ conda-forge ยังใช้ได้ฟรีอยู่
uv ใช้ PyPI เฉย ๆ ดังนั้นถ้ามีปัญหา ก็เปลี่ยนจาก uv ไปเป็น pip หรือ Poetry ฯลฯ ได้ และยังดึงแพ็กเกจจากที่เดิมต่อไป
เจ้าของแบบนั้นสุดท้ายก็จะถูกคนรวยที่แย่ที่สุดเท่าที่นึกออกซื้อไป
ลองไล่ดูคู่มือการมีส่วนร่วมและ issue ของ uv แล้ว แต่ไม่เห็น CLA ส่วน PyTorch มี CLA อยู่บนสุดของคู่มือการมีส่วนร่วม
ถึงอย่างนั้นก็ควรมี fork จากชุมชนของ Anaconda เวอร์ชัน FOSS ล่าสุด
Redis เคยเกิดแบบนั้น และ Redis ก็ใช้ CLA: https://github.com/redis/redis/blob/unstable/CONTRIBUTING.md#redis-software-grant-and-contributor-license-agreement
ไม่ควรเซ็น CLA เด็ดขาด
คิดว่าไป contribute ให้เฉพาะโปรเจกต์ copyleft ยังดีกว่า
เราได้รับค่าจ้างมากเกินไปสำหรับการทำงานฟรี
สำหรับยูทิลิตีเล็ก ๆ ดูเหมือนเป็น ทางเลือกด้านแพ็กเกจจิง ที่ดีแทนการทำเป็นคอนเทนเนอร์
คราวนี้ต้องไปโน้มน้าวเพื่อนร่วมงานทุกคนให้ติดตั้ง uv
ดูคล้ายกับ bundler/inline ของฝั่ง Ruby
ดีใจที่ Python ก็มีอะไรคล้าย ๆ กัน และในทางปฏิบัติก็สะดวกมากจริง ๆ
https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html
อยากรู้ว่ามีใครทำให้สิ่งนี้ทำงานบน Windows ได้บ้างไหม
อยากใช้ทริกนี้กับเครื่องมือสำหรับม็อดเกมที่กำลังทำอยู่ แต่ทำให้ทริก shebang ทำงานไม่ได้
$> uv init --script .py$> uv add --script .py ...$> uv add --script .py --dev ...$> uv run .pyหวังว่าจะช่วยได้
ที่มา: https://docs.astral.sh/uv/guides/scripts/
.pypy launcher รองรับบรรทัด shebang
บล็อกโพสต์ในหัวข้อเดียวกันที่เพิ่งลงเมื่อไม่กี่วันก่อนก็พูดถึงเรื่องนี้ และตามนั้นต้องตัด
-Sออก: https://thisdavej.com/share-python-scripts-like-a-pro-uv-and-pep-723-for-easy-deployment/https://news.ycombinator.com/item?id=43500124
ยังไม่ได้ลองเอง แต่เปลี่ยน file association แทน เพื่อให้ไฟล์
.pyทั้งหมดเปิดด้วยuv runเป็นค่าเริ่มต้นhttps://docs.python.org/3/using/windows.html#python-launcher-for-windows
https://peps.python.org/pep-0397/
https://pyinstaller.org
https://onor.io/2025/01/more-scripting-with-racket.html
กรณีใช้งานนี้ทำให้ชอบ uv ขึ้นมา แต่รู้สึกว่าการที่ PEP อย่างเป็นทางการและมีประโยชน์มากไม่ถูกรองรับในเครื่องมือ Python อย่างเป็นทางการนั้นขัดกับ Zen of Python
ในมุมมองของผม นี่เป็นครั้งแรกที่ Python ไม่ได้ “batteries included”
ตอนนี้ในระบบก็มีตัวจัดการ dependency ของ Python สองตัวแล้ว
รู้ว่ามีเรื่องให้พูดได้เยอะเกี่ยวกับการจัดการ dependency ของ Python แต่ตลอดหลายปีที่ผ่านมา ถ้าโปรเจกต์มีแค่
requirements.txtก็อยู่ได้ด้วยpip+venvพื้นฐานจำได้ว่า
pyproject.tomlออกมาก่อนไลบรารีtomllibดังนั้นอยู่หลายเวอร์ชันที่ต้องระบุโมดูลด้วยภาษาที่ Python อ่านเองเป็นค่าเริ่มต้นไม่ได้
เป็นสถานการณ์ที่แย่กว่าการมีวิธีมาตรฐานสำหรับใส่ metadata ที่ไม่ได้ใช้งานเสียอีก
ก็เพราะมันเป็น metadata ตั้งแต่แรกนั่นแหละ ไม่อย่างนั้นก็คงเป็นไวยากรณ์ Python ไปแล้ว