โครงสร้างควบคุมและฟังก์ชัน
UAT 104 การเขียนโปรแกรมคอมพิวเตอร์
บทเรียน
เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ
- เขียนเงื่อนไข if/elif/else และวนซ้ำด้วย for และ while
- ใช้ list, tuple และ dict เก็บข้อมูลภารกิจ
- เขียนฟังก์ชันที่มี docstring ค่าเริ่มต้น และค่าคืนกลับ
- จัดการข้อผิดพลาดด้วย exception อย่างเหมาะสม
- ทดสอบฟังก์ชันด้วย assert และ pytest
ทำไมต้องรู้
โปรแกรมที่มีประโยชน์ต้อง ตัดสินใจ ได้ เช่น แบตเตอรี่ต่ำแล้วต้องเตือน และต้อง ทำซ้ำ ได้ เช่น คำนวณระยะของทุกช่วงในเส้นทาง เมื่อโค้ดยาวขึ้น ต้องแบ่งเป็น ฟังก์ชัน ที่มีหน้าที่ชัด ทดสอบแยกได้ และนำกลับมาใช้ซ้ำ ออโตไพลอตที่ใช้บินจริงก็สร้างจากองค์ประกอบเดียวกันนี้ในระดับที่ใหญ่กว่ามาก
ตัดสินใจด้วย if
ตัวอย่างนี้จำลองการจัดระดับแบตเตอรี่ตามเกณฑ์เริ่มต้นของ PX4 ในรุ่นที่ตรวจ (ต่ำ 15% วิกฤต 7%) ซึ่งเรียนแล้วใน UAT 312 โมดูล 2
def battery_status(percent):
"""Return "OK", "LOW" or "CRITICAL" for a battery percentage (0-100)."""
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
if percent <= 7:
return "CRITICAL"
if percent <= 15:
return "LOW"
return "OK"
for level in [100, 16, 15, 8, 7, 0]:
print(level, battery_status(level))
100 OK
16 OK
15 LOW
8 LOW
7 CRITICAL
0 CRITICAL
ลำดับของเงื่อนไขสำคัญมาก ต้องตรวจ <= 7 ก่อน <= 15 ถ้าสลับกัน ค่า 5% จะได้ “LOW” แทน “CRITICAL” และค่าที่อยู่บนเส้นแบ่งพอดี เช่น 15 และ 7 เป็นจุดที่ต้องทดสอบเสมอ
จัดการข้อผิดพลาด
เมื่อได้ค่าที่เป็นไปไม่ได้ เช่น 120% ฟังก์ชันไม่ควรเดาคำตอบ แต่ควร ยกข้อผิดพลาด (raise exception) ให้ผู้เรียกรู้ แล้วผู้เรียกเลือกวิธีจัดการด้วย try/except
readings = [45, 120, 12]
for reading in readings:
try:
print(reading, battery_status(reading))
except ValueError as err:
print(reading, "rejected:", err)
45 OK
120 rejected: percent must be between 0 and 100
12 LOW
การรับข้อผิดพลาดแบบกว้าง ๆ ด้วย except: เปล่า ๆ แล้วไม่ทำอะไร เป็นนิสัยอันตราย เพราะซ่อนปัญหาจริงไว้ ให้ระบุชนิดของข้อผิดพลาดที่คาดไว้เสมอ
วนซ้ำและโครงสร้างข้อมูล
list เก็บข้อมูลเรียงลำดับที่แก้ไขได้ tuple เก็บข้อมูลที่ไม่ควรเปลี่ยน เช่น พิกัดหนึ่งจุด และ dict เก็บคู่ชื่อกับค่า
ตัวอย่างที่ 1 ความยาวเส้นทางและเวลาบิน
เส้นทางสี่เหลี่ยมขนาด 200 × 100 m ในพิกัดท้องถิ่น (เมตร ตะวันออก, เหนือ) บินที่ 8 m/s
import math
waypoints = [(0, 0), (200, 0), (200, 100), (0, 100), (0, 0)]
drone = {"id": "RSU-QUAD-01", "cruise_ms": 8.0}
total_m = 0.0
for start, end in zip(waypoints, waypoints[1:]):
leg = math.dist(start, end)
total_m += leg
print(f"{start} -> {end}: {leg:.0f} m")
minutes, seconds = divmod(total_m / drone["cruise_ms"], 60)
print(f"total {total_m:.0f} m, {minutes:.0f} min {seconds:.0f} s")
(0, 0) -> (200, 0): 200 m
(200, 0) -> (200, 100): 100 m
(200, 100) -> (0, 100): 200 m
(0, 100) -> (0, 0): 100 m
total 600 m, 1 min 15 s
zip(waypoints, waypoints[1:]) จับคู่จุดที่ติดกันทีละคู่ เป็นรูปแบบที่ใช้บ่อยมากกับข้อมูลเส้นทางและข้อมูลอนุกรมเวลา
ฟังก์ชันที่นำกลับมาใช้ได้
ฟังก์ชันที่ดีทำ งานเดียว มีชื่อบอกหน้าที่ มี docstring อธิบายสิ่งที่รับและคืนตาม PEP 257 และไม่แอบแก้ค่าภายนอก
ตัวอย่างที่ 2 ตรวจว่าจุดบินอยู่ในเขต VTR1 หรือไม่
ใช้ข้อมูลจาก UAT 313 โมดูล 2: VTR1 เป็นวงกลมรัศมี 10 NM รอบ 13°45′54″N 100°32′18″E และจุดบินอยู่ที่ 13°50′00″N 100°40′00″E
EARTH_RADIUS_M = 6_371_000.0
NM_TO_M = 1852
def distance_m(lat1, lon1, lat2, lon2, radius=EARTH_RADIUS_M):
"""Great-circle distance in metres between two points given in degrees."""
phi1, phi2 = math.radians(lat1), math.radians(lat2)
d_phi = phi2 - phi1
d_lambda = math.radians(lon2 - lon1)
a = math.sin(d_phi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2
return 2 * radius * math.asin(math.sqrt(a))
def dms_to_deg(degrees, minutes, seconds):
"""Convert degrees, minutes and seconds to decimal degrees."""
return degrees + minutes / 60 + seconds / 3600
vtr1_centre = (dms_to_deg(13, 45, 54), dms_to_deg(100, 32, 18))
site = (dms_to_deg(13, 50, 0), dms_to_deg(100, 40, 0))
d = distance_m(*vtr1_centre, *site)
print(f"{d / 1000:.1f} km, inside VTR1: {d <= 10 * NM_TO_M}")
15.8 km, inside VTR1: True
ผลตรงกับที่คำนวณไว้ใน UAT 313 เครื่องหมาย * หน้า tuple แตกค่าออกเป็นอาร์กิวเมนต์ทีละตัว และ 6_371_000.0 ใช้ขีดล่างคั่นหลักให้อ่านง่าย
ทดสอบฟังก์ชัน
assert ตรวจว่าเงื่อนไขเป็นจริง ถ้าไม่จริงโปรแกรมจะหยุดและแจ้งทันที ใช้กับค่าที่รู้คำตอบ รวมถึงค่าบนเส้นแบ่งและค่าที่ผิดปกติ
assert battery_status(16) == "OK"
assert battery_status(15) == "LOW"
assert battery_status(7) == "CRITICAL"
assert math.isclose(distance_m(0, 0, 0, 1), 111_194.9, abs_tol=0.1)
try:
battery_status(-1)
except ValueError:
print("all tests passed")
all tests passed
เมื่อโครงการใหญ่ขึ้น ให้ย้ายการทดสอบไปไว้ในไฟล์ test_*.py แล้วรันด้วย pytest ซึ่งติดตั้งมากับ requirements.txt แล้ว
# test_battery.py
import pytest
from mission_tools import battery_status
def test_boundaries():
assert battery_status(15) == "LOW"
assert battery_status(7) == "CRITICAL"
def test_rejects_impossible_values():
with pytest.raises(ValueError):
battery_status(120)
รันด้วยคำสั่ง python -m pytest ในโฟลเดอร์งาน
ปฏิบัติการประจำโมดูล
ปฏิบัติการ: ไลบรารีเครื่องมือภารกิจ
- สร้างไฟล์
mission_tools.pyที่มีฟังก์ชันbattery_status(),distance_m()และroute_length_m()ซึ่งรับ list ของพิกัดละติจูดลองจิจูด พร้อม docstring ทุกฟังก์ชัน - เขียน
test_mission_tools.pyที่ทดสอบค่าบนเส้นแบ่ง ค่าผิดปกติ และค่าที่รู้คำตอบ (ระยะ 1 องศาลองจิจูดบนเส้นศูนย์สูตร 111 194.9 m) แล้วรัน pytest ให้ผ่านทั้งหมด - เขียนฟังก์ชันตรวจว่า waypoint ทุกจุดห่างจุดขึ้นบินไม่เกินรัศมี geofence ที่กำหนด แล้วรายงานจุดที่เกิน
- แลกไฟล์ทดสอบกับเพื่อนอีกกลุ่ม แล้วรันกับโค้ดของเพื่อน บันทึกกรณีที่ทำให้โค้ดของอีกฝ่ายผิด
ข้อผิดพลาดที่พบบ่อย
ระวัง
- เรียงเงื่อนไขผิดลำดับ จนเงื่อนไขที่แคบกว่าไม่เคยถูกตรวจ
- ไม่ทดสอบค่าบนเส้นแบ่ง เช่น 15 พอดี
- ใช้
except:เปล่า ๆ จนซ่อนข้อผิดพลาดจริง - ใส่ list เป็นค่าเริ่มต้นของพารามิเตอร์ เช่น
def f(points=[])ซึ่งค่าจะค้างข้ามการเรียก ให้ใช้Noneแล้วสร้าง list ใหม่ในฟังก์ชัน - ใส่องศาในฟังก์ชันตรีโกณมิติ ที่ต้องการเรเดียน
สรุป
if/elif/elseใช้ตัดสินใจ ลำดับเงื่อนไขมีผลต่อคำตอบforวนตามข้อมูล ส่วนzip(a, a[1:])จับคู่ข้อมูลที่ติดกัน- list, tuple และ dict เก็บข้อมูลภารกิจคนละลักษณะ
- ฟังก์ชันที่ดีทำงานเดียว มี docstring และยกข้อผิดพลาดเมื่อได้ค่าที่เป็นไปไม่ได้
- ทดสอบด้วย
assertและ pytest โดยเน้นค่าบนเส้นแบ่งและค่าที่รู้คำตอบ
แบบฝึกตรวจความเข้าใจ
battery_status(15)คืนค่าอะไร เพราะอะไร- ถ้าสลับให้ตรวจ
percent <= 15ก่อนpercent <= 7ค่า 5 จะได้ผลอะไร list(zip([1, 2, 3], [2, 3]))ได้ผลอะไร- เส้นทาง (0, 0) → (30, 40) → (30, 100) ยาวกี่เมตร
- ทำไมจึงไม่ควรเขียน
def add_point(points=[])
เฉลย
"LOW"เพราะ 15 ไม่เกิน 15 แต่เกิน 7"LOW"ซึ่งผิด เพราะเงื่อนไขแรกจับไปก่อนจะถึงเงื่อนไขวิกฤต[(1, 2), (2, 3)]- m
- list ค่าเริ่มต้นถูกสร้างครั้งเดียวและใช้ร่วมกันทุกครั้งที่เรียก ค่าจากการเรียกครั้งก่อนจึงค้างอยู่
สรุปสูตรสำคัญ
| ระยะทางบนผิวโลก (haversine) | |
| ความยาวเส้นทางหลาย waypoint |
แหล่งอ้างอิงหลัก
- Python Software Foundation. The Python tutorial (Python 3.14 documentation). link
- van Rossum, G., Warsaw, B., & Coghlan, A. (2001). PEP 8 – Style guide for Python code. link
- Goodger, D., & van Rossum, G. (2001). PEP 257 – Docstring conventions. link
- Downey, A. B. (2024). Think Python: How to think like a computer scientist (3rd ed.). O’Reilly. link
- Matthes, E. (2023). Python crash course (3rd ed.). No Starch Press.
- PX4 Autopilot. PX4 user and developer guide. link
อ่านเพิ่มเติม
ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล
ในชั้นเรียน / ภาคสนาม
ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย
หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz