วันนี้เราขอนำเสนอส่วนแรกของการแปลเนื้อหาเกี่ยวกับวิธีที่ Dropbox จัดการกับการควบคุมประเภทของโค้ด Python
Dropbox เขียนอะไรมากมายใน Python เป็นภาษาที่เราใช้กันอย่างแพร่หลายมาก ทั้งสำหรับบริการส่วนหลังและแอปพลิเคชันไคลเอนต์เดสก์ท็อป นอกจากนี้เรายังใช้ Go, TypeScript และ Rust เป็นจำนวนมาก แต่ Python เป็นภาษาหลักของเรา เมื่อพิจารณาขนาดของเราและเรากำลังพูดถึงโค้ด Python หลายล้านบรรทัด ปรากฎว่าการพิมพ์แบบไดนามิกของโค้ดดังกล่าวทำให้ความเข้าใจซับซ้อนโดยไม่จำเป็นและเริ่มส่งผลกระทบต่อผลิตภาพแรงงานอย่างจริงจัง เพื่อลดปัญหานี้ เราได้เริ่มเปลี่ยนโค้ดของเราเป็นการตรวจสอบประเภทคงที่โดยใช้ mypy อย่างค่อยเป็นค่อยไป นี่อาจเป็นระบบตรวจสอบประเภทแบบสแตนด์อโลนที่ได้รับความนิยมมากที่สุดสำหรับ Python Mypy เป็นโครงการโอเพ่นซอร์ส นักพัฒนาหลักทำงานใน Dropbox
Dropbox เป็นหนึ่งในบริษัทแรกๆ ที่ใช้การตรวจสอบประเภทแบบสแตติกในโค้ด Python ในระดับนี้ Mypy ถูกใช้ในโครงการนับพันในปัจจุบัน เครื่องมือนี้นับครั้งไม่ถ้วนตามที่พวกเขากล่าวว่า "ทดสอบในสนามรบ" เรามาไกลเพื่อมาถึงจุดที่เราอยู่ตอนนี้ ระหว่างทาง มีการทดลองที่ไม่สำเร็จและล้มเหลวมากมาย โพสต์นี้ครอบคลุมประวัติของการตรวจสอบประเภทแบบสแตติกใน Python ตั้งแต่จุดเริ่มต้นที่ยากลำบากซึ่งเป็นส่วนหนึ่งของโครงการวิจัยของฉันจนถึงปัจจุบัน เมื่อการตรวจสอบประเภทและการบอกใบ้ประเภทกลายเป็นเรื่องธรรมดาสำหรับนักพัฒนาจำนวนนับไม่ถ้วนที่เขียนด้วย Python ขณะนี้กลไกเหล่านี้ได้รับการสนับสนุนโดยเครื่องมือมากมาย เช่น IDE และตัววิเคราะห์โค้ด
ทำไมการตรวจสอบประเภทจึงจำเป็น
หากคุณเคยใช้ Python ที่พิมพ์แบบไดนามิก คุณอาจมีความสับสนว่าเหตุใดจึงมีความยุ่งยากเกี่ยวกับการพิมพ์แบบคงที่และ mypy เมื่อเร็ว ๆ นี้ หรือบางทีคุณอาจชอบ Python เพราะการพิมพ์แบบไดนามิก และสิ่งที่เกิดขึ้นก็ทำให้คุณอารมณ์เสีย กุญแจสำคัญของค่าของการพิมพ์แบบคงที่คือขนาดของโซลูชัน ยิ่งโปรเจ็กต์ของคุณมีขนาดใหญ่เท่าใด คุณก็ยิ่งเอนเอียงไปทางการพิมพ์แบบคงที่มากขึ้นเท่านั้น และท้ายที่สุด คุณก็ยิ่งต้องการมันมากเท่านั้น
สมมติว่าโครงการหนึ่งมีขนาดถึงหมื่นบรรทัดและปรากฎว่ามีโปรแกรมเมอร์หลายคนกำลังทำงานอยู่ เมื่อพิจารณาจากโครงการที่คล้ายกัน จากประสบการณ์ของเรา เราสามารถพูดได้ว่าการทำความเข้าใจโค้ดจะเป็นกุญแจสำคัญที่ทำให้นักพัฒนามีประสิทธิผล หากไม่มีคำอธิบายประกอบประเภท อาจเป็นเรื่องยากที่จะเข้าใจ ตัวอย่างเช่น อาร์กิวเมนต์ใดที่จะส่งไปยังฟังก์ชัน หรือประเภทใดที่ฟังก์ชันสามารถส่งคืนได้ ต่อไปนี้คือคำถามทั่วไปที่มักจะตอบได้ยากหากไม่ใช้คำอธิบายประกอบประเภท:
- ฟังก์ชันนี้คืนค่าได้ไหม
None
? - ข้อโต้แย้งนี้ควรเป็นอย่างไร
items
? - ประเภทแอตทริบิวต์คืออะไร
id
:int
ใช่ไหม,str
หรืออาจเป็นประเภทที่กำหนดเอง - อาร์กิวเมนต์นี้ควรเป็นรายการหรือไม่ เป็นไปได้ไหมที่จะส่งทูเพิลไป?
หากคุณดูข้อมูลโค้ดที่มีคำอธิบายประกอบประเภทต่อไปนี้และพยายามตอบคำถามที่คล้ายกัน ปรากฎว่านี่เป็นงานที่ง่ายที่สุด:
class Resource:
id: bytes
...
def read_metadata(self,
items: Sequence[str]) -> Dict[str, MetadataItem]:
...
read_metadata
ไม่กลับมาNone
เนื่องจากประเภทการส่งคืนไม่ใช่Optional[…]
.- อาร์กิวเมนต์
items
เป็นลำดับของบรรทัด ไม่สามารถทำซ้ำแบบสุ่มได้ - คุณลักษณะ
id
เป็นสตริงของไบต์
ในโลกอุดมคติ เราคาดหวังว่ารายละเอียดปลีกย่อยดังกล่าวทั้งหมดจะได้รับการอธิบายไว้ในเอกสารประกอบ (docstring) ที่มีมาให้ภายใน แต่ประสบการณ์ให้ตัวอย่างมากมายเกี่ยวกับความจริงที่ว่าเอกสารดังกล่าวมักไม่ได้รับการสังเกตในรหัสที่คุณต้องทำงาน แม้ว่าเอกสารดังกล่าวจะมีอยู่ในรหัส แต่ก็ไม่สามารถไว้วางใจในความถูกต้องสมบูรณ์ได้ เอกสารนี้อาจคลุมเครือ ไม่ถูกต้อง และเปิดโอกาสให้เกิดความเข้าใจผิดได้ ในทีมขนาดใหญ่หรือโครงการขนาดใหญ่ ปัญหานี้อาจรุนแรงมาก
แม้ว่า Python จะเก่งในขั้นเริ่มต้นหรือขั้นกลางของโครงการ แต่ในบางจุดโครงการที่ประสบความสำเร็จและบริษัทที่ใช้ Python อาจเผชิญกับคำถามที่สำคัญ: "เราควรเขียนทุกอย่างใหม่ด้วยภาษาที่พิมพ์แบบคงที่หรือไม่"
ระบบการตรวจสอบประเภทเช่น mypy แก้ปัญหาข้างต้นโดยการจัดหาภาษาที่เป็นทางการสำหรับนักพัฒนาเพื่ออธิบายประเภท และโดยการตรวจสอบว่าการประกาศประเภทนั้นตรงกับการใช้งานโปรแกรม (และอาจเป็นทางเลือกโดยการตรวจสอบการมีอยู่) โดยทั่วไป เราสามารถพูดได้ว่าระบบเหล่านี้มีบางอย่างเช่นการตรวจสอบเอกสารอย่างละเอียดถี่ถ้วน
การใช้ระบบดังกล่าวมีข้อดีอื่น ๆ และไม่ใช่เรื่องเล็กน้อย:
- ระบบตรวจสอบประเภทสามารถตรวจจับข้อผิดพลาดเล็กๆ น้อยๆ (และไม่เล็กมาก) ได้ ตัวอย่างทั่วไปคือเมื่อพวกเขาลืมประมวลผลค่า
None
หรือเงื่อนไขพิเศษอื่นๆ - การปรับโครงสร้างโค้ดทำได้ง่ายมากเนื่องจากระบบตรวจสอบประเภทมักจะแม่นยำมากเกี่ยวกับโค้ดที่ต้องเปลี่ยน ในขณะเดียวกัน เราไม่จำเป็นต้องหวังว่าจะครอบคลุมการทดสอบโค้ด 100% ซึ่งโดยปกติแล้วจะไม่สามารถทำได้ไม่ว่าในกรณีใด เราไม่จำเป็นต้องเจาะลึกลงไปในการติดตามสแต็กเพื่อค้นหาสาเหตุของปัญหา
- แม้แต่ในโครงการขนาดใหญ่ mypy มักจะทำการตรวจสอบแบบเต็มได้ในเสี้ยววินาที และการดำเนินการทดสอบมักใช้เวลาหลายสิบวินาทีหรือแม้แต่นาที ระบบตรวจสอบประเภทให้ข้อเสนอแนะแก่โปรแกรมเมอร์ทันทีและช่วยให้เขาทำงานได้เร็วขึ้น เขาไม่จำเป็นต้องเขียนการทดสอบหน่วยที่เปราะบางและยากอีกต่อไป ซึ่งแทนที่เอนทิตีจริงด้วยการจำลองและแพตช์เพียงเพื่อให้ได้ผลการทดสอบโค้ดเร็วขึ้น
IDE และเอดิเตอร์ เช่น PyCharm หรือ Visual Studio Code ใช้พลังของคำอธิบายประกอบประเภทเพื่อให้นักพัฒนาโค้ดสมบูรณ์ การเน้นข้อผิดพลาด และการสนับสนุนสำหรับโครงสร้างภาษาที่ใช้กันทั่วไป และนี่เป็นเพียงประโยชน์บางประการของการพิมพ์ สำหรับโปรแกรมเมอร์บางคน ทั้งหมดนี้เป็นข้อโต้แย้งหลักที่สนับสนุนการพิมพ์ นี่คือสิ่งที่ได้ประโยชน์ทันทีหลังการนำไปใช้ กรณีการใช้งานสำหรับประเภทนี้ไม่ต้องการระบบตรวจสอบประเภทแยกต่างหากเช่น mypy แม้ว่าควรสังเกตว่า mypy ช่วยให้คำอธิบายประกอบประเภทสอดคล้องกับรหัส
ภูมิหลังของ mypy
ประวัติของ mypy เริ่มต้นขึ้นในสหราชอาณาจักร ในเคมบริดจ์ ไม่กี่ปีก่อนที่ฉันจะเข้าร่วม Dropbox ฉันได้มีส่วนร่วม ซึ่งเป็นส่วนหนึ่งของการวิจัยระดับปริญญาเอกของฉัน ในการรวมภาษาแบบสแตติกและไดนามิกเข้าด้วยกัน ฉันได้รับแรงบันดาลใจจากบทความเกี่ยวกับการพิมพ์ที่เพิ่มขึ้นโดย Jeremy Siek และ Walid Taha และจากโครงการ Typed Racket ฉันพยายามค้นหาวิธีใช้ภาษาโปรแกรมเดียวกันสำหรับโปรเจ็กต์ต่างๆ ตั้งแต่สคริปต์ขนาดเล็กไปจนถึงโค้ดเบสที่ประกอบด้วยหลายล้านบรรทัด ในเวลาเดียวกัน ฉันต้องการให้แน่ใจว่าในโครงการขนาดใดก็ตาม จะไม่มีการประนีประนอมมากเกินไป ส่วนสำคัญของทั้งหมดนี้คือแนวคิดในการค่อยๆ ย้ายจากโครงการต้นแบบที่ไม่ได้พิมพ์ไปเป็นผลิตภัณฑ์สำเร็จรูปที่พิมพ์แบบคงที่ที่ผ่านการทดสอบอย่างครอบคลุม ทุกวันนี้ แนวคิดเหล่านี้มักถูกมองข้ามไป แต่ในปี 2010 ปัญหานี้ยังคงได้รับการสำรวจอย่างต่อเนื่อง
งานเดิมของฉันในการตรวจสอบประเภทไม่ได้มุ่งเป้าไปที่ Python แต่ฉันใช้ภาษา "โฮมเมด" เล็กน้อยแทน
def Fib(n as Int) as Int
if n <= 1
return n
else
return Fib(n - 1) + Fib(n - 2)
end
end
การใช้ภาษาพื้นเมืองที่เรียบง่ายเป็นวิธีการทั่วไปที่ใช้ในการวิจัยทางวิทยาศาสตร์ ไม่น้อยเพราะมันช่วยให้คุณทำการทดลองได้อย่างรวดเร็วและเนื่องจากความจริงที่ว่าสิ่งที่ไม่เกี่ยวข้องกับการศึกษาสามารถถูกเพิกเฉยได้ง่าย ภาษาการเขียนโปรแกรมในโลกแห่งความเป็นจริงมักจะเป็นปรากฏการณ์ขนาดใหญ่ที่มีการใช้งานที่ซับซ้อน และทำให้การทดลองช้าลง อย่างไรก็ตาม ผลลัพธ์ใด ๆ ที่อิงจากภาษาที่เรียบง่ายนั้นดูน่าสงสัยเล็กน้อย เนื่องจากในการได้รับผลลัพธ์เหล่านี้ ผู้วิจัยอาจเสียสละการพิจารณาที่สำคัญสำหรับการใช้ภาษาจริง
ตัวตรวจสอบประเภทของฉันสำหรับ Alore ดูดีมาก แต่ฉันต้องการทดสอบโดยการทดลองกับโค้ดจริง ซึ่งคุณอาจพูดได้ว่าไม่ได้เขียนด้วยภาษา Alore โชคดีสำหรับฉัน ภาษา Alore ส่วนใหญ่มาจากแนวคิดเดียวกันกับ Python การสร้างตัวตรวจสอบตัวพิมพ์ใหม่นั้นง่ายพอที่จะทำงานร่วมกับไวยากรณ์และความหมายของ Python ได้ สิ่งนี้ทำให้เราลองตรวจสอบประเภทในโค้ด Python แบบโอเพ่นซอร์ส นอกจากนี้ ฉันได้เขียน transpiler เพื่อแปลงโค้ดที่เขียนด้วย Alore เป็นโค้ด Python และใช้มันเพื่อแปลโค้ดตัวตรวจสอบตัวพิมพ์ของฉัน ตอนนี้ฉันมีระบบตรวจสอบประเภทที่เขียนด้วย Python ที่รองรับเซ็ตย่อยของ Python ซึ่งเป็นภาษาประเภทนั้น! (การตัดสินใจทางสถาปัตยกรรมบางอย่างที่สมเหตุสมผลสำหรับ Alore นั้นไม่เหมาะสมสำหรับ Python และสิ่งนี้ยังคงสังเกตเห็นได้ในส่วนของ mypy codebase)
อันที่จริง ภาษาที่รองรับโดยระบบประเภทของฉันไม่สามารถเรียกว่า Python ได้ในตอนนี้: มันเป็นตัวแปรของ Python เนื่องจากข้อจำกัดบางประการของไวยากรณ์คำอธิบายประกอบประเภท Python 3
ดูเหมือนว่าเป็นส่วนผสมของ Java และ Python:
int fib(int n):
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
หนึ่งในความคิดของฉันในเวลานั้นคือการใช้คำอธิบายประกอบประเภทเพื่อปรับปรุงประสิทธิภาพโดยการรวบรวม Python ประเภทนี้ไปยัง C หรืออาจเป็น JVM bytecode ฉันมาถึงขั้นตอนของการเขียนต้นแบบคอมไพเลอร์ แต่ฉันละทิ้งแนวคิดนี้ เนื่องจากการตรวจสอบประเภทนั้นดูมีประโยชน์ทีเดียว
ฉันจบลงด้วยการนำเสนอโครงการของฉันที่ PyCon 2013 ในซานตาคลารา ฉันได้พูดคุยเกี่ยวกับเรื่องนี้กับ Guido van Rossum เผด็จการ Python ที่ใจดีต่อชีวิต เขาโน้มน้าวให้ฉันเลิกใช้ไวยากรณ์ของตัวเองและใช้ไวยากรณ์ Python 3 มาตรฐานต่อไป Python 3 รองรับคำอธิบายประกอบของฟังก์ชัน ดังนั้น ตัวอย่างของฉันสามารถเขียนใหม่ได้ดังที่แสดงด้านล่าง ส่งผลให้โปรแกรม Python ปกติ:
def fib(n: int) -> int:
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
ฉันต้องประนีประนอมบางอย่าง (ก่อนอื่น ฉันต้องการทราบว่าฉันคิดค้นไวยากรณ์ของตัวเองด้วยเหตุผลนี้เอง) โดยเฉพาะอย่างยิ่ง Python 3.3 ซึ่งเป็นเวอร์ชันล่าสุดของภาษาในขณะนั้น ไม่รองรับคำอธิบายประกอบแบบแปรผัน ฉันได้พูดคุยกับ Guido ทางอีเมลเกี่ยวกับความเป็นไปได้ต่างๆ สำหรับการออกแบบวากยสัมพันธ์ของคำอธิบายประกอบดังกล่าว เราตัดสินใจใช้ความคิดเห็นประเภทสำหรับตัวแปร สิ่งนี้เป็นไปตามจุดประสงค์ แต่ค่อนข้างยุ่งยาก (Python 3.6 ทำให้เรามีไวยากรณ์ที่ดีกว่า):
products = [] # type: List[str] # Eww
ความคิดเห็นประเภทยังมีประโยชน์ในการรองรับ Python 2 ซึ่งไม่รองรับคำอธิบายประกอบประเภทในตัว:
f fib(n):
# type: (int) -> int
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
ปรากฎว่าการแลกเปลี่ยน (และอื่น ๆ ) เหล่านี้ไม่สำคัญ - ประโยชน์ของการพิมพ์แบบคงที่หมายความว่าในไม่ช้าผู้ใช้จะลืมเกี่ยวกับไวยากรณ์ที่น้อยกว่าอุดมคติ เนื่องจากไม่มีการใช้โครงสร้างวากยสัมพันธ์พิเศษในโค้ด Python ที่ตรวจสอบประเภท เครื่องมือ Python ที่มีอยู่และกระบวนการประมวลผลโค้ดจึงทำงานต่อไปได้ตามปกติ ทำให้นักพัฒนาสามารถเรียนรู้เครื่องมือใหม่ได้ง่ายขึ้นมาก
Guido ยังโน้มน้าวให้ฉันเข้าร่วม Dropbox หลังจากที่ฉันทำวิทยานิพนธ์ระดับบัณฑิตศึกษาเสร็จ นี่คือจุดเริ่มต้นของเรื่องราว Mypy ที่น่าสนใจที่สุด
จะยังคง ...
เรียนผู้อ่าน! หากคุณใช้ Python โปรดบอกเราเกี่ยวกับขนาดของโครงการที่คุณพัฒนาในภาษานี้
ที่มา: will.com