Python จากศูนย์สู่มืออาชีพ

คู่มือเรียนรู้ด้วยตนเองสำหรับการคิด ออกแบบ พัฒนา ทดสอบ และส่งมอบโปรแกรมจริง

ผู้เขียน: TEERAPOL KITTIKANJANARUK

วิธีใช้คู่มือนี้

คู่มือนี้ไม่ได้วัดความก้าวหน้าจากจำนวนหน้าที่อ่าน แต่ดูจากสิ่งที่ผู้อ่านสร้างและอธิบายได้ ในทุกบท ให้ทำตามลำดับนี้:

  1. อ่านเป้าหมายและดูผลลัพธ์ที่ต้องสร้าง
  2. พิมพ์โค้ดด้วยตนเอง อย่าเพียงคัดลอก
  3. รันโปรแกรมและสังเกตผลลัพธ์
  4. ลองป้อนข้อมูลที่ทำให้เกิดข้อผิดพลาดอย่างน้อยหนึ่งครั้ง แล้วอ่าน traceback
  5. ทำแบบฝึกหัดโดยไม่เปิดเฉลย
  6. ทำ checkpoint ให้ผ่านก่อนเริ่มบทถัดไป

เมื่อเห็นคำว่า “หยุดตรวจสอบ” ให้ปิดคู่มือแล้วอธิบายแนวคิดด้วยภาษาของตนเอง หากอธิบายไม่ได้ ให้ย้อนกลับไปทำตัวอย่างที่เล็กลง

เส้นทางทั้งหมด

flowchart LR
    A[ระดับ 0\nเริ่มเขียน] --> B[ระดับ 1\nคิดและเขียนพื้นฐาน]
    B --> C[ระดับ 2\nไฟล์และฐานข้อมูล]
    C --> D[ระดับ 3\nคุณภาพและการทดสอบ]
    D --> E[ระดับ 4\nPython ขั้นสูง]
    E --> F[ระดับ 5\nAlgorithms และระบบ]
    F --> G[ระดับ 6\nProfessional Engineering]
    G --> H[ระดับ 7\nสายเชี่ยวชาญ]
    H --> I[ระดับ 8\nCapstone พร้อมทำงานจริง]

เล่มนี้เริ่มต้นที่ระดับ 0 และระดับ 1 ส่วนระดับต่อไปจะต่อยอดจากโปรเจกต์เดียวกัน เพื่อไม่ให้ความรู้แยกเป็นคำสั่งที่จำโดยไม่เห็นภาพรวม


ระดับ 0: เริ่มเขียนโปรแกรม

บทที่ 0.1 โปรแกรมคือชุดคำสั่ง

โปรแกรมคือคำสั่งที่เราเขียนให้คอมพิวเตอร์ทำตามลำดับ คอมพิวเตอร์ไม่ได้เดาเจตนาแทนเรา คำสั่งต้องชัดเจนและอยู่ในรูปแบบที่ภาษาเข้าใจได้

การทำงานพื้นฐานมีลักษณะดังนี้:

flowchart LR
    I[ข้อมูลนำเข้า\nInput] --> P[โปรแกรม\nProcess: ประมวลผล]
    P --> O[ผลลัพธ์\nOutput]

ตัวอย่าง “หารค่าอาหาร”:

  • Input (ข้อมูลนำเข้า): ราคาอาหารและจำนวนคน
  • Process (การประมวลผล): ราคาอาหาร ÷ จำนวนคน
  • Output (ผลลัพธ์): ค่าอาหารต่อคน

บทที่ 0.2 รันโปรแกรมแรก

สร้างไฟล์ชื่อ hello.py แล้วเขียน:

print("สวัสดี Python")

รันด้วยคำสั่ง:

python hello.py

ถ้าเครื่องของคุณใช้คำสั่ง python3 ให้ใช้:

python3 hello.py

ผลลัพธ์ควรเป็น:

สวัสดี Python

print() เป็นฟังก์ชันสำหรับแสดงข้อมูลออกทางหน้าจอ เครื่องหมายคำพูดบอกว่าเนื้อหาภายในเป็นข้อความ หากเขียน print(สวัสดี Python) โดยไม่มีเครื่องหมายคำพูด Python จะคิดว่านั่นคือชื่อของตัวแปรและแจ้งข้อผิดพลาด

บทที่ 0.3 เปลี่ยนโค้ดแล้วสังเกตผล

ลองแก้เป็น:

print("ฉันกำลังหัดเขียนโปรแกรม")
print("โปรแกรมทำงานตามคำสั่ง")

สังเกตว่าคำสั่งถูกทำจากบนลงล่าง นี่คือแนวคิดสำคัญของโปรแกรมแบบลำดับ (sequence)

บทที่ 0.4 รับข้อมูลจากผู้ใช้

ฟังก์ชัน input() หยุดรอให้ผู้ใช้พิมพ์ข้อมูล:

name = input("คุณชื่ออะไร: ")
print("สวัสดี", name)

บรรทัดแรกทำงานสามอย่าง:

  1. แสดงคำถาม
  2. รอรับข้อความ
  3. เก็บข้อความไว้ในชื่อ name

ชื่อ name เป็นตัวแปร ตัวแปรคือชื่อที่เราใช้เรียกข้อมูลในภายหลัง

บทที่ 0.5 โปรแกรมแรกที่ใช้งานได้จริง

สร้างไฟล์ meal_share.py:

print("โปรแกรมหารค่าอาหาร")

total = float(input("ราคาอาหารทั้งหมด: "))
people = int(input("จำนวนคน: "))

share = total / people
print("แต่ละคนจ่าย", share, "บาท")

input() คืนค่าเป็นข้อความเสมอ เราจึงใช้ float() แปลงราคาเป็นเลขทศนิยม และ int() แปลงจำนวนคนเป็นจำนวนเต็ม

แบบฝึกหัด

  1. เพิ่มคำถาม “ค่าบริการ” แล้วนำไปรวมกับราคาอาหาร
  2. แสดงผลให้เหลือทศนิยม 2 ตำแหน่งด้วย f-string
  3. ทดลองป้อนจำนวนคนเป็น 0 แล้วจด error ที่เกิดขึ้นไว้ ยังไม่ต้องแก้

ตัวอย่างการจัดรูปแบบ:

print(f"แต่ละคนจ่าย {share:.2f} บาท")

Checkpoint ระดับ 0

ผ่านเมื่อคุณทำได้โดยไม่เปิดตัวอย่าง:

  • สร้างไฟล์ .py และรันจาก terminal
  • อธิบายความแตกต่างระหว่าง print() กับ input()
  • รับตัวเลขแล้วแปลงชนิดข้อมูลได้
  • อธิบาย Input → Process → Output ของโปรแกรมหารค่าอาหาร
  • อ่าน error จากการหารด้วยศูนย์ได้ว่าเกิดจากขั้นตอนไหน

ถ้ายังไม่ผ่าน ให้กลับไปเขียนโปรแกรม “คำนวณราคาสินค้า × จำนวนชิ้น” ด้วยตัวเองก่อน


ระดับ 1: คิดและเขียนโปรแกรมพื้นฐาน

บทที่ 1.1 ตัวแปรและชนิดข้อมูล

ตัวแปรไม่ได้เป็นกล่องว่างอย่างเดียว แต่เป็นชื่อที่ผูกกับค่าบางอย่างในหน่วยความจำ สิ่งสำคัญคือรู้ว่าค่านั้นมีชนิดใด เพราะชนิดกำหนดการทำงานที่ทำได้

name = "Mali"       # str ข้อความ
age = 28             # int จำนวนเต็ม
height = 1.62        # float ทศนิยม
is_member = True     # bool จริง/เท็จ

ตรวจสอบชนิดได้ด้วย:

print(type(name))
print(type(age))

กฎการตั้งชื่อที่ควรใช้

  • ใช้ภาษาอังกฤษและ snake_case เช่น total_price
  • ชื่อควรบอกความหมาย ไม่ใช้ x หากค่าคือ number_of_people
  • ห้ามขึ้นต้นด้วยตัวเลข
  • อย่าใช้ชื่อชนิดข้อมูล เช่น list, str หรือ input

บทที่ 1.2 ตัวดำเนินการและการแปลงชนิด

price = 120
quantity = 3
subtotal = price * quantity
discount = subtotal * 0.10
total = subtotal - discount

print(total)

ตัวดำเนินการพื้นฐาน:

ตัวดำเนินการความหมาย
+บวก หรือเชื่อมข้อความ
-ลบ
*คูณ
/หาร ได้ float
//หารปัดเศษลง
%เศษจากการหาร
**ยกกำลัง

วงเล็บช่วยบอกลำดับให้ชัด:

total = (price * quantity) - discount

บทที่ 1.3 การเลือกทางด้วย Boolean

โปรแกรมที่ใช้งานจริงต้องตอบสนองตามข้อมูล ไม่ใช่ทำแบบเดิมทุกครั้ง:

score = float(input("คะแนน: "))

if score >= 80:
    grade = "A"
elif score >= 70:
    grade = "B"
elif score >= 60:
    grade = "C"
else:
    grade = "ต้องปรับปรุง"

print(f"ผลการประเมิน: {grade}")

เครื่องหมาย : และการเยื้องบรรทัดไม่ใช่การตกแต่ง แต่เป็นส่วนหนึ่งของ syntax Python โค้ดที่อยู่ในบล็อกเดียวกันต้องเยื้องเท่ากัน

บทที่ 1.4 วงจรการแก้ปัญหา

ก่อนเขียนโปรแกรม ให้เขียนข้อมูลสามส่วน:

โจทย์: คำนวณส่วนลดตามยอดซื้อ
Input: ยอดซื้อ
Process: ถ้ายอดซื้อ >= 1,000 ให้ลด 10% มิฉะนั้นไม่ลด
Output: ราคาสุทธิและจำนวนเงินที่ลด

จากนั้นจึงแปลงเป็นโค้ด:

amount = float(input("ยอดซื้อ: "))

if amount >= 1000:
    discount_rate = 0.10
else:
    discount_rate = 0

discount = amount * discount_rate
net_amount = amount - discount

print(f"ส่วนลด: {discount:.2f} บาท")
print(f"ราคาสุทธิ: {net_amount:.2f} บาท")

นี่คือการออกแบบแบบ top-down: เริ่มจากเป้าหมายใหญ่ แล้วแยกเป็นขั้นตอนที่เล็กพอจะเขียนและตรวจสอบได้

บทที่ 1.5 รายการข้อมูลด้วย List

เมื่อมีข้อมูลหลายรายการ ใช้ list:

expenses = [120, 80, 250]

total = 0
for expense in expenses:
    total = total + expense

print(f"รวมค่าใช้จ่าย: {total} บาท")

การทำงานของ for คือหยิบสมาชิกทีละตัวมาใส่ในตัวแปร expense แล้วรันบล็อกคำสั่งซ้ำจนหมดรายการ

บทที่ 1.6 Dictionary แทนหนึ่งรายการข้อมูล

รายการค่าใช้จ่ายที่เป็นเพียงตัวเลขยังขาดรายละเอียด ใช้ dictionary เก็บข้อมูลที่มีชื่อกำกับ:

expense = {
    "title": "อาหารกลางวัน",
    "amount": 120,
    "category": "อาหาร",
}

print(expense["title"])
print(expense["amount"])

เมื่อมีหลายรายการ เรามักใช้ list of dictionaries:

expenses = [
    {"title": "อาหารกลางวัน", "amount": 120},
    {"title": "รถไฟฟ้า", "amount": 45},
]

total = sum(item["amount"] for item in expenses)
print(total)

โครงสร้างนี้จะกลายเป็นรากฐานของโปรเจกต์รายรับ–รายจ่ายในระดับถัดไป

บทที่ 1.7 Function: ตั้งชื่อให้ขั้นตอน

เมื่อโค้ดทำงานหนึ่งอย่าง ควรตั้งชื่อเป็นฟังก์ชัน:

def calculate_total(items):
    return sum(item["amount"] for item in items)


expenses = [
    {"title": "อาหารกลางวัน", "amount": 120},
    {"title": "รถไฟฟ้า", "amount": 45},
]

total = calculate_total(expenses)
print(f"รวม: {total} บาท")

return ส่งค่ากลับไปให้ผู้เรียก ส่วน print() เพียงแสดงผล การแยกสองหน้าที่นี้ทำให้ฟังก์ชันทดสอบและนำกลับมาใช้ซ้ำได้

บทที่ 1.8 ทดสอบก่อนขยายโปรแกรม

เพิ่มการตรวจสอบเล็ก ๆ:

def calculate_total(items):
    return sum(item["amount"] for item in items)


assert calculate_total([]) == 0
assert calculate_total([{"amount": 120}]) == 120
assert calculate_total([
    {"amount": 120},
    {"amount": 45},
]) == 165

assert ทำให้โปรแกรมหยุดทันทีเมื่อผลลัพธ์ไม่ตรงกับสิ่งที่เราคาดไว้ การทดสอบกรณีว่างเป็นกรณีขอบ (edge case) ที่มักถูกลืม

โปรเจกต์ระดับ 1: รายรับ–รายจ่ายในหน่วยความจำ

ข้อกำหนดรุ่นแรก

โปรแกรมต้อง:

  1. แสดงเมนู
  2. เพิ่มรายการรายรับหรือรายจ่าย
  3. แสดงรายการทั้งหมด
  4. แสดงยอดรวม
  5. ออกจากโปรแกรม

โครงสร้างเมนูขั้นต่ำ:

1) เพิ่มรายการ
2) แสดงรายการ
3) สรุปยอด
4) ออก

ยังไม่ต้องบันทึกลงไฟล์ เมื่อปิดโปรแกรมข้อมูลหายได้ จุดประสงค์ของรุ่นนี้คือฝึก input, condition, loop, list, dictionary และ function ให้ทำงานร่วมกัน

งานที่ต้องส่ง

  • ไฟล์ expense_tracker.py (รุ่นเริ่มต้นของระดับ 1; รุ่นที่เก็บข้อมูลจริงอยู่ในโฟลเดอร์ expense_tracker/)
  • ฟังก์ชันอย่างน้อย add_transaction, list_transactions, summarize
  • test cases สำหรับรายการว่าง ยอดเดียว และหลายรายการ
  • README สั้น ๆ ที่บอกวิธีรันและตัวอย่างการใช้งาน

Change request หลังส่งครั้งแรก

เพิ่มความสามารถ “กรองรายการตามประเภท” โดยไม่ทำให้ความสามารถเดิมเสีย ทดสอบกรณีที่ไม่พบรายการด้วย

Checkpoint ระดับ 1

ผ่านเมื่อคุณสามารถ:

  • แตกโจทย์เป็น Input, Process และ Output ก่อนเริ่มเขียน
  • เลือกใช้ list และ dictionary ได้พร้อมอธิบายเหตุผล
  • เขียน function ที่คืนค่าแทนการพิมพ์อย่างเดียว
  • ใช้เงื่อนไขและลูปสร้างเมนูที่ทำงานซ้ำได้
  • เขียน test case สำหรับกรณีปกติ กรณีว่าง และข้อมูลผิด
  • เพิ่ม change request โดยไม่ทำให้ test เดิมพัง
  • อธิบายโปรแกรมทั้งระบบจากแผนภาพการไหลของข้อมูล

แบบฝึกหัดทบทวนโดยไม่ดูเฉลย

สร้างโปรแกรม “คลังหนังสือส่วนตัว” ที่ทำได้ดังนี้:

  1. เพิ่มหนังสือด้วยชื่อ ผู้เขียน และสถานะอ่านแล้ว/ยังไม่อ่าน
  2. แสดงหนังสือทั้งหมด
  3. ค้นหาด้วยชื่อบางส่วน
  4. สรุปจำนวนหนังสือที่อ่านแล้ว
  5. ปฏิเสธข้อมูลที่ชื่อว่าง

ห้ามนำโค้ดโปรเจกต์รายรับ–รายจ่ายมาเปลี่ยนชื่ออย่างเดียว ให้เขียน Input, Process, Output และ test cases ใหม่ก่อน


ระดับ 2: ทำให้โปรแกรมเก็บข้อมูลได้จริง

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

flowchart LR
    U[ผู้ใช้] --> CLI[เมนูโปรแกรม]
    CLI --> L[Business logic]
    L --> S[Storage layer]
    S --> J[(JSON/CSV)]
    S --> D[(SQLite)]
    L --> T[Tests]

บทที่ 2.1 เส้นทางไฟล์ด้วย pathlib

อย่าต่อ path ด้วย string เอง เช่น "data/" + filename เพราะเครื่องแต่ละระบบใช้ตัวคั่น path ต่างกัน Python มี pathlib สำหรับจัดการ path ให้เหมาะกับระบบปฏิบัติการ

from pathlib import Path

DATA_DIR = Path("data")
DATA_DIR.mkdir(exist_ok=True)

file_path = DATA_DIR / "expenses.txt"
print(file_path)
print(file_path.exists())

mkdir(exist_ok=True) สร้างโฟลเดอร์ถ้ายังไม่มี และไม่แจ้ง error หากมีอยู่แล้ว ส่วนเครื่องหมาย / ของ Path ใช้ต่อ path อย่างปลอดภัย

อ่านและเขียนไฟล์ข้อความ

from pathlib import Path

path = Path("data/notes.txt")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("บันทึกแรก\n", encoding="utf-8")

content = path.read_text(encoding="utf-8")
print(content)

ระบุ encoding="utf-8" เสมอเมื่อทำงานกับข้อความภาษาไทย เพื่อไม่ให้ผลลัพธ์ขึ้นเป็นอักขระแปลก ๆ บนเครื่องอื่น

แบบฝึกหัด

สร้าง settings.txt ที่บันทึกชื่อผู้ใช้หนึ่งบรรทัด อ่านกลับมา และแสดงข้อความทักทาย หากไฟล์ยังไม่มีให้สร้างไฟล์ใหม่พร้อมข้อความเริ่มต้น

บทที่ 2.2 เก็บรายการด้วย JSON

JSON เหมาะกับข้อมูลโครงสร้างเล็กถึงกลางที่ต้องอ่านและแก้ไขได้ง่าย:

import json
from pathlib import Path

path = Path("data/expenses.json")
path.parent.mkdir(parents=True, exist_ok=True)

expenses = [
    {"title": "อาหารกลางวัน", "amount": 120, "kind": "expense"},
    {"title": "เงินเดือน", "amount": 30000, "kind": "income"},
]

path.write_text(
    json.dumps(expenses, ensure_ascii=False, indent=2),
    encoding="utf-8",
)

loaded = json.loads(path.read_text(encoding="utf-8"))
print(loaded)

json.dumps() แปลงค่าจาก Python เป็นข้อความ JSON และ json.loads() แปลงข้อความกลับเป็น Python การใช้ ensure_ascii=False ทำให้ภาษาไทยยังอ่านได้ในไฟล์

สัญญาข้อมูล (data contract)

ก่อนบันทึกข้อมูล ให้กำหนดรูปแบบที่ทุกส่วนของโปรแกรมใช้ร่วมกัน:

transaction:
  title: ข้อความไม่ว่าง
  amount: จำนวนเงินมากกว่าศูนย์
  kind: income หรือ expense

ถ้าแต่ละฟังก์ชันใช้ชื่อฟิลด์ไม่เหมือนกัน เช่น บางแห่งใช้ type และบางแห่งใช้ kind โปรแกรมจะเสียหายเมื่อเปลี่ยน storage ดังนั้นเขียน schema ไว้ใน README และตรวจสอบข้อมูลตอนนำเข้า

ฟังก์ชันโหลดและบันทึก

import json
from pathlib import Path


def load_json(path: Path) -> list[dict]:
    if not path.exists():
        return []
    text = path.read_text(encoding="utf-8")
    return json.loads(text)


def save_json(path: Path, data: list[dict]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    text = json.dumps(data, ensure_ascii=False, indent=2)
    path.write_text(text, encoding="utf-8")

ฟังก์ชันเหล่านี้ยังไม่จัดการไฟล์เสียหรือ JSON ผิดรูปแบบ เราจะเพิ่มความปลอดภัยในบท exception handling

บทที่ 2.3 แลกเปลี่ยนข้อมูลด้วย CSV

CSV เหมาะกับการเปิดใน spreadsheet หรือส่งให้ระบบอื่น:

import csv
from pathlib import Path


def export_csv(path: Path, transactions: list[dict]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", newline="", encoding="utf-8") as file:
        writer = csv.DictWriter(
            file,
            fieldnames=["title", "amount", "kind"],
        )
        writer.writeheader()
        writer.writerows(transactions)

เมื่ออ่าน CSV ค่าทุกช่องเป็นข้อความ จึงต้องแปลงจำนวนเงินเอง:

def import_csv(path: Path) -> list[dict]:
    with path.open(newline="", encoding="utf-8") as file:
        rows = csv.DictReader(file)
        return [
            {
                "title": row["title"],
                "amount": float(row["amount"]),
                "kind": row["kind"],
            }
            for row in rows
        ]

CSV ไม่เหมาะเป็นแหล่งข้อมูลหลักของโปรแกรมที่ต้องแก้ไขทีละรายการ เพราะไม่มี type, constraint และ transaction แบบฐานข้อมูล ใช้เป็นรูปแบบนำเข้า/ส่งออกจะเหมาะกว่า

บทที่ 2.4 จัดการข้อผิดพลาดอย่างมีความหมาย

ข้อผิดพลาดมีอย่างน้อยสามกลุ่ม:

  1. Syntax error: เขียนไวยากรณ์ผิด โปรแกรมเริ่มไม่ได้
  2. Runtime error: ข้อมูลหรือสภาพแวดล้อมทำให้รันต่อไม่ได้
  3. Logic error: โปรแกรมรันได้แต่คำนวณผิด

อย่าดักทุกอย่างด้วย except Exception แล้วซ่อนปัญหา ให้ดักชนิดที่คาดไว้และแสดงคำแนะนำที่ผู้ใช้ทำต่อได้:

def read_amount() -> float:
    while True:
        raw = input("จำนวนเงิน: ").strip()
        try:
            amount = float(raw)
        except ValueError:
            print("กรุณาป้อนตัวเลข เช่น 125.50")
            continue

        if amount <= 0:
            print("จำนวนเงินต้องมากกว่าศูนย์")
            continue
        return amount

สำหรับไฟล์ JSON:

import json
from pathlib import Path


def load_json_safely(path: Path) -> list[dict]:
    if not path.exists():
        return []

    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as error:
        raise ValueError(f"ไฟล์ JSON เสียหาย: {path}") from error
    except OSError as error:
        raise RuntimeError(f"อ่านไฟล์ไม่ได้: {path}") from error

การใช้ raise ... from error รักษาสาเหตุเดิมไว้สำหรับนักพัฒนา แต่แสดงข้อความระดับธุรกิจที่เข้าใจได้สำหรับผู้ใช้

การบันทึกแบบปลอดภัยขึ้น

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

def save_json_atomic(path: Path, data: list[dict]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_suffix(path.suffix + ".tmp")
    temporary.write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    temporary.replace(path)

บทที่ 2.5 แยกโปรแกรมเป็นโมดูล

เมื่อไฟล์เดียวเริ่มยาว ให้แยกตามหน้าที่ ไม่ใช่แยกตามชนิดคำสั่ง:

expense_tracker/
├── app.py          # จุดเริ่มต้นและเมนู
├── domain.py       # กฎธุรกิจและการคำนวณ
├── storage_json.py # อ่าน/เขียน JSON
├── validation.py   # ตรวจสอบข้อมูล
└── tests/
    ├── test_domain.py
    └── test_validation.py

ตัวอย่าง domain.py:

def calculate_balance(transactions: list[dict]) -> float:
    balance = 0.0
    for item in transactions:
        if item["kind"] == "income":
            balance += item["amount"]
        elif item["kind"] == "expense":
            balance -= item["amount"]
        else:
            raise ValueError(f"ไม่รู้จักประเภท: {item['kind']}")
    return balance

ใน app.py:

from domain import calculate_balance


def main() -> None:
    transactions = []
    print(calculate_balance(transactions))


if __name__ == "__main__":
    main()

เงื่อนไข if __name__ == "__main__" ทำให้ไฟล์ทำงานเป็นโปรแกรมเมื่อรันตรง ๆ แต่ไม่รันเมนูทันทีเมื่อถูก import เพื่อทดสอบ

บทที่ 2.6 Virtual environment และ pip

โปรเจกต์แต่ละตัวควรมีสภาพแวดล้อมของ dependencies แยกกัน:

flowchart LR
    P[โปรเจกต์ A] --> E1[.venv A\nแพ็กเกจชุด A]
    Q[โปรเจกต์ B] --> E2[.venv B\nแพ็กเกจชุด B]
    E1 -. ไม่ปะปน .- E2

สร้างและเปิดใช้งาน:

python -m venv .venv

macOS/Linux:

source .venv/bin/activate

Windows PowerShell:

.venv\Scripts\Activate.ps1

อัปเดต pip และติดตั้งแพ็กเกจ:

python -m pip install --upgrade pip
python -m pip install pytest
python -m pip freeze > requirements.txt

ไม่ควร commit โฟลเดอร์ .venv ลง Git เพิ่มใน .gitignore:

.venv/
__pycache__/
*.pyc
data/*.db

ไฟล์ requirements.txt เป็นคำอธิบาย dependencies ของโปรเจกต์ แต่ต้องทบทวนและอัปเดตอย่างระมัดระวัง เพราะการอัปเกรดแพ็กเกจอาจเปลี่ยนพฤติกรรมของโปรแกรม

บทที่ 2.7 เปลี่ยนมาใช้ SQLite

JSON เหมาะกับข้อมูลไม่มาก แต่เมื่อจำเป็นต้องค้นหา แก้ไข ลบ และตรวจสอบความถูกต้องทีละรายการ ฐานข้อมูลจะเหมาะกว่า SQLite ที่ติดมากับ Python ไม่ต้องติดตั้ง server แยก

flowchart TB
    A[Python application] --> C[Connection]
    C --> T[Transaction]
    T --> Q[Parameterized SQL]
    Q --> DB[(SQLite database)]

สร้างตาราง

import sqlite3
from pathlib import Path


def connect(path: Path) -> sqlite3.Connection:
    path.parent.mkdir(parents=True, exist_ok=True)
    connection = sqlite3.connect(path)
    connection.row_factory = sqlite3.Row
    return connection


def create_schema(connection: sqlite3.Connection) -> None:
    connection.execute(
        """
        CREATE TABLE IF NOT EXISTS transactions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            amount REAL NOT NULL CHECK (amount > 0),
            kind TEXT NOT NULL CHECK (kind IN ('income', 'expense')),
            created_at TEXT NOT NULL
        )
        """
    )
    connection.commit()

CHECK, NOT NULL และ PRIMARY KEY คือการให้ฐานข้อมูลช่วยป้องกันข้อมูลผิด ไม่ควรฝากความถูกต้องไว้ที่หน้าจอเพียงอย่างเดียว

เพิ่มรายการอย่างปลอดภัย

from datetime import datetime, timezone


def add_transaction(
    connection: sqlite3.Connection,
    title: str,
    amount: float,
    kind: str,
) -> int:
    cursor = connection.execute(
        """
        INSERT INTO transactions (title, amount, kind, created_at)
        VALUES (?, ?, ?, ?)
        """,
        (title, amount, kind, datetime.now(timezone.utc).isoformat()),
    )
    connection.commit()
    return int(cursor.lastrowid)

เครื่องหมาย ? คือ parameterized query ห้ามนำ input ของผู้ใช้ไปต่อเป็น SQL string เช่น f"... {title} ..." เพราะเสี่ยง SQL injection และปัญหาเรื่องเครื่องหมายคำพูด

อ่านรายการและคำนวณยอด

def list_transactions(connection: sqlite3.Connection) -> list[sqlite3.Row]:
    cursor = connection.execute(
        "SELECT id, title, amount, kind, created_at "
        "FROM transactions ORDER BY created_at DESC"
    )
    return list(cursor.fetchall())


def calculate_balance_db(connection: sqlite3.Connection) -> float:
    row = connection.execute(
        """
        SELECT COALESCE(
            SUM(CASE WHEN kind = 'income' THEN amount ELSE -amount END),
            0
        ) AS balance
        FROM transactions
        """
    ).fetchone()
    return float(row["balance"])

การเปลี่ยนโครงสร้างฐานข้อมูล

อย่าแก้ CREATE TABLE เดิมแบบเงียบ ๆ กับฐานข้อมูลที่มีผู้ใช้แล้ว ให้ใช้ migration ซึ่งบันทึกว่า schema เปลี่ยนจากรุ่นใดเป็นรุ่นใด ในคู่มือนี้ให้เริ่มจากฟังก์ชัน create_schema และเขียนหมายเลข schema ไว้ใน README ก่อนเรียน migration tool ในระดับ 6

บทที่ 2.8 ทดสอบและเปลี่ยน storage

กฎธุรกิจไม่ควรรู้ว่าใช้ JSON หรือ SQLite ถ้าเขียน function ให้รับข้อมูลหรือ interface ที่ชัดเจน เราจะเปลี่ยน storage ได้โดยไม่ต้องเขียนการคำนวณใหม่

ตัวอย่าง test ของกฎธุรกิจ:

from domain import calculate_balance


def test_balance_with_income_and_expense() -> None:
    transactions = [
        {"amount": 1000, "kind": "income"},
        {"amount": 250, "kind": "expense"},
    ]
    assert calculate_balance(transactions) == 750


def test_empty_balance() -> None:
    assert calculate_balance([]) == 0

รันด้วย:

python -m pytest

เริ่มจาก test ที่ไม่แตะไฟล์หรือฐานข้อมูล เพราะ test แบบนี้เร็วและบอกได้ว่ากฎธุรกิจผิดตรงไหน จากนั้นค่อยเพิ่ม integration test สำหรับ storage

บทที่ 2.9 Git และ README

Git บันทึกประวัติการเปลี่ยนแปลง ทำให้ย้อนดูได้ว่าโปรแกรมเสียหลังแก้ไขครั้งใด:

git init
git add .
git commit -m "สร้างโปรแกรมรายรับรายจ่ายรุ่นแรก"
git status
git log --oneline

Commit ควรเป็นหน่วยความเปลี่ยนแปลงที่อธิบายได้ เช่น เพิ่มการบันทึก JSON หรือ แก้การตรวจจำนวนเงินติดลบ หลีกเลี่ยงข้อความกว้าง ๆ เช่น แก้แล้ว หรือ commit เดียวที่รวมการเปลี่ยนแปลงหลายเรื่อง

README ขั้นต่ำต้องมี:

# ชื่อโปรเจกต์
## ความสามารถ
## ข้อกำหนดระบบ
## การติดตั้ง
## วิธีรัน
## รูปแบบข้อมูล
## การทดสอบ
## การสำรองข้อมูล
## ข้อจำกัดที่ทราบ

โปรเจกต์ระดับ 2: โปรแกรมรายรับ–รายจ่ายแบบถาวร

ปรับโปรเจกต์ระดับ 1 ให้มีความสามารถดังนี้:

  1. บันทึกข้อมูลลง SQLite
  2. สร้าง schema อัตโนมัติเมื่อเริ่มโปรแกรมครั้งแรก
  3. เพิ่ม แสดง แก้ไข และลบรายการ
  4. ตรวจสอบชื่อ จำนวนเงิน และประเภทก่อนบันทึก
  5. แสดงยอดคงเหลือและสรุปตามประเภท
  6. ส่งออกรายการเป็น CSV
  7. สำรองไฟล์ฐานข้อมูล
  8. แสดงข้อความที่เข้าใจได้เมื่อฐานข้อมูลหรือไฟล์ส่งออกมีปัญหา
  9. แยก domain, storage, cli และ validation เป็นโมดูล
  10. มี tests สำหรับกฎธุรกิจและ integration ของฐานข้อมูล
  11. มี .gitignore, requirements.txt และ README

โครงสร้างเป้าหมาย

expense_tracker/
├── src/
│   └── expense_tracker/
│       ├── __init__.py
│       ├── cli.py
│       ├── domain.py
│       ├── repository.py
│       └── service.py
├── tests/
│   ├── test_domain.py
│   └── test_repository.py
├── data/.gitkeep
├── .gitignore
├── README.md
└── requirements.txt

Definition of Done

โปรเจกต์ระดับ 2 ผ่านเมื่อ:

  • ปิดและเปิดโปรแกรมใหม่แล้วยังเห็นข้อมูลเดิม
  • ป้อนข้อมูลผิดแล้วโปรแกรมไม่พังและบอกวิธีแก้
  • คำสั่ง SQL ใช้ parameters ทั้งหมด
  • test ผ่านทั้งกรณีปกติ กรณีว่าง และกรณีผิด
  • ลบหรือแก้รายการผิด id แล้วไม่ทำลายรายการอื่น
  • ส่งออก CSV แล้วเปิดใน spreadsheet ได้
  • สำรองและกู้ไฟล์ฐานข้อมูลได้
  • ผู้ใช้ใหม่ติดตั้งและรันตาม README ได้
  • git log แสดงประวัติการพัฒนาที่เข้าใจได้

Change requests สำหรับฝึกแบบมืออาชีพ

หลังโปรเจกต์รุ่นแรกผ่าน ให้ทำทีละข้อและให้ test เดิมยังผ่าน:

  1. เพิ่มวันที่ของรายการและกรองตามเดือน
  2. เพิ่มหมวดหมู่และสรุปยอดตามหมวดหมู่
  3. เพิ่มคำสั่ง export JSON นอกเหนือจาก CSV
  4. ป้องกันการบันทึกชื่อว่างและจำนวนเงินที่ไม่ใช่ตัวเลข
  5. เพิ่ม backup อัตโนมัติก่อน migration
  6. เพิ่ม --db PATH ให้ผู้ใช้เลือกไฟล์ฐานข้อมูล
  7. เพิ่ม logging สำหรับการเพิ่มและลบรายการ โดยไม่บันทึกข้อมูลลับ

Checkpoint ระดับ 2

หยุดและประเมินตนเองด้วยคำถามต่อไปนี้:

  • อธิบายได้หรือไม่ว่า JSON, CSV และ SQLite เหมาะกับงานต่างกันอย่างไร
  • โปรแกรมจัดการอย่างไรเมื่อไฟล์ไม่มี ไฟล์เสีย หรือไม่มีสิทธิ์อ่าน
  • เหตุใด parameterized SQL จึงสำคัญ
  • เหตุใดกฎธุรกิจไม่ควรผูกกับ CLI หรือ storage
  • สร้าง virtual environment ใหม่และติดตั้ง dependencies ได้หรือไม่
  • บุคคลอื่น clone โปรเจกต์แล้วทำตาม README จน test ผ่านได้หรือไม่
  • เขียน test ที่จับบั๊กจาก change request ได้หรือไม่

หากตอบไม่ได้ ให้กลับไปทำ mini-project “บันทึกรายการหนังสือเป็น JSON แล้ว export เป็น CSV” ก่อนเริ่มระดับ 3


ระดับ 3: เปลี่ยนโค้ดให้เป็นซอฟต์แวร์ที่ดูแลได้

ระดับ 2 ทำให้โปรแกรมเก็บข้อมูลได้จริง ระดับ 3 จะตอบคำถามที่สำคัญกว่า: เมื่อโปรแกรมมีผู้ใช้เพิ่ม มีข้อผิดพลาด มีคนอื่นเข้ามาแก้ และต้องเพิ่มความสามารถใหม่ เราจะทำอย่างไรให้โปรแกรมยังเข้าใจง่ายและไม่พังเป็นโดมิโน

flowchart TB
    R[Requirement] --> D[Design]
    D --> C[Code]
    C --> T[Test]
    T --> Q{ผ่านหรือไม่}
    Q -- ไม่ผ่าน --> B[อ่าน failure\nแก้ที่ต้นเหตุ]
    B --> C
    Q -- ผ่าน --> V[Review และ Refactor]
    V --> R

บทที่ 3.1 แยกหน้าที่ก่อนเพิ่มโค้ด

โค้ดที่ดีไม่ได้วัดจากจำนวนบรรทัดสั้นที่สุด แต่วัดจากการเปลี่ยนแปลงหนึ่งเรื่องแล้วกระทบส่วนอื่นน้อยที่สุด

ระบบรายรับ–รายจ่ายควรแยกเป็นชั้น:

CLI / API
  ↓ รับข้อมูลและแสดงผล
Application service
  ↓ ประสาน use case
Domain
  ↓ กฎธุรกิจที่ไม่ผูกกับหน้าจอหรือฐานข้อมูล
Repository
  ↓ อ่านและเขียนข้อมูล
Database / File

ตัวอย่างสิ่งที่ไม่ควรอยู่ในฟังก์ชันเดียวกัน:

# ไม่ควรทำ: รับ input, validate, คำนวณ และ INSERT ในฟังก์ชันเดียว
def add_from_menu(connection):
    title = input("ชื่อ: ")
    amount = float(input("จำนวนเงิน: "))
    connection.execute("INSERT ...")

ฟังก์ชันดังกล่าวทดสอบยาก เพราะต้องจำลองทั้งหน้าจอ ผู้ใช้ และฐานข้อมูลพร้อมกัน ให้แยกเป็น:

def create_transaction(title: str, amount: float, kind: str) -> dict:
    if not title.strip():
        raise ValueError("ชื่อต้องไม่ว่าง")
    if amount <= 0:
        raise ValueError("จำนวนเงินต้องมากกว่าศูนย์")
    if kind not in {"income", "expense"}:
        raise ValueError("ประเภทไม่ถูกต้อง")
    return {
        "title": title.strip(),
        "amount": amount,
        "kind": kind,
    }

จากนั้น CLI ค่อยรับ input และส่งค่าเข้า create_transaction ส่วน repository ค่อยรับผลลัพธ์ไปบันทึก

บทที่ 3.2 Type hints เป็นสัญญาของโค้ด

Type hint ไม่ได้บังคับ Python ตอนรันโดยอัตโนมัติ แต่ช่วยให้ผู้อ่าน เครื่องมือ และ type checker เข้าใจสัญญาของฟังก์ชัน:

from typing import TypedDict


class Transaction(TypedDict):
    title: str
    amount: float
    kind: str


def calculate_balance(items: list[Transaction]) -> float:
    balance = 0.0
    for item in items:
        if item["kind"] == "income":
            balance += item["amount"]
        else:
            balance -= item["amount"]
    return balance

Type hint ที่ดีควรบอกความหมาย ไม่ใช่ใส่เพื่อให้ดูเป็นมืออาชีพ หากฟังก์ชันรับชนิดที่ซับซ้อนเกินไป ให้สร้าง type alias หรือ class ที่สื่อความหมาย

ติดตั้ง type checker ใน environment ของโปรเจกต์:

python -m pip install mypy
python -m mypy src

เริ่มด้วยการตรวจไฟล์สำคัญ แล้วค่อยเพิ่มความเข้มงวด ไม่ควรปิด error ทั้งหมดด้วย # type: ignore เพราะจะทำให้ปัญหาหายไปจากรายงานโดยไม่ได้หายจากโปรแกรม

บทที่ 3.3 Dataclass สำหรับข้อมูลที่มีโครงสร้าง

Dictionary ยืดหยุ่น แต่สะกดชื่อ field ผิดได้ง่าย dataclass ช่วยสร้าง object สำหรับข้อมูลที่มีโครงสร้างชัด:

from dataclasses import dataclass


@dataclass(frozen=True)
class Transaction:
    title: str
    amount: float
    kind: str


item = Transaction("อาหารกลางวัน", 120.0, "expense")
print(item.title)

frozen=True ทำให้ object แก้ไขไม่ได้หลังสร้าง เหมาะกับข้อมูลที่ต้องการให้คงที่ระหว่างการคำนวณ หากต้องเปลี่ยนข้อมูล ให้สร้าง object ใหม่แทนการแก้ค่ากลางทาง

อย่าเปลี่ยนทุก dictionary เป็น class โดยอัตโนมัติ ใช้ dataclass เมื่อ:

  • มี field หลายตัวและใช้ซ้ำหลายฟังก์ชัน
  • ต้องการ validation หรือ method ที่เกี่ยวข้องกับข้อมูล
  • ต้องการ type checker ช่วยตรวจ

บทที่ 3.4 เขียน unit test ด้วย pytest

ติดตั้ง:

python -m pip install pytest

สร้าง tests/test_domain.py:

import pytest

from expense_tracker.domain import create_transaction


def test_create_transaction_strips_title() -> None:
    item = create_transaction("  อาหาร  ", 120, "expense")
    assert item["title"] == "อาหาร"


@pytest.mark.parametrize("amount", [0, -1])
def test_amount_must_be_positive(amount: float) -> None:
    with pytest.raises(ValueError, match="มากกว่าศูนย์"):
        create_transaction("อาหาร", amount, "expense")

โครงสร้าง test ที่อ่านง่ายคือ Arrange → Act → Assert:

def test_balance() -> None:
    # Arrange
    items = [
        {"amount": 1000, "kind": "income"},
        {"amount": 250, "kind": "expense"},
    ]

    # Act
    result = calculate_balance(items)

    # Assert
    assert result == 750

รันพร้อมรายงานละเอียด:

python -m pytest -q
python -m pytest --maxfail=1 -vv

Test ที่ดีไม่เพียงบอกว่าโค้ดผ่าน แต่บอกความคาดหวังของระบบ หาก test อ่านไม่รู้เรื่อง มักแปลว่า requirement หรือชื่อฟังก์ชันยังไม่ชัด

บทที่ 3.5 Fixture และการทดสอบฐานข้อมูล

การทดสอบฐานข้อมูลไม่ควรใช้ไฟล์ฐานข้อมูลจริงของผู้ใช้ เพราะ test จะเปลี่ยนข้อมูลจริงและผลลัพธ์ขึ้นกับการรันทดสอบครั้งก่อน

ใช้ฐานข้อมูลชั่วคราว:

import sqlite3

import pytest


@pytest.fixture
def connection():
    connection = sqlite3.connect(":memory:")
    connection.row_factory = sqlite3.Row
    create_schema(connection)
    yield connection
    connection.close()


def test_add_transaction(connection) -> None:
    transaction_id = add_transaction(
        connection,
        "อาหาร",
        120,
        "expense",
    )
    row = connection.execute(
        "SELECT title, amount, kind FROM transactions WHERE id = ?",
        (transaction_id,),
    ).fetchone()
    assert dict(row) == {
        "title": "อาหาร",
        "amount": 120,
        "kind": "expense",
    }

Fixture ช่วยเตรียมและทำลาย resource ให้แต่ละ test มีสภาพเริ่มต้นที่คาดเดาได้

บทที่ 3.6 Refactoring โดยมี test คุ้มกัน

Refactoring คือการปรับโครงสร้างภายในโดยไม่เปลี่ยนพฤติกรรมภายนอก ลำดับที่ปลอดภัยคือ:

  1. เขียน test ของพฤติกรรมปัจจุบัน
  2. รัน test ให้ผ่าน
  3. เปลี่ยนโครงสร้างทีละเล็ก
  4. รัน test หลังทุกการเปลี่ยนแปลง
  5. commit เมื่อโปรแกรมอยู่ในสถานะที่ใช้งานได้

ตัวอย่างโค้ดซ้ำ:

def income_total(items):
    return sum(item["amount"] for item in items if item["kind"] == "income")


def expense_total(items):
    return sum(item["amount"] for item in items if item["kind"] == "expense")

ปรับเป็น helper เดียว:

def total_by_kind(items, kind: str) -> float:
    return sum(item["amount"] for item in items if item["kind"] == kind)


def income_total(items) -> float:
    return total_by_kind(items, "income")


def expense_total(items) -> float:
    return total_by_kind(items, "expense")

หาก helper ใหม่ทำให้โค้ดอ่านยากกว่าเดิมหรือซ่อนกฎธุรกิจไว้ในชื่อทั่วไปเกินไป ให้ยกเลิกการ refactor นั้น ความสั้นไม่ใช่เป้าหมายเดียว

บทที่ 3.7 Logging แทน print สำหรับการวินิจฉัย

print() เหมาะกับผลลัพธ์ที่ผู้ใช้ต้องเห็น แต่ไม่เหมาะกับการสืบสวนเหตุการณ์ในระบบ ใช้ logging เพื่อระบุระดับและบริบท:

import logging

logger = logging.getLogger(__name__)


def add_transaction_with_log(repository, item):
    logger.info("adding transaction kind=%s amount=%s", item.kind, item.amount)
    transaction_id = repository.add(item)
    logger.info("transaction added id=%s", transaction_id)
    return transaction_id

ตั้งค่าที่จุดเริ่มต้นโปรแกรม:

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

ระดับที่ควรรู้:

  • DEBUG: รายละเอียดสำหรับนักพัฒนา
  • INFO: เหตุการณ์ปกติที่สำคัญ
  • WARNING: สิ่งผิดปกติแต่ยังทำงานต่อได้
  • ERROR: การทำงานหนึ่งรายการล้มเหลว
  • CRITICAL: ระบบอาจให้บริการต่อไม่ได้

ห้ามบันทึก password, token, เลขบัตร หรือข้อมูลส่วนตัวที่ไม่จำเป็นลง log

บทที่ 3.8 Iterable, comprehension และ generator

ใช้ comprehension เมื่อช่วยให้การแปลงข้อมูลอ่านได้ชัด:

expenses = [item for item in transactions if item["kind"] == "expense"]

ถ้าข้อมูลมีจำนวนมากและไม่จำเป็นต้องเก็บทั้งหมดใน memory ให้ใช้ generator:

def expense_amounts(transactions):
    for item in transactions:
        if item["kind"] == "expense":
            yield item["amount"]


total = sum(expense_amounts(transactions))

Generator คืนค่าทีละตัวเมื่อถูกขอ จึงช่วยลดการสร้าง list ชั่วคราว แต่ไม่ควรใช้ generator เพียงเพื่อทำให้โค้ดดูซับซ้อน หากข้อมูลมีขนาดเล็กและต้องวนซ้ำหลายครั้ง list อาจอ่านง่ายกว่า

บทที่ 3.9 Composition ก่อน Inheritance

Composition คือการประกอบ object หลายตัวเข้าด้วยกัน:

class ExpenseService:
    def __init__(self, repository, clock):
        self.repository = repository
        self.clock = clock

    def add(self, title: str, amount: float, kind: str):
        item = create_transaction(title, amount, kind)
        item["created_at"] = self.clock.now()
        return self.repository.add(item)

ข้อดีคือเปลี่ยน repository หรือ clock ได้โดยไม่เปลี่ยนกฎธุรกิจ เช่น test ใช้ FakeClock ที่คืนเวลาคงที่ได้

Inheritance เหมาะเมื่อความสัมพันธ์ “เป็นชนิดเดียวกัน” และต้องการ polymorphism จริง ไม่ควรใช้เพียงเพื่อลดโค้ดซ้ำ หากต้องการใช้ความสามารถของ object หนึ่ง ให้ส่ง object นั้นเข้ามาเป็น dependency ก่อน

โปรเจกต์ระดับ 3: แพ็กเกจที่มีคุณภาพ

ปรับระบบรายรับ–รายจ่ายให้มี:

  1. src layout และ public API ที่ชัด
  2. type hints ในโมดูลหลัก
  3. dataclass หรือ type ที่เหมาะกับ transaction
  4. unit tests ของ domain และ validation
  5. integration tests ของ SQLite แบบ in-memory
  6. fixtures ที่ไม่พึ่งข้อมูลจริง
  7. logging ที่ไม่เปิดเผยข้อมูลลับ
  8. refactor อย่างน้อยหนึ่งครั้งโดยมี test คุ้มกัน
  9. mypy หรือ type checker ผ่านในโมดูลหลัก
  10. README ระบุวิธีรัน test และตั้งค่า logging

Definition of Done

  • test ผ่านทุกครั้งด้วยคำสั่งเดียว
  • test ไม่ใช้ฐานข้อมูลหรือไฟล์ข้อมูลจริง
  • เปลี่ยนจาก SQLite เป็น fake repository ใน unit test ได้
  • ฟังก์ชันหลักมี type hints และชื่อสื่อความหมาย
  • ไม่มีฟังก์ชันที่ทำ input, business logic และ database write พร้อมกันโดยไม่จำเป็น
  • log มี event และ context ที่ใช้สืบสวนได้
  • change request ใหม่เพิ่มได้โดยไม่ต้องแก้ทุกโมดูล
  • ผู้เรียนอธิบายเหตุผลของ abstraction แต่ละชั้นได้

Checkpoint ระดับ 3

  1. อธิบายได้ว่า unit test, integration test และ end-to-end test ต่างกันอย่างไร
  2. เขียน test ที่จำลอง repository ได้โดยไม่เปิดฐานข้อมูลจริง
  3. อธิบายว่าทำไม return สำคัญกว่า print ใน business logic
  4. ใช้ type hints เพื่อค้นหาความผิดพลาดก่อนรันได้
  5. refactor ฟังก์ชันโดย test เดิมยังผ่าน
  6. เลือกได้ว่าเมื่อใดควรใช้ list, generator, dataclass หรือ dictionary
  7. อธิบายเหตุผลที่ composition เหมาะกว่า inheritance ในกรณีของ service
  8. ใช้ log หาเหตุการณ์ที่ทำให้การบันทึกล้มเหลวได้ โดยไม่เปิดเผยข้อมูลสำคัญ

หากยังไม่ผ่าน ให้สร้าง mini-project “ระบบคลังหนังสือ” ที่มี repository แบบ in-memory และ SQLite สองแบบ โดยใช้ test ชุดเดียวกันตรวจพฤติกรรมทั้งสอง implementation


ระดับ 4: Python ขั้นกลางและขั้นสูง

ระดับนี้ไม่ได้เพิ่มคำสั่งเพื่อให้จำมากขึ้น แต่ทำให้เข้าใจว่า Python จัดการ object, name, resource และ interface อย่างไร ความเข้าใจดังกล่าวช่วยป้องกันบั๊กที่เกิดเฉพาะเมื่อโปรแกรมใหญ่ขึ้นหรือทำงานพร้อมกันหลายส่วน

บทที่ 4.1 ชื่อ ตัวตน และค่าของ object

ตัวแปรใน Python เป็นชื่อที่อ้างถึง object ไม่ใช่กล่องที่บรรจุค่าเสมอไป:

first = [1, 2]
second = first
second.append(3)

print(first)   # [1, 2, 3]
print(second)  # [1, 2, 3]

first และ second อ้างถึง list ตัวเดียวกัน การกำหนด second = first ไม่ได้สร้างสำเนา

ถ้าต้องการสำเนา:

first = [1, 2]
second = first.copy()
second.append(3)

print(first)   # [1, 2]
print(second)  # [1, 2, 3]

Mutable และ immutable

  • Mutable: list, dict, set และ object ที่แก้ข้อมูลภายในได้
  • Immutable: int, float, str, tuple และ frozenset

ปัญหาที่พบบ่อยคือค่าเริ่มต้นแบบ mutable ใน function:

# หลีกเลี่ยง
def add_item(item, items=[]):
    items.append(item)
    return items

list จะถูกสร้างครั้งเดียวตอนนิยามฟังก์ชัน ทำให้การเรียกครั้งต่อไปใช้ list เดิม ใช้ None เป็นสัญญาณแทน:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

บทที่ 4.2 Data model และ special methods

Python ใช้ protocol จาก special methods เพื่อกำหนดพฤติกรรมของ object:

class Money:
    def __init__(self, amount: float, currency: str = "THB") -> None:
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount!r}, {self.currency!r})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return (self.amount, self.currency) == (other.amount, other.currency)

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("สกุลเงินไม่ตรงกัน")
        return Money(self.amount + other.amount, self.currency)

ควร implement special method เมื่อ object มีความหมายทางโดเมนจริง อย่า overload operator เพียงเพื่อให้โค้ดสั้น เพราะอาจทำให้ผู้ใช้เข้าใจผิดว่าการดำเนินการนั้นมีความหมายทางคณิตศาสตร์

บทที่ 4.3 Decorator

Decorator คือฟังก์ชันที่รับฟังก์ชันเดิมแล้วคืนฟังก์ชันที่เพิ่มพฤติกรรม:

from functools import wraps
from time import perf_counter


def timed(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        started = perf_counter()
        result = function(*args, **kwargs)
        elapsed = perf_counter() - started
        print(f"{function.__name__}: {elapsed:.4f}s")
        return result

    return wrapper


@timed
def calculate_total(values):
    return sum(values)

@wraps รักษาชื่อและ docstring ของฟังก์ชันเดิม ช่วยให้ debugger และ documentation ยังแสดงข้อมูลที่ถูกต้อง

ใช้ decorator เมื่อพฤติกรรมเป็น cross-cutting concern เช่น timing, retry, authorization หรือ logging อย่าใช้เพื่อซ่อนกฎธุรกิจสำคัญไว้จนผู้อ่านมองไม่เห็นลำดับการทำงาน

บทที่ 4.4 Context manager และการคืน resource

ไฟล์ connection และ lock ต้องถูกปิดแม้เกิด exception with ใช้ context manager เพื่อกำหนดการเริ่มและสิ้นสุดของ resource:

from pathlib import Path


with Path("data/report.txt").open("w", encoding="utf-8") as file:
    file.write("รายงาน\n")

สำหรับ resource ของเราเอง ใช้ contextlib.contextmanager:

from contextlib import contextmanager
import sqlite3


@contextmanager
def database(path: str):
    connection = sqlite3.connect(path)
    try:
        yield connection
        connection.commit()
    except Exception:
        connection.rollback()
        raise
    finally:
        connection.close()

รูปแบบนี้ทำให้ caller ไม่ต้องจำว่าเมื่อใดต้อง commit, rollback หรือ close แต่ต้องกำหนดให้ชัดว่า exception ใดควร rollback และไม่ควรกลืน exception

บทที่ 4.5 Protocol และการออกแบบ interface

ถ้า service ต้องการ object ที่มี method save ก็ไม่จำเป็นต้องผูกกับ class ใด class หนึ่ง ใช้ Protocol อธิบายพฤติกรรมที่ต้องการ:

from typing import Protocol


class TransactionRepository(Protocol):
    def add(self, item: dict) -> int:
        ...


class ExpenseService:
    def __init__(self, repository: TransactionRepository) -> None:
        self.repository = repository

    def add(self, item: dict) -> int:
        return self.repository.add(item)

ทั้ง SQLite repository และ fake repository สำหรับ test ใช้กับ service ได้ หากมี method ตาม protocol นี่คือ structural typing: สนใจว่า object ทำอะไรได้ ไม่ใช่สืบทอดจาก class ใด

บทที่ 4.6 Generic และข้อมูลหลายชนิด

เมื่อเขียน container หรือ helper ที่ใช้กับหลาย type ให้ใช้ generic แทนการปิดบังด้วย Any:

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar("T")


def first_or_none(items: Iterable[T]) -> T | None:
    for item in items:
        return item
    return None

T บอกว่า type ที่คืนเป็นชนิดเดียวกับสมาชิกที่รับเข้า Type checker จึงช่วยตรวจการใช้ฟังก์ชันผิดชนิด

บทที่ 4.7 Standard library ที่ช่วยลดโค้ดเอง

ก่อนติดตั้งแพ็กเกจใหม่ ให้สำรวจ standard library:

from collections import Counter

categories = ["อาหาร", "เดินทาง", "อาหาร", "บันเทิง"]
counts = Counter(categories)
print(counts.most_common())
from datetime import date, timedelta

today = date.today()
next_week = today + timedelta(days=7)
print(today, next_week)
from itertools import groupby

items = sorted(transactions, key=lambda item: item["kind"])
for kind, group in groupby(items, key=lambda item: item["kind"]):
    print(kind, list(group))

groupby จะรวมสมาชิกที่ติดกันและต้อง sort ด้วย key เดียวกันก่อน ไม่ใช่การจัดกลุ่มทุกตำแหน่งโดยอัตโนมัติ

บทที่ 4.8 Serialization และ schema evolution

ข้อมูลที่บันทึกไว้อาจถูกอ่านด้วยโปรแกรมรุ่นใหม่ ดังนั้น schema ต้องมีรุ่นและค่าเริ่มต้นที่ชัด:

CURRENT_SCHEMA_VERSION = 2


def normalize_transaction(raw: dict) -> dict:
    version = raw.get("schema_version", 1)
    item = dict(raw)

    if version == 1:
        item["kind"] = item.pop("type", "expense")
        item["schema_version"] = 2
    elif version != CURRENT_SCHEMA_VERSION:
        raise ValueError(f"ไม่รองรับ schema version: {version}")

    return item

อย่าแก้ข้อมูลเก่าด้วยการเดาเงียบ ๆ หากไม่แน่ใจ ให้แจ้ง migration ที่ต้องทำและสำรองข้อมูลก่อนเสมอ

บทที่ 4.9 ออกแบบ public API ของ package

ผู้ใช้ package ควร import จากจุดที่เสถียร:

# expense_tracker/__init__.py
from .domain import Transaction, calculate_balance

__all__ = ["Transaction", "calculate_balance"]

เมื่อ implementation ภายในเปลี่ยน ผู้ใช้ยัง import แบบเดิมได้ อย่าให้ผู้ใช้ต้อง import จากไฟล์ลึก ๆ เช่น expense_tracker.internal._helpers เพราะชื่อที่มี _ สื่อว่าเป็น internal และอาจเปลี่ยนได้ทุกเมื่อ

กำหนดกติกา versioning:

  • เพิ่มความสามารถโดยไม่ทำลาย API เดิม: minor change
  • แก้ bug ที่ไม่เปลี่ยนสัญญา: patch change
  • ลบหรือเปลี่ยนพฤติกรรม public API: major change

ก่อนลบ API ให้ประกาศ deprecation และมีช่วงเปลี่ยนผ่านพร้อมข้อความแนะนำ

โปรเจกต์ระดับ 4: Reusable Python package

เปลี่ยนโค้ดระบบรายรับ–รายจ่ายให้เป็น package ที่ทีมอื่นนำไปใช้ได้:

  1. สร้าง Transaction ที่มี type ชัดเจน
  2. ใช้ repository protocol และมี SQLite/in-memory implementation
  3. ใช้ context manager จัดการ connection
  4. เพิ่ม schema version และ migration function อย่างน้อยหนึ่งรุ่น
  5. เพิ่ม decorator สำหรับวัดเวลาของ use case ที่เลือก
  6. เพิ่ม generic helper ที่มีประโยชน์จริงอย่างน้อยหนึ่งตัว
  7. ออกแบบ public API ผ่าน __init__.py
  8. เขียน docstring และตัวอย่างการใช้งาน
  9. รัน pytest และ mypy ในคำสั่งตรวจชุดเดียว
  10. สร้าง deprecation note สำหรับ API ทดลอง

Definition of Done

  • อธิบาย aliasing และ mutability ของ object สำคัญในระบบได้
  • resource ทุกชนิดถูกคืนแม้เกิด exception
  • implementation ใหม่ใช้แทนของเดิมได้ตาม protocol
  • schema เก่าถูกอ่านหรือแจ้งข้อผิดพลาดอย่างคาดเดาได้
  • public API มีรายการชัดและมี test ป้องกันการเปลี่ยนโดยไม่ตั้งใจ
  • package ติดตั้งใน environment ใหม่และใช้งานตามตัวอย่างได้
  • ไม่มี decorator หรือ abstraction ที่ทำให้ลำดับการทำงานซ่อนจน debug ไม่ได้

Checkpoint ระดับ 4

  1. อธิบายได้ว่า a = b ต่างจาก b = a.copy() อย่างไร
  2. แก้ mutable default argument ได้โดยไม่ต้องท่องจำเพียงรูปแบบเดียว
  3. เลือกใช้ decorator และ context manager จากปัญหาที่พบจริงได้
  4. เขียน Protocol จากพฤติกรรมที่ service ต้องการได้
  5. อธิบายข้อดีของ generic และเหตุผลที่ไม่ควรใช้ Any ทุกแห่ง
  6. ออกแบบการอ่าน schema รุ่นเก่าโดยไม่ทำลายข้อมูล
  7. ระบุว่าอะไรคือ public API และอะไรควรเป็น internal

หากยังไม่ผ่าน ให้สร้าง package “ตัวติดตามงาน” ที่มี in-memory repository และ JSON repository ใช้ service เดียวกัน และทดสอบว่าเปลี่ยน implementation ได้โดยไม่เปลี่ยน CLI


ระดับ 5: Algorithms และระบบที่นักพัฒนาควรรู้

ระดับนี้สอนวิธีเลือกวิธีแก้ปัญหา ไม่ใช่การแข่งขันจำ algorithm ให้ได้มากที่สุด ทุกหัวข้อเชื่อมกับคำถามว่า “ข้อมูลมีขนาดเท่าไร”, “ต้องตอบสนองเร็วเพียงใด” และ “ความถูกต้องหรือความล้มเหลวมีผลอย่างไร”

บทที่ 5.1 วิเคราะห์ต้นทุนของโปรแกรม

ให้ n แทนจำนวนข้อมูล การวิเคราะห์แบบ Big-O บอกแนวโน้มเมื่อข้อมูลโตขึ้น:

รูปแบบตัวอย่างความหมายโดยย่อ
O(1)เข้าถึง list ด้วย indexเวลาแทบไม่ขึ้นกับจำนวนข้อมูล
O(log n)binary searchลดขอบเขตลงครึ่งหนึ่งซ้ำ ๆ
O(n)วน list หนึ่งรอบเวลาโตตามจำนวนข้อมูล
O(n log n)sorting ที่ดีทั่วไปเหมาะกับข้อมูลขนาดใหญ่
O(n²)วนซ้อนสองชั้นอาจช้าเมื่อข้อมูลโต

อย่าดูเพียง Big-O ให้ดูต้นทุนจริง เช่น I/O, network, memory และ overhead ของ library ด้วย แต่ Big-O ช่วยให้มองเห็นปัญหาก่อนนำข้อมูลจริงมาใช้

def contains_duplicate_slow(values):
    for index, value in enumerate(values):
        if value in values[index + 1:]:
            return True
    return False


def contains_duplicate_fast(values):
    return len(values) != len(set(values))

วิธีแรกสร้าง slice และค้นซ้ำหลายรอบ ส่วนวิธีที่สองใช้ set แลก memory เพื่อให้ค้นหาได้เร็วขึ้น การเลือกต้องพิจารณาว่า memory มีพอหรือไม่

บทที่ 5.2 เลือกโครงสร้างข้อมูล

flowchart TD
    Q{ต้องการจัดเก็บอะไร?}
    Q -->|ลำดับและเข้าถึงด้วยตำแหน่ง| L[List]
    Q -->|key -> value| D[Dictionary]
    Q -->|สมาชิกไม่ซ้ำ/การดำเนินการเซต| S[Set]
    Q -->|เข้าออกปลายเดียว| ST[Stack]
    Q -->|เข้าแถวก่อนออกก่อน| QU[Queue]
    Q -->|ลำดับความสำคัญ| H[Heap/Priority Queue]

Stack และ Queue

ใช้ deque สำหรับการเพิ่มและนำออกจากสองปลาย:

from collections import deque

queue = deque(["งาน A", "งาน B"])
queue.append("งาน C")
current = queue.popleft()
print(current)

อย่าใช้ list.pop(0) กับ queue ขนาดใหญ่ เพราะสมาชิกที่เหลือต้องเลื่อนตำแหน่ง ใช้ deque.popleft() ที่เหมาะกับงานนี้กว่า

Priority queue

import heapq

jobs = []
heapq.heappush(jobs, (1, "แก้ระบบล่ม"))
heapq.heappush(jobs, (3, "ปรับข้อความ"))
heapq.heappush(jobs, (2, "เพิ่มรายงาน"))

priority, job = heapq.heappop(jobs)
print(priority, job)

ค่าลำดับที่น้อยกว่าจะถูกนำออกก่อน หากกติกาธุรกิจต้องการลำดับอื่น ให้แปลง priority ให้ชัดเจนและเขียน test ป้องกันความเข้าใจผิด

บทที่ 5.3 Searching และ sorting

Linear search

def find_title(items, title):
    for item in items:
        if item["title"] == title:
            return item
    return None

เหมาะเมื่อข้อมูลยังไม่เรียงหรือมีจำนวนไม่มาก

Binary search

ข้อมูลต้องเรียงก่อน:

def binary_search(values, target) -> int:
    low, high = 0, len(values) - 1
    while low <= high:
        middle = (low + high) // 2
        if values[middle] == target:
            return middle
        if values[middle] < target:
            low = middle + 1
        else:
            high = middle - 1
    return -1

อย่าใช้ binary search กับข้อมูลที่ยังไม่ได้ sort และอย่าเลือกเพราะดู “ขั้นสูงกว่า” หากการ sort ทุกครั้งแพงกว่าการค้น linear search เพียงครั้งเดียว

Sorting ด้วย key

ใช้ built-in sort ก่อนเขียน algorithm เอง:

ordered = sorted(
    transactions,
    key=lambda item: item["amount"],
    reverse=True,
)

เขียน sorting algorithm เองเพื่อเรียนรู้ trade-off หรือเมื่อมีข้อกำหนดเฉพาะ ไม่ใช่เพื่อแทนที่ implementation ที่ผ่านการปรับปรุงและทดสอบแล้วของ Python

บทที่ 5.4 Recursion และ dynamic programming

ทุก recursion ต้องมี base case ที่หยุดได้:

def countdown(number: int) -> None:
    if number <= 0:
        return
    print(number)
    countdown(number - 1)

ถ้าเรียกตัวเองโดยไม่เข้าใกล้ base case จะเกิด RecursionError

Memoization

from functools import cache


@cache
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

@cache เหมาะกับฟังก์ชันบริสุทธิ์ที่ผลลัพธ์ขึ้นกับ input และ input เป็น hashable หาก function อ่านเวลาปัจจุบันหรือแก้ state ภายนอก ห้าม cache โดยไม่ออกแบบความถูกต้องให้ชัด

บทที่ 5.5 State machine และ event-driven design

โปรแกรมที่มีสถานะหลายขั้น เช่น งาน pending, running, succeeded, failed ควรกำหนด transition ให้ชัด:

stateDiagram-v2
    [*] --> pending
    pending --> running: start
    running --> succeeded: complete
    running --> failed: error
    failed --> pending: retry
    succeeded --> [*]

เขียน transition เป็นกฎที่ทดสอบได้:

ALLOWED = {
    "pending": {"start"},
    "running": {"complete", "error"},
    "failed": {"retry"},
    "succeeded": set(),
}


def transition(state: str, event: str) -> str:
    if event not in ALLOWED.get(state, set()):
        raise ValueError(f"transition ไม่ถูกต้อง: {state} + {event}")
    return {
        ("pending", "start"): "running",
        ("running", "complete"): "succeeded",
        ("running", "error"): "failed",
        ("failed", "retry"): "pending",
    }[(state, event)]

State machine ช่วยป้องกันสถานะที่เป็นไปไม่ได้ เช่น งานที่ succeeded แล้วกลับไป running โดยไม่มีเหตุผล

บทที่ 5.6 File system, encoding และ HTTP

โปรแกรมจริงทำงานกับระบบภายนอกซึ่งช้าหรือเสียได้:

  • ไฟล์อาจไม่มีหรือถูกลบระหว่างทำงาน
  • encoding อาจไม่ตรงกับที่คาด
  • network อาจ timeout
  • server อาจตอบ status code ที่ไม่ใช่ success

หลักการสำคัญคือกำหนด timeout และตรวจผลลัพธ์:

import requests


def fetch_json(url: str) -> dict:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.json()

ในโปรเจกต์ใหม่ ควรแยก client ที่ติดต่อ network ออกจาก business logic เพื่อให้ test ใช้ fake response ได้ ห้าม retry ทุก error เท่ากัน เช่น 401 ไม่ควร retry แบบไม่สิ้นสุด ส่วน 503 อาจ retry ด้วย backoff

บทที่ 5.7 เลือก concurrency model

flowchart TD
    Q{งานลักษณะใด?}
    Q -->|รอ network/I-O จำนวนมาก| A[asyncio หรือ async I/O]
    Q -->|I/O แบบ blocking และ library รองรับ thread| T[Thread]
    Q -->|คำนวณหนัก ใช้ CPU| P[Process]
    Q -->|ทำทีละงานก็พอ| S[ทำแบบ synchronous]

Thread สำหรับงานรอ I/O แบบ blocking

from concurrent.futures import ThreadPoolExecutor


def download(url: str) -> str:
    return fetch_json(url)["name"]


with ThreadPoolExecutor(max_workers=4) as executor:
    names = list(executor.map(download, urls))

Thread ไม่ได้ทำให้ Python คำนวณ CPU-bound เร็วขึ้นโดยอัตโนมัติ และ shared mutable state ทำให้เกิด race condition ต้องลด state ที่แชร์หรือใช้ synchronization อย่างมีเหตุผล

Process สำหรับ CPU-bound

from concurrent.futures import ProcessPoolExecutor


def calculate(value: int) -> int:
    return value * value


if __name__ == "__main__":
    with ProcessPoolExecutor() as executor:
        results = list(executor.map(calculate, range(100)))

ต้องมี if __name__ == "__main__" เพื่อป้องกันการสร้าง process ซ้ำบนบางระบบ และ function ที่ส่งเข้า process ควรอยู่ระดับ module และ serialize ได้

อย่าเพิ่ม concurrency ก่อนวัดปัญหา เพราะความซับซ้อนด้าน cancellation, retry, ordering และ shutdown อาจมากกว่าประโยชน์

บทที่ 5.8 Profiling และ benchmark

ใช้การวัดเพื่อยืนยัน ไม่ใช้ความรู้สึก:

python -m cProfile -s cumulative app.py
python -m timeit "sum(range(1000))"

แยก benchmark ออกจาก test correctness เพราะ test ต้องเน้นความถูกต้อง ส่วน benchmark ต้องควบคุมข้อมูล เครื่อง และจำนวนรอบให้เหมาะสม

ขั้นตอนแก้ performance:

  1. กำหนดเป้าหมาย เช่น response ไม่เกิน 300 ms
  2. สร้าง workload ที่แทนของจริง
  3. วัด baseline
  4. หา bottleneck ด้วย profiler
  5. ปรับจุดเดียว
  6. รัน test และวัดซ้ำ
  7. ตรวจว่า memory และความถูกต้องยังอยู่ในเกณฑ์

โปรเจกต์ระดับ 5: Task-processing system

สร้างระบบรับงานและประมวลผลที่มี:

  1. queue สำหรับงานใหม่
  2. priority ของงาน
  3. state machine pending/running/succeeded/failed
  4. retry ที่จำกัดจำนวนและมี backoff
  5. timeout และการยกเลิกงาน
  6. worker แบบ synchronous เป็น baseline
  7. worker แบบ thread หรือ process เมื่อมีเหตุผลจาก benchmark
  8. logging ของ lifecycle แต่ไม่เปิดเผยข้อมูลลับ
  9. persistence ของสถานะและผลลัพธ์
  10. test ของ transition, retry, timeout และกรณี worker ล้ม

Definition of Done

  • เลือกโครงสร้างข้อมูลโดยอธิบายต้นทุนและ trade-off ได้
  • algorithm มี test สำหรับข้อมูลว่าง ข้อมูลซ้ำ ขอบเขต และ input ผิด
  • state machine ปฏิเสธ transition ที่ไม่ถูกต้อง
  • งานภายนอกมี timeout และจัดการ status/error อย่างเหมาะสม
  • concurrency model มีเหตุผลจากลักษณะงานและผล benchmark
  • shutdown ไม่ทิ้งงานที่กำลังบันทึกโดยไม่มีนโยบาย
  • profiler แสดงหลักฐานของ bottleneck และการปรับปรุง
  • ระบบสามารถกู้ state หลัง process หยุดได้ตามข้อกำหนด

Checkpoint ระดับ 5

  1. อธิบายได้ว่าเมื่อใดใช้ list, dict, set, deque และ heap
  2. เขียน linear และ binary search พร้อมระบุ prerequisite ของ binary search
  3. วิเคราะห์ loop ซ้อนและเลือกวิธีลดต้นทุนได้
  4. เขียน recursion ที่มี base case และรู้ว่าเมื่อใดควรเปลี่ยนเป็น loop
  5. ออกแบบ state transition และ test กรณีที่ไม่อนุญาต
  6. อธิบายความแตกต่างของ I/O-bound และ CPU-bound
  7. เลือก synchronous, thread, process หรือ async จากข้อมูลจริงได้
  8. ใช้ profiler ก่อนปรับ performance และรายงานผลหลังปรับได้

หากยังไม่ผ่าน ให้ทำ mini-project “ระบบคิวส่งอีเมลจำลอง” แบบ synchronous ก่อน แล้วเพิ่ม priority, retry และ worker ทีละความสามารถ


ระดับ 6: Professional Software Engineering

ระดับนี้เปลี่ยนจาก “เขียนโปรแกรมให้เสร็จ” เป็น “ส่งมอบระบบที่คนอื่นเชื่อถือได้” นักพัฒนามืออาชีพต้องรับผิดชอบทั้ง requirement, design, code, test, security, deployment และการแก้ปัญหาหลังระบบถูกใช้งาน

flowchart LR
    R[Requirement] --> A[Architecture]
    A --> I[Implementation]
    I --> V[Verification]
    V --> D[Deploy]
    D --> O[Observe]
    O --> M[Maintain]
    M --> R

บทที่ 6.1 จากปัญหาคลุมเครือสู่ requirement

อย่าเริ่มด้วยคำถามว่า “จะใช้ framework อะไร” ให้เริ่มด้วย:

  1. ใครคือผู้ใช้
  2. ผู้ใช้ต้องการทำอะไร
  3. ความสำเร็จวัดอย่างไร
  4. ข้อมูลใดจำเป็น
  5. กรณีผิดพลาดและข้อจำกัดคืออะไร
  6. สิ่งใดอยู่นอกขอบเขต

ตัวอย่าง user story:

ในฐานะเจ้าของร้าน
ฉันต้องการดูยอดขายรายวัน
เพื่อใช้ตรวจสอบรายรับก่อนปิดร้าน

เปลี่ยนเป็น acceptance criteria ที่ทดสอบได้:

Given มีรายการขายของวันที่ 2026-08-01
When ผู้ใช้ขอรายงานของวันดังกล่าว
Then ระบบแสดงยอดรวมแยกตามวิธีชำระเงิน
And ไม่รวมรายการที่ถูกยกเลิก
And หากไม่มีข้อมูลให้แสดงยอดศูนย์พร้อมข้อความที่เข้าใจได้

เขียน non-functional requirements ด้วย เช่น response time, availability, จำนวนผู้ใช้พร้อมกัน, การเก็บข้อมูล และข้อกำหนดความเป็นส่วนตัว

บทที่ 6.2 ออกแบบก่อนลงมือ

ใช้เอกสารสั้น ๆ ตัดสินใจเรื่องสำคัญ:

Context: ระบบรายงานยอดขาย
Decision: ใช้ SQLite ในรุ่นแรก
Reason: ผู้ใช้คนเดียว ข้อมูลไม่มาก ติดตั้งง่าย
Trade-off: ไม่เหมาะกับการเขียนพร้อมกันหลายเครื่อง
Revisit when: มีผู้ใช้พร้อมกันเกิน 5 คนหรือข้อมูลเกินขนาดที่กำหนด

เอกสารแบบนี้เรียกว่า Architecture Decision Record (ADR) ช่วยให้ทีมรู้ว่าเหตุใดจึงเลือกวิธีหนึ่ง และไม่กลับมาถกประเด็นเดิมโดยไม่มีข้อมูลใหม่

หลัก separation of concerns

flowchart TB
    UI[HTTP/CLI/UI] --> APP[Application use cases]
    APP --> DOM[Domain rules]
    APP --> PORTS[Ports / Interfaces]
    PORTS --> DB[(Database adapter)]
    PORTS --> EXT[External service adapter]

ชั้นบนควรพึ่ง abstraction ที่จำเป็น ชั้นล่างไม่ควรรู้รายละเอียดหน้าจอหรือ HTTP status code การวาง dependency แบบนี้ทำให้เปลี่ยน adapter และทดสอบ use case ได้

บทที่ 6.3 Git workflow และ code review

ก่อนเริ่มงาน ให้สร้าง issue ที่มี context, acceptance criteria และ Definition of Done จากนั้นทำ branch เล็ก ๆ:

git switch -c feature/monthly-report
git status
git add src tests
git commit -m "เพิ่มรายงานยอดขายรายเดือน"
git push -u origin feature/monthly-report

แนวทาง commit:

  • หนึ่ง commit ควรมีเหตุผลเดียว
  • commit ต้องอยู่ในสถานะที่ test ผ่าน
  • อย่า commit secret, database จริง หรือไฟล์ build
  • แยก refactor ที่ไม่เปลี่ยน behavior ออกจาก feature

Checklist review

  • ตรงตาม acceptance criteria หรือไม่
  • มี test สำหรับ success, failure และ edge case หรือไม่
  • validation อยู่ที่ชั้นที่เหมาะสมหรือไม่
  • มี SQL ต่อ string จาก input หรือไม่
  • log เปิดเผยข้อมูลส่วนตัวหรือไม่
  • error message ช่วยผู้ใช้หรือทำให้ความลับรั่วหรือไม่
  • migration ย้อนกลับหรือกู้คืนได้หรือไม่
  • code เปลี่ยนเกินขอบเขตของ issue หรือไม่

Code review ไม่ใช่การตัดสินคนเขียน แต่เป็นการลดความเสี่ยงของระบบและถ่ายทอดความรู้ให้ทีม

บทที่ 6.4 Testing pyramid และ contract

flowchart TB
    E2E[End-to-end\nน้อยและช้า]
    INT[Integration\nพฤติกรรมระหว่างส่วน]
    UNIT[Unit\nมาก เร็ว และเฉพาะจุด]
    UNIT --> INT --> E2E

สัดส่วนไม่ใช่กฎตายตัว แต่โดยทั่วไปควรมี unit test จำนวนมาก, integration test เฉพาะจุดเชื่อมต่อสำคัญ และ E2E สำหรับเส้นทางผู้ใช้ที่สำคัญจริง

Contract test ใช้ยืนยันข้อตกลงระหว่าง service เช่น schema ของ response, ชื่อ field, status code และเงื่อนไข error โดยไม่ต้องทดสอบ implementation ภายในของอีก service

อย่าใช้ coverage เป็นเป้าหมายเดียว 100% coverage อาจยังไม่ทดสอบ behavior สำคัญ หาก test ไม่ตรวจผลลัพธ์หรือ failure mode

บทที่ 6.5 Database design และ migration

การออกแบบตารางต้องระบุ:

  • primary key และชนิดข้อมูล
  • nullability และ default
  • unique constraint
  • foreign key และการลบที่เกี่ยวข้อง
  • index ตาม query ที่ใช้จริง
  • timezone ของ timestamp

ตัวอย่าง index ควรเกิดจาก query ที่วัดแล้ว:

CREATE INDEX IF NOT EXISTS idx_transactions_created_at
ON transactions(created_at);

Migration ที่ปลอดภัยมักใช้ขั้นตอน expand-and-contract:

  1. เพิ่ม column ใหม่ให้รองรับข้อมูลเก่า
  2. deploy code ที่เขียนทั้งเก่าและใหม่ตามช่วงเปลี่ยนผ่าน
  3. backfill ข้อมูลอย่างตรวจสอบได้
  4. เปลี่ยน code ให้ใช้ field ใหม่
  5. ลบ field เก่าหลังไม่มี consumer ใช้แล้ว

อย่า rename หรือลบ column ใน deploy เดียวกับ code ที่ยังอ่าน schema รุ่นเดิม เพราะ rollback อาจทำไม่ได้

บทที่ 6.6 API design และ error contract

API ที่ดีต้องมีสัญญาสม่ำเสมอ:

{
  "error": {
    "code": "INVALID_AMOUNT",
    "message": "amount must be greater than zero",
    "request_id": "..."
  }
}

แยก error ที่ผู้ใช้แก้ได้ เช่น validation ออกจาก server error ที่ควรรายงานให้ทีม แต่อย่าเผย stack trace, SQL หรือ secret ให้ client

สำหรับ endpoint ที่แก้ข้อมูล ให้คิดเรื่อง:

  • authentication: คุณเป็นใคร
  • authorization: คุณทำสิ่งนี้ได้หรือไม่
  • idempotency: retry แล้วข้อมูลซ้ำหรือไม่
  • pagination: รายการมีจำนวนมากจะส่งอย่างไร
  • versioning: client รุ่นเก่าจะทำงานต่อได้อย่างไร
  • timeout และ rate limit

บทที่ 6.7 Secure coding

รายการตรวจขั้นต่ำ:

  1. validate input ที่ขอบเขตระบบ
  2. ใช้ parameterized query
  3. encode output ให้ถูกบริบท
  4. เก็บ secret ใน environment หรือ secret manager
  5. hash password ด้วย algorithm ที่เหมาะสม ห้ามเก็บ plain text
  6. กำหนดสิทธิ์น้อยที่สุด (least privilege)
  7. ไม่ log token และข้อมูลส่วนตัวเกินจำเป็น
  8. pin และตรวจ dependencies
  9. ป้องกัน path traversal เมื่อรับชื่อไฟล์จากผู้ใช้
  10. กำหนด timeout และจำกัดขนาด request
  11. ป้องกัน replay หรือ duplicate operation ด้วย idempotency key เมื่อจำเป็น
  12. มี backup และทดสอบการกู้คืน

ตัวอย่าง path validation:

from pathlib import Path


def safe_child(base: Path, name: str) -> Path:
    candidate = (base / name).resolve()
    base_resolved = base.resolve()
    if base_resolved not in candidate.parents:
        raise ValueError("เส้นทางไม่อยู่ในโฟลเดอร์ที่อนุญาต")
    return candidate

การตรวจความปลอดภัยต้องอยู่ทั้งใน code review, automated scan และการทดสอบพฤติกรรม ไม่ใช่เพิ่มหลังระบบเกิดเหตุแล้ว

บทที่ 6.8 Configuration และ environments

แยก code, configuration และ secret:

import os


DATABASE_URL = os.environ.get("DATABASE_URL", "data/app.db")
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")

ค่าที่มีความลับไม่ควรมี default ที่ใช้งานจริง หากไม่มีค่าให้ fail fast พร้อมข้อความที่ไม่เปิดเผย secret

กำหนดค่าต่างกันสำหรับ development, test และ production แต่ใช้ code path เดียวกันเท่าที่ทำได้ อย่า copy ไฟล์ config ไปแก้มือจนไม่รู้ว่าระบบจริงใช้ค่าใด

บทที่ 6.9 Packaging, CI/CD และ deployment

CI ควรรันใน environment ใหม่ทุกครั้ง:

name: checks
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python -m pip install -r requirements.txt
      - run: python -m pytest
      - run: python -m mypy src

Pipeline ขั้นต่ำ:

  1. format/lint
  2. type check
  3. unit test
  4. integration test
  5. security/dependency scan
  6. build artifact
  7. deploy ไป environment ทดสอบ
  8. smoke test
  9. deploy production ตาม approval

Artifact ต้องสร้างซ้ำได้จาก source และ version ที่ระบุ ไม่ควร deploy จากโฟลเดอร์บนเครื่องนักพัฒนาที่ไม่รู้สถานะ

บทที่ 6.10 Observability และ incident response

ระบบที่ทำงานผิดต้องทิ้งหลักฐานพอให้ตอบได้ว่า “ผู้ใช้ใดได้รับผลกระทบ, เริ่มเมื่อใด, เกิดจากอะไร และแก้อย่างไร”

  • Logs: เหตุการณ์และรายละเอียดตามเวลา
  • Metrics: จำนวน request, latency, error rate, queue depth
  • Traces: เส้นทาง request ผ่านหลาย service
  • Alerts: เงื่อนไขที่ต้องให้คนลงมือทำ ไม่ใช่ทุก warning

กำหนด request_id หรือ correlation id ให้ไหลผ่านขอบเขตระบบ และเขียน runbook:

อาการ: API ตอบ 5xx เกิน 5% เป็นเวลา 5 นาที
ตรวจ: dashboard latency, error logs, database connections
บรรเทา: หยุด rollout รุ่นล่าสุดหรือเพิ่ม capacity ตามเงื่อนไข
กู้คืน: rollback artifact รุ่นก่อน
หลังเหตุการณ์: เก็บ timeline, root cause และ action items

Postmortem ที่ดีมุ่งแก้ระบบและกระบวนการ ไม่กล่าวโทษบุคคล

โปรเจกต์ระดับ 6: ระบบบริการพร้อมส่งมอบ

ยกระบบรายรับ–รายจ่ายเป็นบริการที่มี:

  1. requirement และ acceptance criteria
  2. architecture diagram และ ADR
  3. API หรือ CLI ที่มี error contract
  4. authentication และ authorization ขั้นพื้นฐาน
  5. SQLite/PostgreSQL abstraction พร้อม migration
  6. unit, integration และ smoke tests
  7. configuration แยก environment
  8. CI สำหรับ test, type check และ security scan
  9. artifact ที่สร้างซ้ำได้
  10. deployment ไป test environment
  11. structured logging และ metrics พื้นฐาน
  12. backup, restore และ rollback procedure
  13. README, user guide และ operations runbook
  14. threat model และรายการข้อจำกัดที่ทราบ

Definition of Done

  • requirement ทุกข้อมี test หรือหลักฐานการยอมรับ
  • architecture decision ที่สำคัญถูกบันทึกพร้อม trade-off
  • pull request ผ่าน review checklist และ CI
  • secret ไม่อยู่ใน repository หรือ log
  • migration และ rollback ผ่านการทดลองกับสำเนาข้อมูล
  • deployment ทำซ้ำได้จาก artifact เดิม
  • มี dashboard หรือคำสั่งตรวจ health ที่ชัดเจน
  • ผู้ดูแลระบบคนอื่นทำตาม runbook เพื่อกู้บริการได้
  • มี incident report จำลองและ action items หลังเหตุการณ์

Checkpoint ระดับ 6

  1. เปลี่ยนโจทย์ภาษาธรรมดาเป็น user story และ acceptance criteria ได้
  2. เขียน ADR ที่อธิบายทางเลือกและ trade-off ได้
  3. แยก unit, integration และ E2E test ให้เหมาะกับระบบ
  4. ออกแบบ migration ที่ deploy และ rollback ได้ปลอดภัย
  5. อธิบาย authentication, authorization และ idempotency ต่างกันอย่างไร
  6. หา secret, SQL injection, path traversal และข้อมูลรั่วใน log ได้
  7. สร้าง CI ที่รัน test และ type check ใน environment ใหม่ได้
  8. อ่าน log/metric แล้ววาง incident response เบื้องต้นได้

หากยังไม่ผ่าน ให้สร้าง mini-project “บริการรายการสินค้า” ที่มี API, database migration, authentication จำลอง, CI และ runbook ก่อนเริ่มเลือกสายเชี่ยวชาญในระดับ 7


ระดับ 7: เลือกสายเชี่ยวชาญ

พื้นฐานระดับ 0–6 เป็นแกนร่วมของนักพัฒนา Python ทุกคน ระดับ 7 ให้เลือกอย่างน้อยหนึ่งสายเพื่อเปลี่ยนความรู้ทั่วไปเป็นความสามารถที่ใช้สร้างผลงานจริง การเลือกสายไม่ใช่การปิดประตูสายอื่น แต่เป็นการฝึกให้ลึกพอจะออกแบบระบบทั้งชุดในบริบทหนึ่งได้

flowchart TD
    CORE[Professional Core\nระดับ 0-6] --> WEB[Web/API]
    CORE --> AUTO[Automation/Integration]
    CORE --> DATA[Data Engineering/Analytics]
    CORE --> AI[AI/ML]
    CORE --> DESKTOP[Desktop/GUI]

ก่อนเริ่มสายใด ให้ทำ diagnostic ของสายและเขียนเหตุผลว่าโปรเจกต์ที่ต้องการสร้างต้องใช้สายนี้จริง หากต้องการทำงานหลายสาย ให้จบ capstone แรกหนึ่งสายก่อน แล้วจึงขยายด้วย project ที่สอง

สาย A: Web และ API

เป้าหมาย

สร้างบริการเว็บที่มี endpoint, database, authentication, background job, tests, deployment และ observability ได้

Prerequisite

  • HTTP methods, status codes, headers และ JSON
  • SQL, transaction และ migration
  • async concept และ timeout
  • authentication/authorization เบื้องต้น
  • Docker หรือวิธี deploy ที่เลือก

ลำดับการเรียน

  1. เลือก web framework หนึ่งตัวและเรียน routing, dependency และ lifecycle
  2. ออกแบบ REST resource, pagination, filtering และ error contract
  3. ทำ request validation และ response serialization
  4. เชื่อม database ด้วย repository/service layer
  5. เพิ่ม authentication, password hashing, token/session และ authorization
  6. จัดการ CORS, CSRF, rate limit และ request size
  7. เพิ่ม background job, retry, idempotency และ webhook
  8. เขียน unit, integration, contract และ API smoke tests
  9. สร้าง OpenAPI/documentation และตัวอย่าง client
  10. deploy พร้อม health check, metrics, logs และ rollback

โปรเจกต์สาย Web/API: ระบบจองคิวบริการ

ระบบต้อง:

  • สมัครและเข้าสู่ระบบ
  • แสดงช่วงเวลาว่าง
  • จอง ยกเลิก และป้องกันการจองซ้ำ
  • กำหนดสิทธิ์ผู้ใช้และผู้ดูแล
  • ส่ง notification แบบ background job
  • มี idempotency สำหรับการส่งคำขอซ้ำ
  • เก็บ audit log ของการเปลี่ยนแปลงสำคัญ
  • มี API documentation และ test environment

เกณฑ์ผ่านสาย Web/API

  • อธิบาย transaction ที่ป้องกันการจองชนกันได้
  • ทดสอบ unauthorized, forbidden, invalid input และ duplicate request
  • ไม่เก็บ password หรือ token แบบ plain text
  • มี migration และ rollback ที่ทดสอบแล้ว
  • deploy ระบบจาก artifact และกู้กลับรุ่นก่อนหน้าได้
  • มี dashboard หรือคำสั่งตรวจ latency, error rate และ queue backlog

สาย B: Automation และ Integration

เป้าหมาย

เปลี่ยนงานที่ทำซ้ำให้เป็นกระบวนการอัตโนมัติที่ปลอดภัย ตรวจสอบได้ และรันซ้ำได้โดยไม่สร้างข้อมูลซ้ำ

Prerequisite

  • pathlib, files, CSV, JSON และ regex
  • HTTP client, authentication และ retry
  • scheduling, logging และ exit code
  • idempotency และ backup
  • security ของ credential และข้อมูลส่วนตัว

ลำดับการเรียน

  1. สำรวจ workflow เดิมและกำหนด input/output ที่ชัด
  2. อ่านและเขียนไฟล์โดยคง encoding และ metadata
  3. เรียก API ภายนอกด้วย timeout, retry และ rate limit
  4. สร้าง checkpoint เพื่อรันต่อหลังหยุดกลางทาง
  5. ทำงานแบบ idempotent และป้องกัน duplicate
  6. จัดการไฟล์ชั่วคราว, locking และ concurrent run
  7. เพิ่ม dry-run, confirmation และ audit log
  8. ทำ schedule และ alert เมื่อ job ล้มเหลว
  9. ทำ test ด้วย fake filesystem และ fake HTTP server
  10. package และ deploy เป็น command หรือ scheduled job

โปรเจกต์สาย Automation: ระบบรวมรายงานประจำวัน

ระบบต้อง:

  • รับไฟล์ CSV จากหลายโฟลเดอร์
  • ตรวจ schema, encoding และข้อมูลซ้ำ
  • เรียก API เพื่อเติมข้อมูลที่ขาด
  • รวมเป็นรายงาน JSON/CSV และส่ง notification
  • ย้ายไฟล์สำเร็จไป archive และไฟล์ผิดไป quarantine
  • รันซ้ำได้โดยไม่รวมรายการเดิมซ้ำ
  • มี manifest/checkpoint และสรุปจำนวนสำเร็จ/ล้มเหลว
  • มี dry-run และคำสั่ง resume

เกณฑ์ผ่านสาย Automation

  • รันซ้ำสองครั้งแล้วผลลัพธ์ไม่ซ้ำ
  • หยุดกลางงานแล้ว resume ได้โดยไม่เสียข้อมูล
  • API timeout และ rate limit ไม่ทำให้ข้อมูลเสียหาย
  • secret ไม่อยู่ใน script, log หรือ output report
  • ผู้ดูแลเห็นสาเหตุของแต่ละรายการที่ล้มเหลว
  • มี backup, quarantine และขั้นตอนกู้คืนที่ทดลองแล้ว

สาย C: Data Engineering และ Analytics

เป้าหมาย

สร้าง pipeline ที่นำข้อมูลดิบมาทำความสะอาด ตรวจสอบ แปลง วิเคราะห์ และสร้างผลลัพธ์ที่ทำซ้ำได้ โดยแยกข้อเท็จจริงออกจากการตีความ

Prerequisite

  • Python collections, functions และ testing
  • SQL และ relational data
  • CSV/JSON, missing values และ data types
  • pandas/NumPy เบื้องต้น
  • สถิติพื้นฐานและการสื่อสารผลลัพธ์

ลำดับการเรียน

  1. กำหนด data dictionary และ schema
  2. อ่านข้อมูลหลายแหล่งพร้อมระบุ encoding และ timezone
  3. ตรวจ missing, duplicate, outlier และ constraint
  4. แยก raw, cleaned และ curated data
  5. ทำ transformation ที่ deterministic และมี test
  6. เขียนผลเข้า CSV, database หรือ columnar format ตามขนาดข้อมูล
  7. สร้าง aggregate, metric และ visualization ที่ไม่ทำให้เข้าใจผิด
  8. ทำ data quality checks และรายงาน lineage
  9. เพิ่ม incremental processing และ checkpoint
  10. ทำ pipeline scheduling, backfill และ monitoring

โปรเจกต์สาย Data: Pipeline วิเคราะห์รายรับ–รายจ่าย

ระบบต้อง:

  • รับข้อมูลจาก CSV, JSON และ SQLite
  • แปลง timezone และรูปแบบวันที่ให้เป็นมาตรฐาน
  • ตรวจจำนวนเงินติดลบ รายการซ้ำ และหมวดหมู่ที่ไม่รู้จัก
  • แยกข้อมูลผิดไปยัง quarantine พร้อมเหตุผล
  • คำนวณยอดรายวัน รายเดือน และตามหมวดหมู่
  • สร้าง chart พร้อมคำอธิบายหน่วยและช่วงเวลา
  • บันทึก metadata ของ run และจำนวนแถวแต่ละขั้น
  • รันซ้ำหรือ backfill ช่วงเวลาได้

เกณฑ์ผ่านสาย Data

  • ผลลัพธ์ทำซ้ำได้จาก raw input และ version ของ code
  • pipeline ไม่กลบข้อมูลดิบ
  • มี test สำหรับ timezone, missing, duplicate และ boundary date
  • metric มี definition ชัด ไม่ผสมยอดที่มีความหมายต่างกัน
  • data quality failure ทำให้ pipeline หยุดหรือแจ้งเตือนตาม policy
  • อธิบาย lineage จาก input ถึง chart ได้

สาย D: AI และ Machine Learning

เป้าหมาย

สร้างระบบทดลองและใช้งานโมเดลอย่างมีวินัย ไม่สับสนระหว่าง “โมเดลรันได้” กับ “โมเดลเหมาะกับการใช้งานจริง”

Prerequisite

  • Python และ package management
  • NumPy, pandas และ visualization
  • probability/statistics พื้นฐาน
  • train/validation/test split
  • data leakage, bias และ evaluation metrics
  • API/service deployment เบื้องต้น

ลำดับการเรียน

  1. แปลงปัญหาธุรกิจเป็น target และ metric
  2. สำรวจข้อมูลและตรวจ leakage ก่อน train
  3. สร้าง baseline ที่ง่ายที่สุด
  4. แบ่ง train/validation/test อย่างถูกต้องตามเวลาและ entity
  5. ทำ preprocessing ให้ fit เฉพาะ train
  6. เปรียบเทียบ model และ hyperparameter โดยบันทึก experiment
  7. ประเมิน error แยกตามกลุ่ม ไม่ดู score เดียว
  8. ทำ model artifact, feature schema และ inference contract
  9. deploy prediction service พร้อม timeout และ fallback
  10. monitor drift, data quality, latency และ model performance

โปรเจกต์สาย AI/ML: คาดการณ์หมวดหมู่รายการ

ระบบต้อง:

  • รับข้อความรายการและคืนหมวดหมู่พร้อม confidence
  • มี baseline rule-based เปรียบเทียบกับ model
  • แยก train/test ตามเวลาหรือผู้ใช้เพื่อป้องกัน leakage
  • บันทึก feature/schema และ version ของ model
  • อธิบายกรณี confidence ต่ำและให้ผู้ใช้แก้ไขได้
  • เก็บ feedback โดยไม่ใช้ข้อมูลส่วนตัวเกินจำเป็น
  • มี test ของ preprocessing และ contract ของ inference
  • มี monitoring เมื่อ distribution ของข้อมูลเปลี่ยน

เกณฑ์ผ่านสาย AI/ML

  • อธิบายได้ว่า metric ที่เลือกสอดคล้องกับต้นทุนความผิดพลาดอย่างไร
  • มี baseline และหลักฐานว่า model ใหม่ดีกว่า baseline ในเงื่อนไขที่เหมาะสม
  • ไม่มีข้อมูล test รั่วเข้า preprocessing หรือ tuning
  • prediction ที่ input schema ผิดถูกปฏิเสธอย่างปลอดภัย
  • มี rollback model และ fallback เมื่อ service หรือ model ใช้ไม่ได้
  • รายงานข้อจำกัด อคติ และกลุ่มที่ผลลัพธ์แย่กว่าค่าเฉลี่ย

สาย E: Desktop และ GUI

เป้าหมาย

สร้างโปรแกรม desktop ที่ตอบสนองดี จัดการ state ชัดเจน เก็บข้อมูลได้ และ package ให้ผู้ใช้อื่นติดตั้งได้

Prerequisite

  • Python core, modules และ exception handling
  • event-driven programming
  • state machine และ persistence
  • file/database design
  • cross-platform path และ packaging

ลำดับการเรียน

  1. แยก UI state, domain logic และ persistence
  2. สร้าง event handler ที่สั้นและส่งงานให้ service
  3. ป้องกัน UI ค้างด้วย background task
  4. แสดง validation และ error ที่ผู้ใช้แก้ได้
  5. รองรับ keyboard navigation และ accessible labels
  6. จัดการ unsaved changes และ application shutdown
  7. เขียน UI test เท่าที่จำเป็นและ test business logic แยก
  8. จัดการ config, database migration และ backup
  9. package สำหรับระบบเป้าหมายและเซ็น/เผยแพร่ตามความเหมาะสม
  10. ทำ crash log และ support diagnostics โดยไม่เก็บข้อมูลเกินจำเป็น

โปรเจกต์สาย Desktop/GUI: โปรแกรมจัดการค่าใช้จ่าย

ระบบต้อง:

  • เพิ่ม แก้ไข ลบ และค้นหารายการ
  • แสดงสรุปยอดและกราฟที่อ่านได้
  • ตรวจข้อมูลทันทีโดยไม่ทำให้ผู้ใช้เสียข้อมูลที่กรอก
  • ทำงานอ่าน/เขียนฐานข้อมูลโดย UI ไม่ค้าง
  • เตือนเมื่อมีข้อมูลที่ยังไม่บันทึก
  • backup และ restore จากเมนูที่ชัดเจน
  • package ให้ผู้ใช้ที่ไม่มี Python ติดตั้งได้

เกณฑ์ผ่านสาย Desktop/GUI

  • UI ไม่ค้างจากงาน I/O ปกติ
  • business logic ทดสอบได้โดยไม่เปิดหน้าต่าง
  • keyboard และข้อความแจ้ง error ใช้งานได้จริง
  • ปิดโปรแกรมระหว่างบันทึกแล้วข้อมูลไม่เสียหายตาม policy
  • ติดตั้งบนระบบเป้าหมายและมีขั้นตอนถอนการติดตั้ง/สำรองข้อมูล

วิธีเลือกสายและวัดความลึก

เลือกสายจากปัญหาที่ต้องการแก้ ไม่ใช่จากชื่อ library ที่กำลังเป็นที่นิยม ใช้ตารางนี้เป็นตัวช่วย:

ถ้าต้องการสร้าง...เริ่มสาย...
บริการให้ผู้ใช้หลายคนเรียกผ่านเว็บWeb/API
งานซ้ำที่อ่านไฟล์ เรียก API หรือส่งรายงานAutomation
pipeline และรายงานจากข้อมูลจำนวนมากData
ระบบคาดการณ์หรือจัดประเภทAI/ML
โปรแกรมที่ผู้ใช้ติดตั้งบนเครื่องDesktop/GUI

ทุกสายต้องผ่าน security, testing, deployment และ documentation จากระดับ 6 ไม่สามารถข้ามแกนร่วมด้วยการจำ framework อย่างเดียว

Checkpoint ระดับ 7

  1. เลือกสายโดยเขียน problem statement และเหตุผลด้าน technical fit ได้
  2. วาง prerequisite และลำดับการพัฒนาที่ตรวจสอบได้
  3. สร้าง capstone ของสายที่เลือกโดยมี user, failure mode และ acceptance criteria
  4. ทำระบบรุ่นแรกให้ใช้งานได้ก่อนเพิ่ม optimization หรือ feature ที่ไม่จำเป็น
  5. ทดสอบทั้ง business behavior และ integration กับเครื่องมือเฉพาะสาย
  6. deploy/ส่งมอบและเขียนคู่มือให้ผู้ใช้หรือผู้ดูแลคนอื่นทำตามได้
  7. อธิบายข้อจำกัดและสิ่งที่ยังไม่รองรับอย่างตรงไปตรงมา

หลังผ่าน checkpoint นี้ ให้เข้าสู่ระดับ 8 เพื่อสร้าง capstone ที่ไม่มีโค้ดเฉลยและผ่าน change request จากผู้ประเมิน


ระดับ 8: Capstone พร้อมทำงานจริง

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

flowchart TB
    P[Problem] --> R[Requirements]
    R --> D[Design + ADR]
    D --> B[Backlog]
    B --> I[Iterative implementation]
    I --> T[Test + review]
    T --> S[Security + performance]
    S --> DEP[Deploy]
    DEP --> H[Handoff]
    H --> CR[Change request]
    CR --> B

บทที่ 8.1 เลือกปัญหาที่ดี

Capstone ไม่ควรเป็นเพียงแอป To-do ที่ทำตาม tutorial แต่ก็ไม่ควรใหญ่จนไม่มีวันจบ ให้เลือกปัญหาที่:

  • มีผู้ใช้หรือบทบาทชัดเจนอย่างน้อยหนึ่งกลุ่ม
  • มีข้อมูลที่ต้องจัดการจริงหรือข้อมูลจำลองที่สมเหตุผล
  • มี failure mode ที่ต้องออกแบบ
  • มีความสามารถหลักอย่างน้อย 3 รายการ
  • สามารถสร้างรุ่นแรกที่เล็กลงได้
  • วัดความสำเร็จได้

ตัวอย่างหัวข้อที่เหมาะสม:

  • ระบบจัดการงานซ่อมบำรุงอุปกรณ์
  • ระบบติดตามเอกสารและสถานะอนุมัติ
  • ระบบรวมรายงานจากหลายไฟล์และตรวจคุณภาพข้อมูล
  • ระบบจองทรัพยากรที่ป้องกันการจองซ้ำ
  • ระบบช่วยจัดหมวดหมู่ข้อมูลพร้อมให้ผู้ใช้แก้ผลลัพธ์

หลีกเลี่ยงระบบที่มีข้อกำหนดกฎหมายหรือข้อมูลส่วนตัวจริง หากยังไม่มีสิทธิ์และมาตรการปกป้องที่เหมาะสม ใช้ข้อมูลสังเคราะห์แทน

บทที่ 8.2 Project charter และขอบเขต

เขียนเอกสารหนึ่งหน้าก่อนสร้าง repository:

ชื่อโครงการ:
ปัญหาที่แก้:
ผู้ใช้หลัก:
ผลลัพธ์ที่ต้องการ:
ขอบเขตรุ่นแรก:
สิ่งที่ไม่ทำ:
ความเสี่ยงสูงสุด:
ตัวชี้วัดความสำเร็จ:
เงื่อนไขที่ทำให้หยุดหรือเปลี่ยนทิศทาง:

กำหนด MVP ที่เล็กที่สุดซึ่งพิสูจน์คุณค่าได้ อย่าเริ่มจาก login, dashboard หรือ animation หากยังไม่พิสูจน์ว่ากฎธุรกิจหลักถูกต้อง

บทที่ 8.3 เก็บ requirement และ acceptance criteria

ทำรายการ use case ตามบทบาท:

Actor: เจ้าหน้าที่
Goal: เปลี่ยนสถานะงานซ่อม
Precondition: งานอยู่ในสถานะ pending และเจ้าหน้าที่มีสิทธิ์
Main flow: เปิดงาน -> กำหนดช่าง -> เปลี่ยนเป็น assigned
Failure: งานถูกปิดแล้ว -> ปฏิเสธพร้อมเหตุผล
Audit: บันทึกผู้เปลี่ยน เวลา และค่าเดิม/ค่าใหม่

ทุก use case ต้องมีอย่างน้อย:

  • success path
  • validation failure
  • authorization failure
  • missing resource
  • duplicate/retry behavior
  • dependency failure

บทที่ 8.4 ออกแบบข้อมูลและ architecture

วาด context diagram, data model และ sequence ของ use case สำคัญก่อนเขียน code:

sequenceDiagram
    actor User
    participant UI
    participant Service
    participant DB
    User->>UI: ส่งคำขอเปลี่ยนสถานะ
    UI->>Service: validate command
    Service->>DB: อ่านสถานะปัจจุบัน
    DB-->>Service: current state
    Service->>Service: ตรวจ transition และสิทธิ์
    Service->>DB: บันทึกสถานะใหม่ + audit
    DB-->>Service: commit result
    Service-->>UI: response
    UI-->>User: แสดงผล

บันทึก ADR สำหรับการตัดสินใจที่มีผลต่ออนาคต เช่น database, authentication, queue, deployment และการเลือก synchronous/async

บทที่ 8.5 วาง backlog และพัฒนาเป็นรอบ

แบ่งงานให้แต่ละ issue เสร็จได้ภายในหนึ่งการเปลี่ยนแปลงที่ตรวจสอบได้:

[ ] สร้าง schema งานซ่อม
[ ] เพิ่ม use case สร้างงาน
[ ] เพิ่ม validation ของรายละเอียดงาน
[ ] เพิ่ม transition pending -> assigned
[ ] เพิ่ม audit log
[ ] เพิ่ม endpoint/query สำหรับรายการของฉัน
[ ] เพิ่ม integration test ของ transaction

แต่ละรอบควรมีวงจร:

  1. เลือก issue ที่มี acceptance criteria
  2. เขียนหรือปรับ test
  3. implement ให้เล็กที่สุด
  4. รัน format, type check และ test
  5. review diff และ security impact
  6. commit และอัปเดตเอกสาร
  7. สาธิตผลลัพธ์จาก acceptance criteria

บทที่ 8.6 ทดสอบ capstone

สร้าง test plan แยกตามความเสี่ยง:

ความเสี่ยงหลักฐานที่ต้องมี
กฎสถานะผิดunit tests ของทุก transition
ข้อมูลซ้ำidempotency/integration test
สิทธิ์รั่วauthorization tests แยกบทบาท
ฐานข้อมูลเสียtransaction/rollback test
dependency ล่มtimeout/retry/fallback test
ข้อมูลรั่วsecret/log/path/security review
ระบบช้าworkload และ benchmark
deploy ผิดsmoke test และ rollback drill

อย่าทดสอบเฉพาะ happy path การประเมินระดับมืออาชีพอยู่ที่การรับมือสิ่งที่ผิดพลาดและสิ่งที่ยังไม่เคยคิดถึง

บทที่ 8.7 Security และ privacy review

เขียน threat model แบบง่าย:

Asset: ข้อมูลรายการและบัญชีผู้ใช้
Threat: ผู้ใช้เห็นข้อมูลของอีกคน
Entry point: endpoint ดูรายการ
Control: authorization ตาม owner_id + integration test
Evidence: test IDOR และ audit log
Residual risk: ผู้ดูแลระบบเห็นข้อมูลทั้งหมด ต้องมี policy และ audit

ตรวจอย่างน้อย:

  • authentication และ session/token expiry
  • authorization ทุก resource ไม่ใช่แค่หน้าเมนู
  • SQL injection และ input validation
  • path traversal และ file upload limit
  • secret/configuration
  • logging และ retention
  • backup encryption และ access
  • dependency vulnerabilities
  • การลบหรือส่งออกข้อมูลตามสิทธิ์

บทที่ 8.8 Performance และ reliability review

กำหนด SLO แบบที่วัดได้ เช่น:

95% ของคำขออ่านรายการตอบภายใน 300 ms
งานที่ยาวเกิน 2 วินาทีต้องทำเป็น background job
การบันทึกสำเร็จต้องไม่สูญหายหลัง process restart

สร้าง workload ที่แทนการใช้งานจริง วัด baseline แล้วปรับเฉพาะ bottleneck ที่พบ ตรวจ memory, database connection, queue และ retry storm ด้วย

ความน่าเชื่อถือไม่ได้หมายถึงไม่เคยล้มเหลว แต่หมายถึงล้มเหลวแล้วสูญเสียน้อย กู้ได้ และมีหลักฐานให้หาสาเหตุ

บทที่ 8.9 ส่งมอบและ runbook

ผู้เรียนต้องส่งมอบชุดต่อไปนี้:

project/
├── src/
├── tests/
├── migrations/
├── docs/
│   ├── architecture.md
│   ├── threat-model.md
│   ├── user-guide.md
│   └── runbook.md
├── scripts/
├── .env.example
├── pyproject.toml
├── README.md
└── CHANGELOG.md

README ต้องพาผู้ใช้ใหม่จากเครื่องว่างไปถึงการใช้งาน ส่วน runbook ต้องพาผู้ดูแลจากอาการผิดปกติไปถึงการตรวจ กู้ และยืนยันว่าระบบกลับมาทำงาน

บทที่ 8.10 การสาธิตและ change request

สาธิต capstone โดยไม่แก้โค้ดสดตามใจ ให้แสดง:

  1. ปัญหาและผู้ใช้
  2. use case รุ่นแรก
  3. architecture และ trade-off
  4. การทำงานปกติ
  5. validation/security failure
  6. test และ CI evidence
  7. deployment และ health check
  8. backup/restore หรือ rollback
  9. ข้อจำกัดที่ทราบ

หลังสาธิต ให้รับ change request ใหม่ที่ไม่อยู่ในแผน เช่น:

  • เพิ่มบทบาทผู้ตรวจสอบ
  • รองรับ timezone ใหม่
  • ส่งออกข้อมูลเฉพาะผู้ใช้หนึ่งราย
  • ทำให้การเรียกซ้ำไม่สร้างรายการซ้ำ
  • เพิ่มจำนวนข้อมูลเป็นสิบเท่า

วิเคราะห์ผลกระทบก่อนแก้ และทำให้ test เดิมยังผ่าน Change request นี้เป็นส่วนสำคัญของการประเมิน เพราะงานจริงไม่จบเมื่อรุ่นแรกทำงานได้

Rubric ระดับ 8

ด้านต้องแสดงให้เห็น
Problem framingscope, user และ success metric ชัด
Designdiagram, data model และ ADR มีเหตุผล
Implementationโค้ดแบ่งหน้าที่ อ่านง่าย และมี type ที่เหมาะสม
Correctnesstests ครอบคลุม behavior และ failure สำคัญ
Securitythreat model และ controls ใช้งานจริง
Reliabilitytimeout, retry, backup, restore และ rollback
DeliveryCI, artifact และ deployment ทำซ้ำได้
Operationslogging, metrics, health check และ runbook
CommunicationREADME, demo และอธิบาย trade-off ได้
Adaptabilityรับ change request โดยไม่ทำลายระบบเดิม

ไม่จำเป็นต้องได้คะแนนสูงสุดทุกด้านในครั้งแรก แต่ต้องระบุ gap, ความเสี่ยง และแผนปรับปรุงอย่างตรงไปตรงมา

Definition of Done ระดับ 8

Capstone ผ่านเมื่อ:

  • ผู้ประเมินติดตั้งและใช้งานจาก README ได้
  • acceptance criteria รุ่นแรกมีหลักฐานผ่านครบ
  • test suite ทำงานใน environment ใหม่
  • มีการตรวจ security และไม่มี secret จริงใน repository
  • deployment, rollback และ restore ผ่านการซ้อม
  • ผู้เรียนอธิบายเหตุผลของ architecture และ trade-off สำคัญได้
  • ผู้เรียนรับ change request และส่งรุ่นใหม่พร้อม test/documentation ได้
  • มีรายการข้อจำกัดที่ทราบและ roadmap ที่สมจริง

หลังจบหลักสูตร

การจบระดับ 8 ไม่ได้แปลว่าเรียน Python จบ แต่หมายถึงคุณมีวิธีเรียนรู้ปัญหาใหม่อย่างเป็นระบบ ให้ทำวงจรต่อไปเป็นประจำ:

  1. อ่าน source และ documentation ของเครื่องมือที่ใช้
  2. สร้างโปรเจกต์เล็กเพื่อทดสอบสมมติฐาน
  3. เขียนสิ่งที่เรียนรู้และข้อผิดพลาดที่พบ
  4. อ่าน code review จากผู้อื่น
  5. ปรับปรุงระบบเก่าจากข้อมูลการใช้งานจริง
  6. เปรียบเทียบทางเลือกโดยวัด ไม่ใช่เดา

เป้าหมายสุดท้ายของคู่มือนี้คือให้คุณพึ่งพาคู่มือน้อยลงเรื่อย ๆ และสามารถออกแบบเส้นทางเรียนรู้ของตนเองเมื่อพบโจทย์ใหม่


ภาคผนวก: แผนตรวจความพร้อมสำหรับผู้เรียน

ใช้รายการนี้ก่อนประกาศว่าตนเองพร้อมพัฒนาโปรแกรมเอง:

  • อธิบายปัญหาและขอบเขตได้ก่อนเขียน code
  • แยก domain logic จาก I/O และ framework ได้
  • เขียน test ก่อนหรือพร้อมกับความสามารถใหม่
  • อ่าน traceback และสร้าง minimal reproduction ได้
  • ใช้ Git เพื่อย้อนดูและแบ่งการเปลี่ยนแปลงได้
  • ตรวจ input, authorization, secrets และ dependencies ได้
  • ออกแบบ database constraint และ migration ได้
  • กำหนด timeout, retry, idempotency และ failure policy ได้
  • วัด performance ก่อนและหลังปรับปรุงได้
  • deploy, monitor, backup, restore และ rollback ได้
  • เขียน README, API docs และ runbook ให้คนอื่นใช้ได้
  • รับ feedback และ change request โดยรักษาความถูกต้องของระบบได้

ภาคผนวก A: เตรียมสภาพแวดล้อมสำหรับเรียนเอง

A.1 ตรวจสอบ Python

ติดตั้ง Python 3 รุ่นที่หลักสูตรกำหนดจากแหล่งทางการ แล้วตรวจสอบ:

python --version
python -m pip --version

หากคำสั่ง python ไม่ชี้ไปยัง Python รุ่นที่ต้องการ ให้ลอง python3 บน macOS/Linux หรือใช้ Python Launcher บน Windows ตรวจสอบให้แน่ใจก่อนสร้าง virtual environment เพราะ environment จะผูกกับ interpreter ที่ใช้สร้าง

A.2 สร้างโฟลเดอร์โปรเจกต์

mkdir expense-tracker
cd expense-tracker
python -m venv .venv

เปิดใช้งาน environment ตามระบบปฏิบัติการ แล้วตรวจสอบว่า interpreter ชี้เข้า .venv:

python -c "import sys; print(sys.executable)"

A.3 คำสั่งตรวจคุณภาพขั้นต่ำ

python -m compileall src
python -m pytest
python -m mypy src

ถ้าใช้ formatter/linter ให้รวมไว้ในคำสั่งตรวจเดียวกัน เช่น:

python -m ruff check .
python -m ruff format --check .

อย่าเพิ่มเครื่องมือเพราะความนิยมเพียงอย่างเดียว ให้กำหนดว่าเครื่องมือนั้นตรวจความเสี่ยงอะไร และผลลัพธ์จะถูกนำไปใช้ใน workflow อย่างไร

ภาคผนวก B: Python และเครื่องมือที่ใช้บ่อย

B.1 การรันโปรแกรม

python app.py
python -m package.module
python -c "print(2 + 3)"

การใช้ python -m ช่วยให้ Python จัดการ module path ตาม package ได้ถูกต้องกว่าการเรียกไฟล์ลึก ๆ โดยตรงในบางโครงสร้าง

B.2 การจัดการ package

python -m pip install package-name
python -m pip uninstall package-name
python -m pip list
python -m pip show package-name
python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt

ตรวจ dependency ที่มีช่องโหว่ด้วยเครื่องมือที่ทีมอนุมัติ และอัปเดตแบบมี test/rollback ไม่ใช่อัปเดตทุก package พร้อมกันใน production

B.3 การทำงานกับ Git

git status
git diff
git add path/to/file
git commit -m "ข้อความที่อธิบายการเปลี่ยนแปลง"
git log --oneline --decorate --graph
git switch -c feature/name
git diff main...HEAD

ก่อน commit ให้ตรวจว่าไม่มี:

  • .env หรือ secret
  • database ที่มีข้อมูลจริง
  • ไฟล์ cache และ build
  • log ที่มีข้อมูลส่วนตัว
  • ไฟล์ขนาดใหญ่ที่ไม่จำเป็น

ภาคผนวก C: คู่มือ Debugging แบบเป็นระบบ

เมื่อโปรแกรมผิด อย่าแก้โดยสุ่มเปลี่ยนหลายบรรทัด ให้ทำตามลำดับ:

flowchart TD
    F[สังเกต failure] --> R[ทำซ้ำให้ได้]
    R --> M[สร้าง minimal reproduction]
    M --> H[ตั้ง hypothesis]
    H --> E[เพิ่มหลักฐาน/log/test]
    E --> X{ตรงกับสาเหตุหรือไม่}
    X -- ไม่ตรง --> H
    X -- ตรง --> C[แก้ที่ต้นเหตุ]
    C --> T[รัน regression test]
    T --> D[บันทึกสิ่งที่เรียนรู้]

C.1 อ่าน traceback

อ่านจากล่างขึ้นบน:

  1. ชนิด exception และข้อความบรรทัดสุดท้าย
  2. ไฟล์และบรรทัดที่ exception เกิด
  3. call stack ว่า function ใดเรียก function ใด
  4. ค่า input ที่ทำให้เกิดปัญหา

ตัวอย่าง:

ValueError: could not convert string to float: 'สิบบาท'

ข้อสรุปแรกที่ถูกต้องคือมีข้อความถูกส่งเข้า float() ไม่ใช่รีบแก้ด้วยการดัก exception ทุกชนิด

C.2 เทคนิคสร้าง minimal reproduction

ตัดสิ่งที่ไม่เกี่ยวข้องออกทีละอย่าง:

  • ใช้ข้อมูลหนึ่งรายการแทนทั้งไฟล์
  • ตัด network ออกและใช้ response จำลอง
  • ใช้ฐานข้อมูล :memory:
  • ปิด concurrency ชั่วคราว
  • เรียก function ที่ผิดโดยตรง

Minimal reproduction ที่ดีต้องสั้น ทำซ้ำได้ และยังแสดง failure เดิม หากตัดจนปัญหาหาย ให้บันทึกว่าการตัดส่วนใดทำให้พฤติกรรมเปลี่ยน

C.3 แยกประเภทของปัญหา

อาการสิ่งที่ตรวจเป็นอันดับแรก
โปรแกรมเริ่มไม่ได้syntax, import, environment
ได้ None ผิดคาดreturn path และ branch
ข้อมูลบางรายการหายfilter, loop, transaction
รันซ้ำแล้วข้อมูลเพิ่มซ้ำidempotency และ unique constraint
ช้าลงตามจำนวนข้อมูลcomplexity, query, I/O
ใช้ memory เพิ่มเรื่อย ๆcache, list, resource close
ทำงานเป็นบางครั้งrace condition, timeout, external dependency
ผู้ใช้เห็นข้อมูลคนอื่นauthorization และ query scope

ภาคผนวก D: วิธีอ่าน Documentation ให้เรียนรู้ต่อเอง

เมื่อพบ library หรือ API ใหม่ อย่าเริ่มจากการคัดลอกตัวอย่างยาว ให้ทำตามขั้นตอน:

  1. อ่านหน้า overview เพื่อเข้าใจขอบเขต
  2. ตรวจ version และ compatibility
  3. หา minimal example ของ function ที่ต้องใช้
  4. อ่าน signature, return value และ exceptions
  5. อ่านข้อจำกัดและ security notes
  6. เขียนตัวอย่างเล็กที่รันได้ใน virtual environment
  7. เพิ่ม test ของ success และ failure
  8. ห่อ dependency ไว้หลัง interface ที่เหมาะกับระบบ
  9. บันทึก version และเหตุผลที่เลือก
  10. ตรวจ changelog ก่อนอัปเกรด

เวลาค้นหาคำตอบ ให้เขียนคำถามให้มีบริบท เช่น:

Python 3.12 pathlib atomic replace Windows permission error

จากนั้นตรวจคำตอบกับ documentation ทางการและการทดลองของตนเอง อย่าใช้ snippet ที่ไม่รู้แหล่งที่มาใน production โดยไม่ review

ภาคผนวก E: Checklist ตรวจโค้ดก่อนส่งมอบ

ความถูกต้อง

  • ทุก requirement มี test หรือหลักฐานการตรวจ
  • มีกรณีว่าง ขอบเขต ข้อมูลผิด และ dependency ล้มเหลว
  • error message ระบุวิธีแก้ที่ผู้ใช้ทำได้
  • เวลา วันที่ timezone และ encoding ถูกกำหนดชัดเจน

การดูแลรักษา

  • ชื่อ module/function บอกหน้าที่
  • business logic ไม่ผูกกับ I/O โดยไม่จำเป็น
  • public API มี documentation
  • ไม่มี abstraction ที่ซับซ้อนเกินปัญหา
  • test อ่านแล้วเข้าใจ behavior

ความปลอดภัย

  • input ถูก validate
  • query ใช้ parameters
  • authorization ตรวจที่ resource boundary
  • secret ไม่อยู่ใน code, config ที่ commit หรือ log
  • dependencies และ file path ถูกตรวจ

การปฏิบัติการ

  • มี logging ที่พอวิเคราะห์เหตุการณ์
  • มี health check หรือ smoke test
  • มี backup และขั้นตอน restore
  • deploy และ rollback ทำซ้ำได้
  • runbook ผ่านการทดลองโดยคนอื่น

การสื่อสาร

  • README เริ่มจากเครื่องว่างได้
  • มีข้อจำกัดและ known issues
  • changelog อธิบายผลกระทบต่อผู้ใช้
  • ADR อธิบาย trade-off ที่สำคัญ
  • ตัวอย่างไม่ใช้ข้อมูลส่วนตัวหรือ secret จริง

ภาคผนวก F: รูปแบบบันทึกการเรียนรู้

หลังจบแต่ละบท ให้บันทึกสั้น ๆ:

วันที่:
บท/หัวข้อ:
สิ่งที่สร้าง:
สิ่งที่อธิบายได้โดยไม่เปิดหนังสือ:
บั๊กที่พบ:
สาเหตุที่แท้จริง:
สิ่งที่ทำให้แก้ได้:
สิ่งที่ยังสับสน:
คำถามที่จะค้นต่อ:
การทดลองครั้งถัดไป:

บันทึกนี้ทำให้การเรียนแบบไม่จำกัดเวลาไม่กลายเป็นการอ่านผ่านไปเรื่อย ๆ และช่วยให้เห็นช่องว่างของทักษะก่อนเริ่มโปรเจกต์ใหญ่

ภาคผนวก G: คลังโจทย์ฝึกตามระดับ

โจทย์เหล่านี้ตั้งใจให้ทำโดยไม่เปิดเฉลยเต็มรูปแบบ หากติด ให้เขียนสมมติฐานและขอเพียง hint ที่เล็กที่สุดก่อน การค้น documentation และการทดลองเป็นส่วนหนึ่งของโจทย์ ไม่ใช่การโกง

G.1 ระดับ 0–1: พื้นฐาน

เครื่องคำนวณค่าใช้จ่ายเดินทาง

รับระยะทาง อัตราค่าน้ำมัน ราคาน้ำมัน และจำนวนผู้เดินทาง แล้วแสดงค่าใช้จ่ายต่อคน

ต้องทดสอบ:

  • จำนวนผู้เดินทางเป็นศูนย์
  • ตัวเลขติดลบ
  • ค่าเดินทางมีทศนิยม
  • การจัดรูปแบบทศนิยมสองตำแหน่ง

เกมทายเลข

สุ่มเลขที่กำหนดช่วง ให้ผู้ใช้ทายจนถูกและบอกว่าค่าที่ทายสูงหรือต่ำเกินไป

ต้องทดสอบกฎการนับจำนวนครั้งแยกจาก input และการแสดงผล

สมุดรายชื่อในหน่วยความจำ

เพิ่ม ค้นหา แก้ไข และลบชื่อกับเบอร์โทรศัพท์ โดยห้ามมีเบอร์ซ้ำ

เกณฑ์ผ่าน: ผู้เรียนเลือกได้ว่าจะใช้ dictionary หรือ list of dictionaries และอธิบายเหตุผล

G.2 ระดับ 2: ไฟล์และฐานข้อมูล

ตัวแปลง JSON เป็น CSV

รับ path ของ JSON ตรวจ schema แล้วส่งออก CSV หากรายการใดผิดให้แยกไฟล์ error พร้อมเลขบรรทัดหรือ index

ระบบสต็อกสินค้า

เก็บสินค้า รหัส ราคา และจำนวนคงเหลือ มีคำสั่งรับเข้า จ่ายออก และปรับยอด

ต้องมี constraint ไม่ให้จำนวนติดลบ และบันทึกประวัติการเปลี่ยนแปลงทุกครั้ง

ตัวติดตามหนังสือ

บันทึกข้อมูลลง SQLite ค้นหาด้วยชื่อหรือผู้เขียน กรองตามสถานะ และ export CSV

เกณฑ์ผ่าน: เปลี่ยน storage จาก SQLite เป็น JSON ใน test ได้โดยไม่เปลี่ยนกฎการค้นหา

G.3 ระดับ 3–4: คุณภาพและ Python ขั้นสูง

Library repository ที่เปลี่ยน implementation ได้

กำหนด protocol สำหรับ add, find และ remove สร้าง in-memory และ SQLite implementation แล้วใช้ service เดียวกัน

ตัวจัดการ configuration

อ่านค่าจาก default, environment และไฟล์ config ตามลำดับความสำคัญ ตรวจชนิดและแจ้ง error ที่อ่านได้ ห้าม log secret

Schema migration simulator

สร้างข้อมูลรุ่น 1, migration เป็นรุ่น 2 และทดสอบว่าข้อมูลเดิมไม่สูญหาย หาก schema ไม่รู้จักต้องหยุดอย่างปลอดภัย

Context-managed transaction

สร้าง context manager ที่ commit เมื่อสำเร็จ rollback เมื่อเกิด exception และปิด connection เสมอ ทดสอบทั้งสามกรณี

G.4 ระดับ 5: Algorithms และระบบ

ระบบค้นหาหลายเงื่อนไข

รองรับค้นหาด้วยชื่อ หมวดหมู่ และช่วงราคา เปรียบเทียบ linear scan กับ index ที่สร้างเอง วัดเวลาเมื่อข้อมูลเพิ่มเป็น 10, 100 และ 10,000 รายการ

Priority task queue

รับงานที่มี priority, deadline และ retry count ปฏิเสธ transition ที่ไม่ถูกต้อง และกู้รายการงานหลังโปรแกรมเริ่มใหม่

URL fetcher ที่มีข้อจำกัด

ดึง URL หลายรายการพร้อม timeout, จำกัด concurrency, retry เฉพาะ error ที่เหมาะสม และบันทึกผลสำเร็จ/ล้มเหลวโดยไม่หยุดทั้งระบบ

เกณฑ์ผ่าน: มี baseline synchronous และหลักฐานว่ารูปแบบ concurrent คุ้มกับความซับซ้อนที่เพิ่มขึ้น

G.5 ระดับ 6: Professional Engineering

ระบบอนุมัติเอกสาร

มีบทบาทผู้ส่ง ผู้ตรวจ และผู้อนุมัติ state transition, audit log, authorization, migration, CI และ runbook

API สินค้าหลายผู้ใช้

มี pagination, filtering, idempotency, error contract, authentication และ rate limit จำลอง พร้อม integration test

Incident drill

สร้าง failure ที่ database ช้าหรือ dependency ตอบ 503 แล้วให้ระบบมี timeout, fallback, alert และ runbook สำหรับกู้คืน

G.6 ระดับ 7: สายเชี่ยวชาญ

Web/API

ระบบจองทรัพยากรที่ป้องกันการจองซ้ำภายใต้คำขอพร้อมกัน

Automation

pipeline รวมไฟล์หลายแหล่งที่ resume ได้และไม่สร้างข้อมูลซ้ำเมื่อรันซ้ำ

Data

pipeline ตรวจคุณภาพข้อมูลพร้อม lineage, quarantine และรายงาน metric

AI/ML

ระบบจัดหมวดหมู่ที่มี baseline, leakage check, model version, confidence และ fallback

Desktop/GUI

โปรแกรมจัดการข้อมูลที่ UI ไม่ค้าง มี backup/restore และ package ติดตั้งได้

ภาคผนวก H: วิธีขอ Hint และประเมินตนเอง

เมื่อทำโจทย์แล้วติด ให้ขอความช่วยเหลือตามระดับนี้:

  1. ทิศทาง: ควรตรวจ input, process หรือ output ส่วนใด
  2. แนวคิด: โครงสร้างข้อมูลหรือเครื่องมือประเภทใดเหมาะกว่า
  3. โครงร่าง: ชื่อฟังก์ชันและลำดับขั้น โดยไม่มี implementation
  4. กรณีทดสอบ: ตัวอย่าง input/expected output ที่ควรเพิ่ม
  5. เฉลย: โค้ดหนึ่งวิธี พร้อม trade-off และข้อจำกัด

ก่อนดูระดับถัดไป ให้เขียนสิ่งที่ลองแล้วและผลที่ได้ หากเปิดเฉลยแล้ว ให้ปิดและเขียนใหม่จากความเข้าใจของตนเองโดยเปลี่ยน input หรือ requirement อย่างน้อยหนึ่งข้อ

H.1 Rubric แบบ 4 ระดับ

ระดับลักษณะงาน
0 ยังไม่พร้อมทำตามตัวอย่างได้แต่เริ่มโจทย์ใหม่ไม่ได้
1 กำลังพัฒนาแก้โจทย์ได้เมื่อมีโครงร่างและ hint
2 ใช้งานได้สร้างเอง ทดสอบ และแก้บั๊กหลักได้
3 พร้อมต่อยอดอธิบาย trade-off, refactor, deploy และรับ change request ได้

เลื่อนระดับเมื่อทำโจทย์ใหม่ได้อย่างน้อยสองโจทย์ที่ไม่ใช่การเปลี่ยนชื่อจากตัวอย่างเดิม และสามารถอธิบายทางเลือกที่ไม่ได้เลือกได้

ภาคผนวก I: Glossary ที่ต้องรู้

  • Argument: ค่าที่ส่งเข้า function ตอนเรียก
  • API: สัญญาจุดเชื่อมต่อระหว่างโปรแกรมหรือส่วนประกอบ
  • Assertion: คำสั่งตรวจว่าค่าตรงกับเงื่อนไขที่คาด
  • Authentication: การพิสูจน์ว่าเป็นใคร
  • Authorization: การตรวจว่ามีสิทธิ์ทำอะไร
  • Baseline: วิธีแก้ที่ง่ายและใช้เป็นจุดเปรียบเทียบ
  • Business logic: กฎของปัญหา ไม่ผูกกับหน้าจอหรือ storage
  • Callback: ฟังก์ชันที่ส่งให้ระบบอื่นเรียกภายหลัง
  • Checkpoint: เกณฑ์ตรวจความพร้อมก่อนข้ามไปหัวข้อถัดไป
  • Concurrency: การจัดการงานหลายงานที่คาบเกี่ยวกันในช่วงเวลาเดียวกัน
  • Dependency: package หรือบริการที่โปรแกรมต้องพึ่งพา
  • Deployment: การนำ artifact ไปติดตั้งใน environment ที่ใช้งาน
  • Exception: สัญญาณว่าการทำงานปกติถูกขัดจังหวะ
  • Fixture: ข้อมูลหรือ resource สำหรับเตรียม test
  • Idempotency: เรียก operation ซ้ำแล้วผลลัพธ์ไม่เพิ่มความเสียหายหรือข้อมูลซ้ำ
  • Interface/Protocol: สัญญาพฤติกรรมที่ object ต้องจัดให้
  • Migration: การเปลี่ยน schema หรือข้อมูลจากรุ่นหนึ่งสู่อีกรุ่น
  • Observability: ความสามารถในการเข้าใจระบบจาก logs, metrics และ traces
  • Regression: ปัญหาเดิมกลับมาเกิดหลังการเปลี่ยนแปลง
  • Repository: abstraction สำหรับอ่าน/เขียนข้อมูล
  • Rollback: การนำระบบหรือข้อมูลกลับไปสู่รุ่นที่ใช้งานได้ก่อนหน้า
  • Serialization: การแปลง object เป็นรูปแบบที่บันทึกหรือส่งต่อได้
  • Timeout: ขีดเวลารอ operation ก่อนยอมแพ้
  • Traceback: ลำดับการเรียก function ที่นำไปสู่ exception
  • Virtual environment: สภาพแวดล้อมแยก dependencies ของแต่ละโปรเจกต์

ภาคผนวก J: เส้นทางหลังจบคู่มือ

หลังผ่านระดับ 8 ให้เลือกการเติบโตต่อไปอย่างน้อยหนึ่งแบบ:

  • เข้าร่วมโปรเจกต์ open source และเริ่มจาก issue ที่มีขอบเขตชัด
  • รับผิดชอบระบบเล็กในงานจริงและสร้าง runbook ให้ทีม
  • อ่านหนังสือหรือ paper เฉพาะสาขาแล้วทำ replication project
  • สร้าง package ที่แก้ปัญหาเฉพาะและดูแล release ต่อเนื่อง
  • ฝึก code review และเขียน design document ให้ผู้อื่นวิจารณ์
  • สอนหรือเขียนบทความเพื่อทดสอบว่าตนเองอธิบาย mental model ได้จริง

ทักษะระดับมืออาชีพไม่ได้จบที่การรู้ syntax แต่คือความสามารถในการตั้งคำถามที่ถูก สร้างหลักฐาน ตัดสินใจภายใต้ข้อจำกัด และดูแลผลลัพธ์หลังส่งมอบ

ภาคผนวก K: Template โปรเจกต์ Python สำหรับเริ่มงานจริง

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

K.1 โครงสร้างเริ่มต้น

my-project/
├── src/
│   └── my_project/
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       └── domain.py
├── tests/
│   └── test_domain.py
├── docs/
│   └── architecture.md
├── .github/
│   └── workflows/
│       └── checks.yml
├── .gitignore
├── .env.example
├── README.md
└── pyproject.toml

K.2 pyproject.toml ขั้นต่ำ

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "my-project"
version = "0.1.0"
description = "A small Python application"
requires-python = ">=3.12"
dependencies = []

[project.optional-dependencies]
dev = [
    "pytest>=8",
    "mypy>=1.10",
    "ruff>=0.5",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

[tool.mypy]
python_version = "3.12"
strict = true
files = ["src"]

[tool.ruff]
line-length = 88
target-version = "py312"

ติดตั้งโปรเจกต์พร้อมเครื่องมือพัฒนา:

python -m pip install -e ".[dev]"

-e ทำให้แก้ source แล้วทดสอบได้ทันทีโดยไม่ต้องติดตั้ง package ใหม่ทุกครั้ง ใช้กับ development environment เท่านั้น ส่วน artifact สำหรับส่งมอบควร build จาก source ที่ระบุ version ชัดเจน

K.3 จุดเริ่มต้นของ package

src/my_project/__init__.py:

"""Public package interface."""

from .domain import add

__all__ = ["add"]

src/my_project/__main__.py:

from my_project.domain import add


def main() -> None:
    print(add(2, 3))


if __name__ == "__main__":
    main()

รันได้ด้วย:

python -m my_project

K.4 Domain ที่ทดสอบง่าย

src/my_project/domain.py:

def add(left: int, right: int) -> int:
    return left + right

เริ่มจาก function เล็กเพื่อให้เห็นวงจร package และ test ก่อนขยายเป็นกฎธุรกิจจริง อย่าเริ่มด้วย framework หรือ database หากยังไม่รู้ว่า use case หลักคืออะไร

K.5 Test แรก

tests/test_domain.py:

from my_project.domain import add


def test_adds_two_numbers() -> None:
    assert add(2, 3) == 5


def test_adds_negative_number() -> None:
    assert add(-2, 3) == 1

รันชุดตรวจ:

python -m pytest
python -m mypy src
python -m ruff check .
python -m ruff format --check .

K.6 Configuration อย่างปลอดภัย

.env.example เก็บเฉพาะชื่อค่าที่ต้องมี ไม่ใส่ค่าจริง:

APP_ENV=development
DATABASE_URL=data/app.db
LOG_LEVEL=INFO

.gitignore:

.venv/
__pycache__/
*.py[cod]
.env
.pytest_cache/
.mypy_cache/
.ruff_cache/
dist/
build/
*.egg-info/
data/*.db

ตรวจ repository ก่อน push ด้วยการค้นหาชื่อไฟล์และรูปแบบ secret ไม่ควรพึ่ง .gitignore อย่างเดียว เพราะไฟล์ที่เคย commit แล้วจะยังอยู่ในประวัติแม้เพิ่มเข้า ignore ภายหลัง

K.7 CI ขั้นต่ำ

.github/workflows/checks.yml:

name: checks

on:
  push:
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python -m pip install --upgrade pip
      - run: python -m pip install -e ".[dev]"
      - run: python -m pytest
      - run: python -m mypy src
      - run: python -m ruff check .
      - run: python -m ruff format --check .

CI ไม่ได้แทนการ review และ exploratory testing แต่เป็นประตูขั้นต่ำที่ทุกการเปลี่ยนแปลงต้องผ่านก่อนรวมเข้า branch หลัก

K.8 README ที่เริ่มจากเครื่องว่าง

README ควรมีขั้นตอนที่ผู้อื่นคัดลอกได้:

# My Project

## Requirements

- Python 3.12+

## Setup

```bash
python -m venv .venv
source .venv/bin/activate  # Windows ใช้ .venv\\Scripts\\Activate.ps1
python -m pip install -e ".[dev]"

Run

python -m my_project

Test

python -m pytest

Known limitations

ระบุสิ่งที่ระบบยังไม่รองรับอย่างตรงไปตรงมา


ทดสอบ README ด้วยการสร้างโฟลเดอร์ชั่วคราวหรือเครื่องใหม่ อย่าถือว่าเอกสารถูกต้องเพียงเพราะผู้เขียนเคยรันใน environment เดิม

### K.9 เมื่อใดควรขยาย template

เพิ่มโครงสร้างเมื่อมีปัญหาจริง:

- เพิ่ม `application/` เมื่อ use case มีหลายขั้นและต้องประสานหลาย adapter
- เพิ่ม `adapters/` เมื่อเชื่อม database, API หรือ queue หลายชนิด
- เพิ่ม `migrations/` เมื่อ schema มีการเปลี่ยนแปลงที่ต้องติดตาม
- เพิ่ม `scripts/` เมื่อมีงานดูแลระบบที่ต้องทำซ้ำและตรวจสอบได้
- เพิ่ม `docs/adr/` เมื่อมี architectural decision หลายรายการ
- เพิ่ม container/deployment files เมื่อมี environment ที่ต้องทำซ้ำ

อย่าสร้างโฟลเดอร์ล่วงหน้าทุกแบบ เพราะโครงสร้างที่ว่างเปล่าทำให้ค้นหา code ยากและบังคับ abstraction ก่อนมีความต้องการ

### K.10 Template ผ่านเมื่อ

- ผู้เรียนสร้าง environment ใหม่และติดตั้ง package ได้
- `python -m my_project` ทำงานจาก repository root
- test, type check, lint และ format check ผ่านคำสั่งที่ระบุ
- secret และข้อมูลจริงไม่อยู่ใน repository
- ผู้ร่วมงานใหม่เข้าใจวิธีเริ่มจาก README
- การขยาย domain ไม่จำเป็นต้องแก้ CI หรือ package entry point โดยไม่เกี่ยวข้อง

## ภาคผนวก L: Walkthrough โปรเจกต์รายรับ–รายจ่าย

โปรเจกต์นี้เป็นเส้นด้ายร่วมของคู่มือ ตั้งแต่โปรแกรมไม่กี่บรรทัดจนถึงระบบที่มี database, tests, packaging และ deployment จุดประสงค์ไม่ใช่ให้ทุกคนใช้โปรแกรมนี้ในงานจริง แต่ให้เห็นว่าความสามารถแต่ละระดับเชื่อมต่อกันอย่างไร

### L.1 รุ่น 0: ระบุปัญหา

ปัญหา:

> ผู้ใช้ต้องการบันทึกรายรับและรายจ่าย ดูยอดคงเหลือ และค้นหารายการย้อนหลัง โดยไม่ต้องคำนวณด้วยตนเอง

ขอบเขตรุ่นแรก:

- เพิ่มรายการ
- แสดงรายการ
- คำนวณยอดคงเหลือ
- ปิดและเปิดโปรแกรมได้โดยข้อมูลยังอยู่

นอกขอบเขตรุ่นแรก:

- การเชื่อมบัญชีธนาคาร
- การแบ่งผู้ใช้หลายคน
- การวิเคราะห์การลงทุน
- การส่งข้อมูลไปบริการภายนอก

### L.2 รุ่น 1: โมเดลข้อมูล

เริ่มจากข้อมูลตัวอย่าง ไม่เริ่มจากหน้าจอ:

```python
transaction = {
    "id": 1,
    "title": "อาหารกลางวัน",
    "amount": 120.0,
    "kind": "expense",
    "created_at": "2026-08-04T12:00:00+00:00",
}

กฎพื้นฐาน:

  • title ต้องไม่ว่าง
  • amount ต้องมากกว่าศูนย์
  • kind ต้องเป็น income หรือ expense
  • created_at ต้องเป็น timestamp ที่มี timezone
  • id ต้องไม่ซ้ำ

เขียนกฎเป็น function ก่อน:

def validate_transaction(title: str, amount: float, kind: str) -> None:
    if not title.strip():
        raise ValueError("ชื่อต้องไม่ว่าง")
    if amount <= 0:
        raise ValueError("จำนวนเงินต้องมากกว่าศูนย์")
    if kind not in {"income", "expense"}:
        raise ValueError("ประเภทต้องเป็น income หรือ expense")

L.3 รุ่น 2: กฎธุรกิจ

def calculate_balance(transactions: list[dict]) -> float:
    balance = 0.0
    for item in transactions:
        if item["kind"] == "income":
            balance += item["amount"]
        elif item["kind"] == "expense":
            balance -= item["amount"]
        else:
            raise ValueError(f"ประเภทไม่ถูกต้อง: {item['kind']}")
    return balance

เขียน test ก่อนเพิ่มเมนู:

def test_balance_empty() -> None:
    assert calculate_balance([]) == 0


def test_balance_income_and_expense() -> None:
    items = [
        {"amount": 1000, "kind": "income"},
        {"amount": 250, "kind": "expense"},
    ]
    assert calculate_balance(items) == 750


def test_balance_rejects_unknown_kind() -> None:
    with pytest.raises(ValueError):
        calculate_balance([{"amount": 10, "kind": "unknown"}])

L.4 รุ่น 3: Storage interface

กำหนดพฤติกรรมที่ application ต้องการก่อนเลือกเทคโนโลยี:

from typing import Protocol


class TransactionRepository(Protocol):
    def add(self, item: dict) -> int:
        ...

    def list_all(self) -> list[dict]:
        ...

    def delete(self, transaction_id: int) -> None:
        ...

สร้าง MemoryRepository เพื่อทดลอง use case อย่างรวดเร็ว:

class MemoryRepository:
    def __init__(self) -> None:
        self._items: dict[int, dict] = {}
        self._next_id = 1

    def add(self, item: dict) -> int:
        transaction_id = self._next_id
        self._next_id += 1
        self._items[transaction_id] = {
            **item,
            "id": transaction_id,
        }
        return transaction_id

    def list_all(self) -> list[dict]:
        return list(self._items.values())

    def delete(self, transaction_id: int) -> None:
        self._items.pop(transaction_id, None)

ถ้า business test ผ่านกับ repository นี้ แปลว่ากฎไม่ได้ผูกกับ SQLite โดยไม่จำเป็น

L.5 รุ่น 4: Application service

from datetime import datetime, timezone


class ExpenseService:
    def __init__(self, repository: TransactionRepository) -> None:
        self.repository = repository

    def add(self, title: str, amount: float, kind: str) -> int:
        validate_transaction(title, amount, kind)
        item = {
            "title": title.strip(),
            "amount": amount,
            "kind": kind,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
        return self.repository.add(item)

    def balance(self) -> float:
        return calculate_balance(self.repository.list_all())

สังเกตว่า service ไม่รู้ว่า repository เก็บข้อมูลด้วย dictionary, JSON หรือ SQLite และไม่รู้ว่าผู้ใช้กดปุ่มหรือส่ง HTTP request

L.6 รุ่น 5: SQLite adapter

ให้ SQLite adapter ทำหน้าที่แปลงระหว่างแถวในฐานข้อมูลกับ dictionary ตาม schema ที่กำหนด ใช้ parameterized query และ transaction เสมอ:

class SQLiteRepository:
    def __init__(self, connection) -> None:
        self.connection = connection

    def add(self, item: dict) -> int:
        cursor = self.connection.execute(
            """
            INSERT INTO transactions (title, amount, kind, created_at)
            VALUES (?, ?, ?, ?)
            """,
            (
                item["title"],
                item["amount"],
                item["kind"],
                item["created_at"],
            ),
        )
        self.connection.commit()
        return int(cursor.lastrowid)

    def list_all(self) -> list[dict]:
        rows = self.connection.execute(
            """
            SELECT id, title, amount, kind, created_at
            FROM transactions
            ORDER BY created_at DESC
            """
        ).fetchall()
        return [dict(row) for row in rows]

    def delete(self, transaction_id: int) -> None:
        self.connection.execute(
            "DELETE FROM transactions WHERE id = ?",
            (transaction_id,),
        )
        self.connection.commit()

ในระบบจริงให้เพิ่มการตรวจจำนวนแถวที่ถูกลบและกำหนดว่าจะเกิดอะไรเมื่อ id ไม่พบ เช่น แจ้ง NotFoundError หรือถือว่า operation idempotent ตาม requirement

L.7 รุ่น 6: CLI adapter

CLI ควรแปลงข้อความเป็น command และแสดงผล ไม่ควรคำนวณกฎธุรกิจเอง:

def run_add(service: ExpenseService) -> None:
    title = input("ชื่อรายการ: ")
    amount = float(input("จำนวนเงิน: "))
    kind = input("ประเภท (income/expense): ")
    transaction_id = service.add(title, amount, kind)
    print(f"บันทึกแล้ว id={transaction_id}")

ในรุ่นถัดไปต้องเพิ่ม input validation ที่ไม่ทำให้ CLI ปิดเมื่อผู้ใช้พิมพ์ผิด และแยก formatter ออกจาก service เพื่อทดสอบผลลัพธ์ได้ง่าย

L.8 รุ่น 7: Test matrix

Domain:
  - รายการว่าง
  - รายรับเดียว
  - รายจ่ายเดียว
  - รายรับและรายจ่ายหลายรายการ
  - kind ไม่ถูกต้อง

Validation:
  - title ว่าง
  - amount เป็นศูนย์
  - amount ติดลบ
  - input ที่แปลงเป็นตัวเลขไม่ได้

Storage:
  - insert และ read กลับได้เท่าเดิม
  - delete id ที่มีอยู่
  - delete id ที่ไม่มี
  - transaction rollback เมื่อเกิด error

CLI:
  - แสดงข้อความสำเร็จ
  - แสดงข้อความผิดพลาดที่ผู้ใช้แก้ได้
  - ออกจากโปรแกรมได้ทุกเมนู

L.9 รุ่น 8: ส่งมอบ

ก่อนส่งมอบ ให้ทำตามลำดับ:

  1. สร้างฐานข้อมูลว่างใน environment ใหม่
  2. รัน migration/schema setup
  3. รัน test, type check และ lint
  4. ทดลองเพิ่ม แก้ ลบ และ export ข้อมูล
  5. ปิดและเปิดโปรแกรมใหม่
  6. ทดลอง backup และ restore
  7. ตรวจ .gitignore และค้นหา secret
  8. ให้คนอื่นทำตาม README โดยไม่ช่วยระหว่างทาง
  9. แก้จุดที่คนอื่นติดขัด
  10. tag รุ่นและเขียน changelog

L.10 สิ่งที่โปรเจกต์นี้สอน

โปรเจกต์เดียวนี้ครอบคลุมแนวคิดหลักของคู่มือ:

  • ระดับ 0–1: input, condition, loop, collections และ function
  • ระดับ 2: files, JSON, CSV, SQLite และ modules
  • ระดับ 3: tests, type hints, logging และ refactoring
  • ระดับ 4: protocol, context manager, schema evolution และ package API
  • ระดับ 5: state, queue, performance และ failure handling
  • ระดับ 6: requirement, security, CI/CD, deployment และ runbook
  • ระดับ 7: นำ core ไปประยุกต์ในสาย Web, Automation, Data, AI หรือ Desktop
  • ระดับ 8: รับโจทย์ใหม่ ส่งมอบ และปรับตาม change request

ภาคผนวก M: เปลี่ยนโปรเจกต์เป็น Portfolio ระดับมืออาชีพ

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

M.1 โครงเรื่องของผลงาน

หน้า project overview ควรตอบคำถามเหล่านี้ภายในไม่กี่นาที:

  1. ระบบนี้แก้ปัญหาอะไร
  2. ใครใช้และ workflow สำคัญคืออะไร
  3. คุณรับผิดชอบส่วนใด
  4. architecture หลักเป็นอย่างไร
  5. trade-off ที่สำคัญมีอะไร
  6. test และคุณภาพวัดอย่างไร
  7. deploy และ monitor อย่างไร
  8. ข้อจำกัดปัจจุบันคืออะไร
  9. หากมีเวลาเพิ่มจะทำอะไรต่อ

ตัวอย่างโครง README:

# Expense Tracker

ระบบบันทึกรายรับรายจ่ายสำหรับผู้ใช้คนเดียว

## Problem
อธิบายปัญหาที่ต้องการลดและกลุ่มผู้ใช้

## Features
รายการความสามารถที่เสร็จแล้วและยังไม่เสร็จ

## Architecture
ลิงก์ไปยัง diagram และ ADR

## Quick start
คำสั่งติดตั้งและรันจากเครื่องว่าง

## Quality
คำสั่ง test, type check, lint และ coverage ที่ใช้

## Security
ขอบเขตข้อมูลที่เก็บและสิ่งที่ยังไม่รองรับ

## Operations
backup, restore, logs และ troubleshooting

## Trade-offs
เหตุผลที่เลือก SQLite, CLI หรือ framework ที่ใช้

## Roadmap
การปรับปรุงที่เรียงตามคุณค่าและความเสี่ยง

M.2 หลักฐานที่ควรแนบ

  • screenshot หรือ terminal transcript ของการใช้งานสำคัญ
  • test report จาก CI
  • architecture diagram รุ่นที่อ่านง่าย
  • ADR ที่อธิบายการตัดสินใจ 2–5 รายการ
  • benchmark ก่อนและหลังปรับปรุง หากมี performance claim
  • threat model และ security checklist
  • runbook ที่ผู้ทดลองคนอื่นทำตามได้
  • changelog ที่แสดงวิวัฒนาการของระบบ

อย่าแนบ token, customer data, private endpoint หรือ screenshot ที่มีข้อมูลส่วนตัว แม้เป็นโปรเจกต์ทดลองก็ตาม

M.3 เขียน Case Study

ใช้โครง Problem → Constraints → Decision → Evidence → Result → Next step:

Problem: การรวมรายงานใช้เวลานานและเกิดรายการซ้ำ
Constraints: ไม่มี server กลางและต้องรันบนเครื่องผู้ใช้
Decision: ใช้ SQLite + idempotency key + export CSV
Evidence: test ซ้ำสองครั้งไม่สร้างรายการเพิ่ม และเวลารันลดจาก ... เป็น ...
Result: ผู้ใช้ตรวจรายงานได้โดยไม่รวมข้อมูลซ้ำ
Next step: เพิ่ม scheduled run และ alert เมื่อ source file ผิด schema

ใช้ตัวเลขที่วัดได้เมื่ออ้างผลลัพธ์ อย่าเขียนว่า “เร็วขึ้นมาก” หากไม่มี baseline และวิธีวัด

M.4 เตรียมสาธิตภายใน 10 นาที

ลำดับที่แนะนำ:

  1. 1 นาที: ปัญหาและผู้ใช้
  2. 2 นาที: workflow ปกติ
  3. 2 นาที: architecture และ data flow
  4. 2 นาที: แสดง test/CI/security evidence
  5. 1 นาที: failure case และการกู้คืน
  6. 1 นาที: trade-off
  7. 1 นาที: next step หรือ change request ที่รับได้

ทำ demo data เตรียมไว้ แต่ต้องมีขั้นตอนสร้างใหม่เพื่อป้องกัน demo ที่ผ่านเพราะ state เก่าโดยไม่ตั้งใจ

M.5 คำถาม review ที่ควรตอบได้

ด้านโค้ด

  • ถ้าเพิ่ม field ใหม่ ต้องแก้ที่ใดบ้าง
  • ถ้า database unavailable จะเกิดอะไรขึ้น
  • function ใดเป็นจุดที่ควรแยกหรือ refactor ต่อ
  • test ใดจะจับบั๊กจาก change request ใหม่

ด้านระบบ

  • จุดใดเป็น single point of failure
  • หาก request ถูกส่งซ้ำ ระบบป้องกันอย่างไร
  • จะ scale ส่วนใดก่อนและเพราะอะไร
  • metric ใดบอกว่าผู้ใช้ได้รับผลกระทบ

ด้านความปลอดภัย

  • ผู้ใช้ A ป้องกันไม่ให้เห็นข้อมูลของ B อย่างไร
  • secret ถูกโหลดและหมุนเวียนอย่างไร
  • log ใดอาจมีข้อมูลอ่อนไหว
  • หาก backup รั่ว ความเสียหายจำกัดอย่างไร

ด้านการตัดสินใจ

  • ทางเลือกที่ไม่ได้เลือกมีอะไร
  • assumption ใดอาจผิด
  • เมื่อใดควรเปลี่ยนจาก SQLite เป็น database server
  • สิ่งใดเป็น technical debt ที่ยอมรับโดยเจตนา

M.6 Portfolio maturity levels

ระดับหลักฐาน
Starterโปรแกรมรันได้ มี README และตัวอย่างใช้งาน
Appliedมี persistence, tests และการจัดการ error
Professionalมี architecture, CI, security, deployment และ runbook
Maintainedมี monitoring, release history, change requests และ postmortem

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

M.7 แผนการปรับปรุงหลัง feedback

หลังให้ผู้อื่นทดลองใช้ ให้จัด feedback เป็นกลุ่ม:

  • blocker: ใช้งานต่อไม่ได้
  • correctness: ผลลัพธ์ผิด
  • usability: เข้าใจหรือใช้งานยาก
  • reliability: ล้มเหลวเป็นบางครั้ง
  • security: มีความเสี่ยงหรือข้อมูลรั่ว
  • maintainability: ทีมแก้ต่อยาก
  • nice-to-have: เพิ่มคุณค่าแต่ไม่เร่งด่วน

แก้ blocker และ correctness ก่อน feature ใหม่ สร้าง issue ที่มี reproduction, expected behavior และ acceptance criteria แล้วเพิ่ม regression test ก่อนแก้

M.8 Release note ของรุ่นแรก

## 0.1.0 - Initial release

### Added
- เพิ่มและดูรายการรายรับรายจ่าย
- คำนวณยอดคงเหลือ
- ส่งออก CSV

### Security
- ใช้ parameterized SQL
- ไม่เก็บ secret ในฐานข้อมูล

### Known limitations
- รองรับผู้ใช้คนเดียว
- ยังไม่มีการ sync ข้ามเครื่อง
- การ restore ต้องทำตาม runbook

### Verification
- pytest: pass
- mypy: pass
- manual smoke test: pass

M.9 เกณฑ์ Portfolio พร้อมเผยแพร่

  • repository สะอาด ไม่มี secret หรือข้อมูลจริง
  • README ทำตามได้จากเครื่องว่าง
  • demo data สร้างใหม่ได้
  • CI ล่าสุดผ่าน
  • architecture และ trade-off อธิบายได้
  • มีหลักฐาน security และ failure handling
  • มี release tag และ changelog
  • known limitations ระบุครบ
  • ผู้ทดลองที่ไม่ใช่ผู้เขียนทำตามได้
  • ผู้เขียนตอบคำถาม review และ change request ได้

ภาคผนวก N: Failure catalog สำหรับผู้เรียน

การติดขัดไม่ได้แปลว่าไม่เหมาะกับการเขียนโปรแกรม แต่ต้องแยกให้ได้ว่าควรแก้ที่ความเข้าใจ ข้อมูล เครื่องมือ หรือการออกแบบ

N.1 ระดับ 0–1

NameError

สาเหตุทั่วไป:

  • สะกดชื่อตัวแปรไม่ตรง
  • ใช้ตัวแปรก่อนกำหนดค่า
  • อยู่คนละ scope

วิธีตรวจ: พิมพ์ชื่อที่ใช้จริง เทียบตัวอักษรทีละตัว และดูบรรทัดที่ error ชี้ ไม่ควรแก้ด้วยการสร้างตัวแปรสุ่มจน error หาย

TypeError

มักเกิดจากนำชนิดข้อมูลต่างกันมาทำ operation เช่น ต่อ string กับ int ให้ตรวจชนิดด้วย type() แล้วตัดสินใจว่าจะ convert หรือแก้ data contract

โปรแกรมวนไม่จบ

ตรวจว่าตัวแปรควบคุม loop เปลี่ยนเข้าใกล้เงื่อนไขหยุดหรือไม่ เพิ่ม counter/limit ชั่วคราว และเขียน test ของกรณีที่ควรหยุด

N.2 ระดับ 2

ไฟล์ภาษาไทยอ่านเพี้ยน

กำหนด encoding="utf-8" ทั้งตอนอ่านและเขียน อย่าแก้ด้วยการเปลี่ยน font อย่างเดียว เพราะปัญหาอาจอยู่ที่ bytes ของไฟล์

JSON โหลดไม่ได้

แยกให้ชัดว่าเป็นไฟล์ไม่พบ, permission, encoding หรือ JSONDecodeError สำรองไฟล์ก่อนแก้ และอย่าเขียนทับข้อมูลที่เสียโดยไม่มีหลักฐาน

ข้อมูลซ้ำในฐานข้อมูล

ตรวจว่าคำสั่งถูกเรียกซ้ำหรือไม่ มี unique constraint หรือ idempotency key หรือไม่ และ transaction commit อยู่ตำแหน่งที่ถูกต้องหรือไม่

SQL ทำงานแต่ผลลัพธ์ผิด

ตรวจ parameter order, timezone, NULL, join และเงื่อนไข filter อย่าดูเพียงว่า query ไม่มี exception

N.3 ระดับ 3–4

Test ผ่านแต่ผู้ใช้ยังเจอปัญหา

อาจทดสอบ implementation ไม่ใช่ behavior, ไม่มี integration test หรือ test data ง่ายเกินจริง เพิ่ม test จากรายงาน failure ของผู้ใช้และทำ reproduction ก่อนแก้

Type checker รายงานจำนวนมาก

อย่าปิดทั้งหมดด้วย ignore แบ่งแก้ตาม boundary: input, domain, storage แล้วค่อยเพิ่ม strictness ทีละโมดูล

Abstraction มากเกินไป

ถ้าต้องเปิดไฟล์หลายชั้นเพื่อหากฎง่าย ๆ ให้ลด abstraction รวม logic ที่เปลี่ยนด้วยเหตุผลเดียวกัน และคง interface เฉพาะจุดที่มีโอกาสเปลี่ยนจริง

Decorator ทำให้ debug ยาก

ใช้ functools.wraps, ตั้งชื่อ wrapper ให้ชัด และอย่าซ้อน decorator หลายชั้นโดยไม่มี documentation ของลำดับการทำงาน

N.4 ระดับ 5

ปรับ algorithm แล้วช้าลง

อาจมี overhead, data size ยังเล็ก, cache ไม่เหมาะ หรือ bottleneck จริงอยู่ที่ I/O ให้เก็บ benchmark ก่อนและหลังด้วย workload เดียวกัน

Thread ทำให้ผลลัพธ์ไม่แน่นอน

ตรวจ shared mutable state, ลำดับการเขียน และการปิด worker ใช้ queue/message passing หรือ lock ที่ขอบเขตเล็กแทนการแชร์ state กว้าง

Retry ทำให้ระบบยิ่งแย่

จำกัดจำนวนครั้ง ใช้ exponential backoff + jitter ตรวจว่า operation idempotent และอย่า retry error ที่ไม่มีทางสำเร็จ เช่น validation หรือ authorization

Queue ค้างโดยไม่รู้สาเหตุ

บันทึก queue depth, age ของงาน, state และ last error เพิ่ม timeout และ dead-letter policy สำหรับงานที่ล้มซ้ำ

N.5 ระดับ 6–8

Deploy ผ่านแต่ระบบใช้งานไม่ได้

แยก build success จาก runtime health ตรวจ environment variables, migration, network access, permissions และ smoke test หลัง deploy

Rollback code แล้ว database ใช้ไม่ได้

ใช้ expand-and-contract migration และทดสอบ rollback กับข้อมูลจำลองก่อน production อย่าคิดว่า rollback artifact จะย้อน schema ให้เอง

Log มีข้อมูลเยอะ แต่หาสาเหตุไม่ได้

เพิ่ม request id, event name, entity id ที่ไม่อ่อนไหว, duration และ error category จัดระดับ log และกำหนด retention ให้เหมาะสม

Security review พบปัญหาหลังทำเสร็จ

ทำ threat model ตั้งแต่ requirement ตรวจ boundary และ authorization ในทุก use case ใช้ dependency scan และ security test เป็นส่วนหนึ่งของ CI

Scope โตจนส่งมอบไม่ได้

กลับไป project charter แยก must-have กับ nice-to-have ลด MVP ให้เล็กลง ส่งรุ่นที่พิสูจน์คุณค่าได้ แล้วใช้ feedback วางรุ่นถัดไป

N.6 วิธีบันทึก failure ให้เกิดประโยชน์

อาการ:
ขั้นตอนทำให้เกิดซ้ำ:
ผลลัพธ์ที่คาด:
ผลลัพธ์ที่เกิด:
หลักฐาน (traceback/log/test):
สมมติฐาน:
การทดลองที่ทำ:
สาเหตุที่ยืนยันแล้ว:
การแก้:
regression test:
สิ่งที่ควรป้องกันครั้งต่อไป:

บันทึกสาเหตุที่ยืนยันแล้ว ไม่ใช่เพียงวิธีที่ทำให้ error หายชั่วคราว Failure catalog ของตนเองจะกลายเป็นคู่มือ debugging ที่ตรงกับบริบทงานมากกว่ารายการ error ทั่วไป \n+# ภาคผนวก O: ระบบสอบวัดผลสำหรับการเรียนด้วยตนเอง

การอ่านจบไม่ใช่หลักฐานว่าใช้งานได้ การประเมินควรมีทั้งความเข้าใจ แนวคิด การลงมือทำ และการรับมือโจทย์ใหม่

O.1 Diagnostic ก่อนเริ่ม

ทำโดยไม่เปิดคู่มือ:

  1. อธิบาย Input → Process → Output ของโจทย์หนึ่งข้อ
  2. เขียน function รับ list ของตัวเลขแล้วคืนค่าค่าเฉลี่ย
  3. ตรวจว่าข้อมูลเป็นจำนวนเต็มบวกหรือไม่
  4. อ่าน JSON จาก string และจัดการกรณีข้อมูลเสีย
  5. อธิบายข้อดีของ list กับ dictionary
  6. เขียน test อย่างน้อยสองกรณี
  7. อธิบายว่าทำไมการต่อ input เข้ากับ SQL จึงอันตราย
  8. อ่าน traceback สั้น ๆ แล้วชี้บรรทัดที่ควรเริ่มตรวจ

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

O.2 Checkpoint exam ระดับ 0–2

สร้างโปรแกรม “บันทึกการอ่านหนังสือ” ที่รับชื่อหนังสือ จำนวนหน้า และหน้าที่อ่านแล้ว ปฏิเสธค่าที่ไม่ถูกต้อง คำนวณเปอร์เซ็นต์ เก็บรายการลง JSON โหลดกลับเมื่อเริ่มโปรแกรม export CSV และมี test ของ function สำคัญ

ผ่านเมื่อโปรแกรมทำงานจากเครื่องใหม่ตาม README และข้อมูลเดิมไม่เสียเมื่อผู้ใช้ป้อนค่าผิด

O.3 Checkpoint exam ระดับ 3–4

ปรับโปรแกรมเดิมให้แยก domain, service และ repository มี in-memory และ SQLite repository ใช้ type hints, pytest fixture, schema version, migration และ logging ที่ไม่เปิดเผยข้อมูลส่วนตัว

ผู้ประเมินควรเปลี่ยน repository ใน test โดยไม่แก้ service และเพิ่ม field ใหม่โดยไม่ทำให้ข้อมูลรุ่นเก่าอ่านไม่ได้

O.4 Checkpoint exam ระดับ 5–6

สร้างระบบคิวประมวลผลรายงานที่มี priority, state machine, retry, backoff, timeout, dead-letter handling, worker model ที่มี benchmark, error contract, CI, security review, configuration และ runbook

ผู้ประเมินจะส่ง change request ให้รองรับการยกเลิกงานและ resume หลัง process restart ต้องเพิ่ม test และเอกสารประกอบ

O.5 Practical exam ระดับ 7

เลือกสายใดสายหนึ่งและสร้างระบบที่ไม่มี tutorial เฉลย:

  • Web/API: ระบบจองทรัพยากร
  • Automation: pipeline รวมและตรวจไฟล์
  • Data: pipeline พร้อม quality checks
  • AI/ML: model service พร้อม baseline และ monitoring
  • Desktop/GUI: โปรแกรมข้อมูลที่มี persistence และ backup

ต้องส่ง charter, architecture, backlog, source, tests, deployment evidence, security review และ demo script

O.6 Final review ระดับ 8

ให้ผู้ทดลองที่ไม่ใช่ผู้เขียนทำตาม README แล้วเก็บจุดติดตั้งไม่ผ่าน คำถาม bug เวลาที่ใช้ และความเข้าใจ จากนั้นเลือกปัญหาหนึ่งข้อมาแก้เป็นรุ่นถัดไป พร้อม release note และ postmortem สั้น ๆ

O.7 เกณฑ์ให้คะแนน

ให้คะแนน 0–3 ต่อหัวข้อ:

หัวข้อ0123
ความถูกต้องใช้ไม่ได้ผ่านกรณีง่ายผ่าน failure สำคัญมีหลักฐานและ regression ครบ
การออกแบบไม่มีโครงโครงผูกกันมากแยกหน้าที่เหมาะสมอธิบาย trade-off และปรับขยายได้
การทดสอบไม่มีtest บางส่วนครอบคลุม behavior หลักรับ change request แล้วยังปลอดภัย
ความปลอดภัยไม่ตรวจรู้ความเสี่ยงบางข้อมี controls หลักมี threat model และ evidence
การส่งมอบรันได้เฉพาะเครื่องผู้เขียนมีคำสั่งบางส่วนทำตาม README ได้deploy/rollback/restore ทำซ้ำได้
การสื่อสารอธิบายไม่ได้อธิบายภาพรวมอธิบายการตัดสินใจตอบ review และสอนผู้อื่นได้

ผ่านระดับมืออาชีพเมื่อไม่มีหัวข้อใดต่ำกว่า 2 และค่าเฉลี่ยอย่างน้อย 2.5 โดยต้องแก้หัวข้อที่ต่ำกว่าก่อนอ้างว่า “พร้อมส่งมอบ”

O.8 หลักฐานการเรียนรู้ที่ควรเก็บ

วันที่สอบ:
ระดับ/สาย:
โจทย์:
repository หรือ artifact:
test report:
security/performance evidence:
ข้อผิดพลาดที่พบ:
change request:
สิ่งที่แก้ในรุ่นถัดไป:
คะแนนตาม rubric:
ผู้ review:

หลักฐานเหล่านี้ใช้ติดตามพัฒนาการของตนเองและช่วยให้ผู้ว่าจ้างหรือทีมใหม่เห็นความสามารถจากผลงานจริง ไม่ใช่จากรายการหัวข้อที่อ่านผ่านเท่านั้น

ภาคผนวก P: แผนที่การนำทางและเส้นทางทบทวน

P.1 เส้นทางแนะนำสำหรับผู้เริ่มต้น

ระดับ 0
  -> ระดับ 1
  -> โปรเจกต์รายรับ–รายจ่ายรุ่นหน่วยความจำ
  -> ระดับ 2
  -> โปรเจกต์รายรับ–รายจ่ายถาวร
  -> ระดับ 3
  -> package ที่มี tests
  -> ระดับ 4
  -> ระดับ 5
  -> ระดับ 6
  -> เลือกสายระดับ 7
  -> Capstone ระดับ 8

อย่าข้ามโปรเจกต์เพียงเพราะเข้าใจตัวอย่าง การสร้างระบบใหม่จากข้อกำหนดที่ต่างออกไปคือหลักฐานว่าความรู้ถูกถ่ายโอนไปใช้งานได้

P.2 เส้นทางสำหรับผู้มีพื้นฐาน

ทำ diagnostic ระดับ 0–2 ก่อน หากทำโจทย์ได้โดยไม่ดูเฉลย ให้เริ่มที่ระดับ 3 และทำโปรเจกต์ storage สองแบบ หากผ่านแล้วค่อยทดสอบระดับ 4–6 ด้วย practical exam อย่าข้ามการประเมินเพียงเพราะคุ้นชื่อหัวข้อ

P.3 ตาราง prerequisite

หัวข้อต้องเข้าใจก่อน
Files/JSONvariables, strings, exceptions เบื้องต้น
SQLitedictionaries, functions, SQL พื้นฐาน
pytestfunction, return, assertions
Type hintsfunction signature และ collections
Protocolfunction, class และ dependency separation
Concurrencyloops, functions, exceptions, resource lifecycle
APIHTTP, JSON, timeout และ authentication
AI/MLNumPy, pandas, statistics และ data validation
CI/CDGit, package install, tests และ configuration
Capstoneระดับ 6 และสายเชี่ยวชาญระดับ 7

P.4 เส้นทางทบทวนเมื่อเจอปัญหา

flowchart TD
    P[ปัญหาที่พบ] --> Q{เกี่ยวกับ syntax หรือไม่?}
    Q -- ใช่ --> S[ทบทวนระดับ 0-1]
    Q -- ไม่ใช่ --> D{เกี่ยวกับ data/storage หรือไม่?}
    D -- ใช่ --> F[ทบทวนระดับ 2]
    D -- ไม่ใช่ --> T{เกี่ยวกับโครงสร้างและ test หรือไม่?}
    T -- ใช่ --> C[ทบทวนระดับ 3-4]
    T -- ไม่ใช่ --> Y{เกี่ยวกับ performance/concurrency หรือไม่?}
    Y -- ใช่ --> A[ทบทวนระดับ 5]
    Y -- ไม่ใช่ --> E[ทบทวนระดับ 6-8 และสายที่เลือก]

P.5 แผนทบทวนแบบเว้นระยะ

ไม่จำเป็นต้องกำหนดตารางเวลา แต่ควรเว้นระยะให้สมองดึงความรู้กลับมา:

  1. หลังจบบท: เขียนสรุปจากความจำและทำโจทย์สั้น
  2. หลังจบระดับ: สร้าง mini-project โดยไม่เปิดตัวอย่าง
  3. เมื่อผ่านไปช่วงหนึ่ง: ทำ checkpoint ซ้ำโดยไม่ดูโค้ดเดิม
  4. หลังมีโปรเจกต์ใหม่: เปรียบเทียบ architecture และ failure ที่พบ
  5. ก่อนเริ่มหัวข้อใหม่: อธิบาย prerequisite ด้วยภาษาตนเอง

หากทำได้เฉพาะเมื่อเปิดหนังสือ แปลว่ายังอยู่ในขั้นรู้จัก ไม่ใช่ขั้นใช้งาน ให้ลดโจทย์และเพิ่ม retrieval practice

P.6 ดัชนีค้นหาหัวข้อ

พื้นฐานภาษา

ค้นในบท: ตัวแปร, ชนิดข้อมูล, input, print, operator, Boolean, condition, loop, string, list, dictionary, set, function, scope

การเก็บข้อมูล

ค้นในบท: pathlib, encoding, JSON, CSV, exception, SQLite, CRUD, transaction, migration, backup

คุณภาพโค้ด

ค้นในบท: type hints, dataclass, pytest, fixture, refactoring, logging, protocol, composition, public API

Algorithms และระบบ

ค้นในบท: Big-O, search, sort, stack, queue, heap, recursion, state machine, HTTP, timeout, thread, process, async, profiling

Professional Engineering

ค้นในบท: requirement, acceptance criteria, ADR, Git workflow, code review, security, authentication, authorization, CI/CD, deployment, observability, runbook

สายเชี่ยวชาญ

ค้นในบท: Web/API, Automation, Data, AI/ML, Desktop/GUI, portfolio, capstone, change request

P.7 Definition of Done สำหรับต้นฉบับคู่มือ

คู่มือฉบับสมบูรณ์ควรผ่านการตรวจดังนี้:

  • สารบัญตรงกับหัวข้อและลิงก์ภายในทำงาน
  • ตัวอย่างโค้ดทุกชิ้นผ่านการรันบน Python รุ่นที่กำหนด
  • แบบฝึกทุกระดับมี expected behavior และเกณฑ์ผ่าน
  • ไม่มีตัวอย่างที่พึ่ง API หรือ package รุ่นเก่าโดยไม่เตือน
  • โปรเจกต์ต่อเนื่องมีจุดตรวจและการเปลี่ยนรุ่นที่ชัด
  • ภาพประกอบอธิบายความสัมพันธ์ที่ข้อความอย่างเดียวเข้าใจยาก
  • คำศัพท์ใช้สม่ำเสมอทั้งเล่ม
  • README และคำสั่งติดตั้งผ่านการทดลองจาก environment ใหม่
  • เนื้อหาสำคัญมี security, testing, error handling และ accessibility
  • มีการตรวจทานลิขสิทธิ์และเขียนตัวอย่างขึ้นใหม่
  • ผู้อ่านทดลองอย่างน้อยหนึ่งกลุ่มทำโปรเจกต์ได้โดยไม่ต้องช่วยทีละบรรทัด

เมื่อ checklist นี้ผ่าน จึงควรสร้างฉบับ PDF/เว็บไซต์สำหรับเผยแพร่ และเก็บ source, code examples, tests และ assets ไว้ใน repository เดียวกันอย่างตรวจสอบย้อนหลังได้


บทส่งท้าย: เริ่มจากสิ่งเล็กและสร้างหลักฐาน

การเขียนโปรแกรมไม่ได้เกิดจากการอ่านคำสั่งให้ครบ แต่เกิดจากการเปลี่ยนปัญหาให้เป็นขั้นตอน ทดลอง สังเกต failure และปรับปรุงอย่างมีหลักฐาน เส้นทางในคู่มือนี้จึงเริ่มจาก print() และจบที่การรับผิดชอบระบบที่ผู้อื่นใช้งานได้ ไม่ใช่เพราะคำสั่งแรกเล็กหรือระบบสุดท้ายใหญ่ แต่เพราะวิธีคิดเดียวกันขยายได้ตลอดทาง

เมื่อเริ่มเรียน ให้ทำเพียงสามอย่าง:

  1. เลือกโจทย์เล็กที่มีประโยชน์กับตัวคุณ
  2. เขียนผลลัพธ์ที่คาดหวังและ test case ก่อนเพิ่มความซับซ้อน
  3. บันทึกสิ่งที่ผิดพลาดและสิ่งที่เรียนรู้ทุกครั้ง

อย่ารอให้รู้ Python ทั้งหมดก่อนเริ่มโปรเจกต์ และอย่าใช้โปรเจกต์ใหญ่เป็นหลักฐานเดียวว่าตนเองก้าวหน้า ความสามารถที่ยั่งยืนคือการแบ่งปัญหาให้เล็กพอจะตรวจสอบได้ แล้วค่อยเพิ่มความสามารถโดยไม่ทำลายสิ่งที่ทำงานอยู่

เมื่อพบ library ใหม่ ให้เรียนจาก documentation เมื่อพบ error ให้สร้าง minimal reproduction เมื่อพบ design ที่ไม่แน่ใจ ให้เขียนทางเลือกและ trade-off เมื่อระบบถูกใช้งานจริง ให้ฟังผู้ใช้ วัดผล และปรับปรุง

วันที่คุณสามารถทำสิ่งเหล่านี้ได้โดยไม่ต้องรอให้ใครสั่ง คุณไม่ได้เพียง “เขียน Python ได้” แต่กำลังทำงานแบบนักพัฒนาซอฟต์แวร์มืออาชีพแล้ว

งานแรกหลังอ่านบทนี้

สร้าง repository ใหม่ ทำไฟล์ README.md เขียนปัญหาหนึ่งข้อที่คุณพบในชีวิตประจำวัน แล้วระบุ:

ผู้ใช้คือใคร:
ปัญหาคืออะไร:
ผลลัพธ์ที่ต้องการ:
input แรก:
output แรก:
กรณีผิดพลาดหนึ่งกรณี:
test แรก:

จากนั้นสร้างโปรแกรมที่เล็กที่สุดซึ่งแสดงผลลัพธ์แรกได้ commit ลง Git และเขียนบันทึกสั้น ๆ ว่าคุณจะขยายมันอย่างไรในรุ่นถัดไป นั่นคือจุดเริ่มต้นของเส้นทางทั้งหมด


แหล่งอ้างอิงและนโยบายการใช้เนื้อหา

คู่มือนี้ใช้เอกสาร PDF ในโฟลเดอร์โครงการเป็นแหล่งสำรวจหัวข้อ รูปแบบการสอน ตัวอย่างแนวคิด และแบบฝึกหัดที่ควรมี แต่ตัวอย่างภาษาไทย โค้ด โครงเรื่อง แบบฝึก และคำอธิบายในคู่มือนี้เขียนขึ้นใหม่เพื่อให้เหมาะกับเส้นทาง self-study และไม่คัดลอกข้อความยาวจากต้นฉบับ

เอกสารที่ใช้เป็นแหล่งศึกษา

  1. The Python Bible 7 in 1 — ใช้สำรวจเส้นทางจาก Python พื้นฐานไปยัง OOP, files, databases, data science, machine learning และ computer vision
  2. Python Cookbook — ใช้สำรวจ recipes ด้าน collections, classes, generators, algorithms, regular expressions และ database access
  3. Python Programming: A Step-by-Step Guide for Absolute Beginners — ใช้ศึกษาแนวการอธิบายผู้เริ่มต้นและหัวข้อ syntax, functions, modules, files, GUI และ database
  4. Python Programming for Beginners: From Basics to AI Integrations — ใช้สำรวจการเชื่อมพื้นฐานกับ data wrangling, debugging, OOP และ AI/ML
  5. How to Program: Computer Science Concepts and Python Exercises — ใช้เป็นแนวทางด้าน problem solving, top-down design, testing, abstraction, debugging, algorithms และ data structures

กฎตรวจสอบความถูกต้อง

  • เอกสาร Python และไลบรารีทางการมีสิทธิ์เหนือคำอธิบายใน PDF เมื่อข้อมูลขัดกัน
  • โค้ดทุกชิ้นต้องรันบน Python รุ่นที่คู่มือระบุ ก่อนเผยแพร่
  • API, package version, installation และ security guidance ต้องตรวจสอบใหม่เมื่อมีการอัปเดต
  • ตัวอย่างจาก PDF ที่ใช้ Python รุ่นเก่าต้องปรับหรือระบุข้อจำกัดอย่างชัดเจน
  • แนวคิดจากหลายแหล่งต้องสังเคราะห์เป็นคำอธิบายใหม่ ไม่เรียงหรือคัดลอกต้นฉบับตามลำดับเดิม

วิธีอ้างอิงภายในต้นฉบับ

เมื่อบทใดได้รับอิทธิพลจากแนวคิดเฉพาะ ให้บันทึกใน source note ของ repository เช่น:

บท: การอ่าน traceback
แนวคิดที่ใช้: systematic debugging และ minimal reproduction
แหล่งสำรวจ: How to Program, Python Programming for Beginners
ตรวจสอบเพิ่มเติม: Python traceback documentation
ตัวอย่างและแบบฝึก: เขียนใหม่สำหรับคู่มือนี้

เก็บ source note แยกจากเนื้อหาที่ผู้อ่านต้องอ่าน เพื่อให้คู่มือไม่กลายเป็นการอ้างอิงแบบรบกวนการเรียน แต่ยังตรวจสอบที่มาของแนวคิดได้เมื่อจำเป็น