โมดูล 2/5 · สัปดาห์ 4–6 · 27 ชม.

โครงสร้างควบคุมและฟังก์ชัน

UAT 104 การเขียนโปรแกรมคอมพิวเตอร์

เวลาเรียนประมาณ 90 นาทีร่าง รอตรวจปรับปรุงล่าสุด 26 กันยายน 2569

บทเรียน

เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ

  1. เขียนเงื่อนไข if/elif/else และวนซ้ำด้วย for และ while
  2. ใช้ list, tuple และ dict เก็บข้อมูลภารกิจ
  3. เขียนฟังก์ชันที่มี docstring ค่าเริ่มต้น และค่าคืนกลับ
  4. จัดการข้อผิดพลาดด้วย exception อย่างเหมาะสม
  5. ทดสอบฟังก์ชันด้วย assert และ pytest

ความรู้พื้นฐานที่ควรมี: UAT 104 โมดูล 1

ทำไมต้องรู้

โปรแกรมที่มีประโยชน์ต้อง ตัดสินใจ ได้ เช่น แบตเตอรี่ต่ำแล้วต้องเตือน และต้อง ทำซ้ำ ได้ เช่น คำนวณระยะของทุกช่วงในเส้นทาง เมื่อโค้ดยาวขึ้น ต้องแบ่งเป็น ฟังก์ชัน ที่มีหน้าที่ชัด ทดสอบแยกได้ และนำกลับมาใช้ซ้ำ ออโตไพลอตที่ใช้บินจริงก็สร้างจากองค์ประกอบเดียวกันนี้ในระดับที่ใหญ่กว่ามาก

ตัดสินใจด้วย if

ตัวอย่างนี้จำลองการจัดระดับแบตเตอรี่ตามเกณฑ์เริ่มต้นของ PX4 ในรุ่นที่ตรวจ (ต่ำ 15% วิกฤต 7%) ซึ่งเรียนแล้วใน UAT 312 โมดูล 2

ผังงาน รับค่า percent ตรวจว่าอยู่ระหว่าง 0 ถึง 100 หรือไม่ ถ้าไม่ใช่เกิด ValueError ถ้าใช่ตรวจว่าไม่เกิน 7 ได้ CRITICAL ถ้าไม่ใช่ตรวจว่าไม่เกิน 15 ได้ LOW มิฉะนั้นได้ OK
ภาพที่ 1 ผังงานของฟังก์ชัน battery_status
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 และไม่แอบแก้ค่าภายนอก

ค่าเข้าสามกลุ่ม คือ lat1 lon1 lat2 lon2 และ radius ที่มีค่าเริ่มต้น 6 371 000 เมตร เข้าสู่ฟังก์ชัน distance_m ที่ใช้สูตร haversine และคืนระยะทางเป็นเมตร ใต้ฟังก์ชันมี docstring และการทดสอบ
ภาพที่ 2 ฟังก์ชันเป็นกล่องที่รับค่าเข้าและคืนผลลัพธ์

ตัวอย่างที่ 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 ในโฟลเดอร์งาน

ปฏิบัติการประจำโมดูล

ปฏิบัติการ: ไลบรารีเครื่องมือภารกิจ

  1. สร้างไฟล์ mission_tools.py ที่มีฟังก์ชัน battery_status(), distance_m() และ route_length_m() ซึ่งรับ list ของพิกัดละติจูดลองจิจูด พร้อม docstring ทุกฟังก์ชัน
  2. เขียน test_mission_tools.py ที่ทดสอบค่าบนเส้นแบ่ง ค่าผิดปกติ และค่าที่รู้คำตอบ (ระยะ 1 องศาลองจิจูดบนเส้นศูนย์สูตร 111 194.9 m) แล้วรัน pytest ให้ผ่านทั้งหมด
  3. เขียนฟังก์ชันตรวจว่า waypoint ทุกจุดห่างจุดขึ้นบินไม่เกินรัศมี geofence ที่กำหนด แล้วรายงานจุดที่เกิน
  4. แลกไฟล์ทดสอบกับเพื่อนอีกกลุ่ม แล้วรันกับโค้ดของเพื่อน บันทึกกรณีที่ทำให้โค้ดของอีกฝ่ายผิด

ข้อผิดพลาดที่พบบ่อย

ระวัง

  • เรียงเงื่อนไขผิดลำดับ จนเงื่อนไขที่แคบกว่าไม่เคยถูกตรวจ
  • ไม่ทดสอบค่าบนเส้นแบ่ง เช่น 15 พอดี
  • ใช้ except: เปล่า ๆ จนซ่อนข้อผิดพลาดจริง
  • ใส่ list เป็นค่าเริ่มต้นของพารามิเตอร์ เช่น def f(points=[]) ซึ่งค่าจะค้างข้ามการเรียก ให้ใช้ None แล้วสร้าง list ใหม่ในฟังก์ชัน
  • ใส่องศาในฟังก์ชันตรีโกณมิติ ที่ต้องการเรเดียน

สรุป

  • if/elif/else ใช้ตัดสินใจ ลำดับเงื่อนไขมีผลต่อคำตอบ
  • for วนตามข้อมูล ส่วน zip(a, a[1:]) จับคู่ข้อมูลที่ติดกัน
  • list, tuple และ dict เก็บข้อมูลภารกิจคนละลักษณะ
  • ฟังก์ชันที่ดีทำงานเดียว มี docstring และยกข้อผิดพลาดเมื่อได้ค่าที่เป็นไปไม่ได้
  • ทดสอบด้วย assert และ pytest โดยเน้นค่าบนเส้นแบ่งและค่าที่รู้คำตอบ

แบบฝึกตรวจความเข้าใจ

  1. battery_status(15) คืนค่าอะไร เพราะอะไร
  2. ถ้าสลับให้ตรวจ percent <= 15 ก่อน percent <= 7 ค่า 5 จะได้ผลอะไร
  3. list(zip([1, 2, 3], [2, 3])) ได้ผลอะไร
  4. เส้นทาง (0, 0) → (30, 40) → (30, 100) ยาวกี่เมตร
  5. ทำไมจึงไม่ควรเขียน def add_point(points=[])
เฉลย
  1. "LOW" เพราะ 15 ไม่เกิน 15 แต่เกิน 7
  2. "LOW" ซึ่งผิด เพราะเงื่อนไขแรกจับไปก่อนจะถึงเงื่อนไขวิกฤต
  3. [(1, 2), (2, 3)]
  4. m
  5. list ค่าเริ่มต้นถูกสร้างครั้งเดียวและใช้ร่วมกันทุกครั้งที่เรียก ค่าจากการเรียกครั้งก่อนจึงค้างอยู่

สรุปสูตรสำคัญ

ระยะทางบนผิวโลก (haversine)
ความยาวเส้นทางหลาย waypoint

แหล่งอ้างอิงหลัก

  1. Python Software Foundation. The Python tutorial (Python 3.14 documentation). link
  2. van Rossum, G., Warsaw, B., & Coghlan, A. (2001). PEP 8 – Style guide for Python code. link
  3. Goodger, D., & van Rossum, G. (2001). PEP 257 – Docstring conventions. link
  4. Downey, A. B. (2024). Think Python: How to think like a computer scientist (3rd ed.). O’Reilly. link
  5. Matthes, E. (2023). Python crash course (3rd ed.). No Starch Press.
  6. PX4 Autopilot. PX4 user and developer guide. link

อ่านเพิ่มเติม

ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล

ในชั้นเรียน / ภาคสนาม

ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย

หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz

แบบทดสอบประจำโมดูล

แบบทดสอบนี้ใช้ตรวจความเข้าใจ (formative) ไม่ใช่การสอบเก็บคะแนน

โดเมนความรู้: การเขียนโปรแกรมและเทคโนโลยีดิจิทัล