목차
미션 파이썬
퀴즈 게임(미션 2)과 Mini NPU 시뮬레이터(미션 3)를 완주하는 데 필요한 최소한의 파이썬만 담은 교재입니다. 문법 백과가 아니라, 각 장을 읽고 나면 미션 요구사항 하나를 직접 구현할 수 있도록 구성했습니다.
왜 이만큼만 배우는가
세 미션의 요구사항을 분석하면, 파이썬이 실제로 필요한 곳은 명확합니다.
- 미션 1 (개발 워크스테이션): 터미널·Docker·Git 실습이 중심입니다. 웹 서버도 정적 HTML + NGINX로 충분해서 파이썬 문법이 거의 필요 없습니다. 이 책에서는 터미널에서 파이썬을 실행·확인하는 방법만 다룹니다.
- 미션 2 (퀴즈 게임): 변수, 자료형, 조건문, 반복문, 함수, 클래스 2개, JSON 파일 저장(
state.json), try/except 예외 처리가 전부 요구사항에 명시되어 있습니다. - 미션 3 (Mini NPU): 2차원 리스트, 중첩 반복문으로 MAC 연산, 부동소수점 epsilon 비교, data.json 로드와 키 파싱, time으로 성능 측정이 핵심입니다. 외부 라이브러리는 금지이므로 표준 문법만으로 해결해야 합니다.
따라서 이 책은 아래 매핑을 커리큘럼으로 삼습니다. 어떤 장을 왜 배우는지 항상 미션 요구사항으로 되짚을 수 있습니다.
| 장 | 배우는 것 | 직접 구현하게 되는 미션 요구사항 |
|---|---|---|
| 1 | 실행, print, 주석 | M1 파이썬 설치 확인 · M2/M3 python main.py 실행 |
| 2 | 변수, int/float/str/bool, input, 형변환 | M2 §3 입력 처리(공백 제거·숫자 변환) · M3 모드 1 숫자 파싱 |
| 3 | list, dict, 중첩 구조 | M2 퀴즈 목록/선택지 · M3 n×n 패턴·필터 저장 |
| 4 | if/elif/else, 범위 검사, epsilon 비교 | M2 메뉴 분기·정답 판정 · M3 점수 비교 정책(1e-9) |
| 5 | for, while, 중첩 루프 | M2 메뉴 루프·퀴즈 출제 · M3 MAC 연산(반복문 직접 구현) |
| 6 | 함수, 매개변수, 반환값 | M2 기능별 함수 분리 · M3 연산/판정 함수 |
| 7 | try/except, KeyboardInterrupt, EOFError | M2 §3 공통 예외 기준 · M3 케이스 단위 FAIL 처리 |
| 8 | class, __init__, self, 메서드 | M2 Quiz·QuizGame 클래스(필수 2개) |
| 9 | open, with, json.load/dump | M2 state.json 저장/복구 · M3 data.json 로드·스키마 검증 |
| 10 | time.perf_counter, random, 문자열 메서드 | M3 10회 반복 평균 측정·O(N²) 표 · M2 보너스 랜덤 출제 |
| 11 | 전체 조립 | M2·M3 프로그램 뼈대와 제출 체크리스트 |
미션 2를 먼저 진행한다면 1→8장을 순서대로 읽고 바로 구현을 시작하세요. 미션 3만 남았다면 3·4·5·6·9·10장이 핵심입니다. 각 장 끝의 연습은 미션 코드의 축소판이므로 건너뛰지 않는 것을 권합니다.
실행하고 출력하기
미션 2는 Python 3.10 이상, 미션 3은 3.8 이상을 요구합니다. 터미널에서 버전부터 확인합니다.
$ python3 --version
Python 3.12.4
$ python3 main.py # 프로젝트 폴더에서 프로그램 실행
파이썬 파일(.py)은 위에서 아래로 한 줄씩 실행됩니다. 화면 출력은 print()가 담당합니다.
# main.py — # 뒤는 주석: 실행되지 않는 설명
print("=" * 40) # 문자열 × 정수 = 반복 → 구분선을 손으로 치지 않는다
print("=== Mini NPU Simulator ===")
print("1. 사용자 입력 (3x3)")
print("2. data.json 분석")
print("-" * 40)
print()는 여러 값을 쉼표로 이어 출력할 수 있고, 자동으로 줄바꿈합니다. 미션 2의 메뉴 화면, 미션 3의 결과 표는 결국 print()의 나열입니다.
맥에서는 python이 아니라 python3일 수 있습니다. 또 main.py가 있는 폴더로 cd한 뒤 실행해야 미션 3의 data.json 같은 상대 경로 파일을 제대로 찾습니다.
미션 2 예시의 메뉴 화면(제목 줄, 1~5번 항목, 구분선)을 print()만으로 그대로 출력하는 menu.py를 만들어 실행해 보세요.
변수·자료형·입력
변수: 값에 이름 붙이기
변수는 값을 담아두는 이름표입니다. 프로그램이 기억해야 하는 것 — 최고 점수, 현재 문제 번호, 필터 크기 — 은 전부 변수에 저장됩니다.
best_score = 0 # int : 정수 (점수, 정답 번호, 크기 N)
score_a = 0.9 # float : 실수 (MAC 점수는 float 가능)
question = "Python의 창시자는?" # str : 문자열
is_correct = True # bool : True / False 둘 중 하나
네 가지 타입의 차이를 설명할 수 있어야 합니다(미션 2 과제 목표에 명시).
int— 셀 수 있는 수. 메뉴 번호, 정답 번호(1~4), 최고 점수.float— 소수점이 있는 수. 미션 3의 점수·시간(ms). 오차가 있을 수 있음(4장에서 다룸).str— 글자들. 문제 텍스트, 파일에서 읽은 내용,input()의 결과는 항상 str.bool— 참/거짓. 판정 결과, 반복 계속 여부.
f-string: 문자열 안에 값 끼워 넣기
total, correct = 5, 4
print(f"🏆 결과: {total}문제 중 {correct}문제 정답! ({correct * 20}점)")
print(f"평균 시간: {0.0123456:.3f} ms") # :.3f → 소수점 3자리
🏆 결과: 5문제 중 4문제 정답! (80점)
평균 시간: 0.012 ms
미션 3의 성능 표(소수점 자리 맞춤)와 미션 2의 결과 화면은 f-string 하나로 해결됩니다.
input(): 사용자 입력 받기
input()은 안내 문구를 출력하고, 사용자가 Enter를 칠 때까지 기다렸다가 항상 문자열(str)을 돌려줍니다. 숫자로 쓰려면 변환이 필요합니다.
raw = input("선택: ") # 사용자가 " 1 " 입력했다면 raw는 " 1 "
raw = raw.strip() # 앞뒤 공백 제거 → "1" (미션 2 필수 요구)
choice = int(raw) # 문자열 → 정수. "abc"면 ValueError 발생(7장)
미션 2 §3 공통 입력 기준의 첫 항목이 바로 "입력 앞뒤 공백 제거 후 처리"입니다. input().strip()을 습관으로 만들면 요구사항 하나가 자동으로 해결됩니다. 빈 입력(그냥 Enter)은 raw == ""로 검사합니다.
한 줄에서 숫자 여러 개 읽기 — 미션 3의 행 입력
미션 3 모드 1은 "한 줄에 숫자 3개를 공백으로 구분해" 입력받습니다. split()이 문자열을 공백 기준으로 쪼개 리스트로 만들어 줍니다.
line = input().strip() # "0 1 0"
parts = line.split() # ["0", "1", "0"] ← 아직 문자열
row = [float(p) for p in parts] # [0.0, 1.0, 0.0] ← 숫자로 변환
print(len(row)) # 3 — 열 개수 검증에 사용
마지막 줄의 [float(p) for p in parts]는 리스트 컴프리헨션으로, "parts의 각 항목 p를 float(p)로 바꾼 새 리스트"라는 뜻입니다. 5장의 for문을 한 줄로 줄인 표현이라, 지금은 관용구로 외워 두어도 됩니다.
한 줄을 입력받아 ① 공백을 제거하고 ② 공백 기준으로 쪼갠 뒤 ③ 숫자 개수가 3개인지 len()으로 확인해 "OK" 또는 "입력 형식 오류: 각 줄에 3개의 숫자를 공백으로 구분해 입력하세요."를 출력하는 프로그램을 작성하세요. 미션 3의 입력 검증 문구 그대로입니다.
리스트와 딕셔너리
리스트: 순서가 있는 묶음
choices = ["Guido", "Linus", "Bjarne", "James"]
choices[0] # "Guido" — 인덱스는 0부터!
choices[3] # "James"
len(choices) # 4
choices.append("Anders") # 끝에 추가 → 퀴즈 추가 기능의 핵심
choices.pop(1) # 인덱스 1 항목을 꺼내며 제거 → 퀴즈 삭제 보너스
del choices[0] # 그냥 지우기만 할 때
삭제 후에는 뒤 항목들의 인덱스가 하나씩 당겨진다는 점만 주의하세요. 미션 2 보너스 4(퀴즈 삭제)는 사용자에게 번호를 받아 quizzes.pop(번호 - 1) 하고 파일에 다시 저장하면 됩니다.
미션 2에서 정답은 1~4번으로 관리하지만 리스트 인덱스는 0~3입니다. 사용자가 3을 입력하면 choices[3 - 1]이 해당 선택지입니다. 이 -1을 빠뜨리는 것이 퀴즈 게임에서 가장 흔한 버그입니다.
딕셔너리: 이름표가 붙은 묶음
딕셔너리(dict)는 키 → 값 쌍의 모음입니다. JSON과 구조가 같아서, 9장에서 파일 저장을 배우면 dict가 그대로 state.json이 됩니다.
quiz = {
"question": "Python의 창시자는?",
"choices": ["Guido", "Linus", "Bjarne", "James"],
"answer": 1,
}
quiz["question"] # 키로 값 꺼내기
quiz["answer"] = 2 # 값 바꾸기
quiz.get("hint") # 없는 키 → None (오류 대신). 검증에 유용
"answer" in quiz # True — 키 존재 확인 (스키마 검증)
None은 "값이 없음"을 나타내는 특별한 값입니다. get()이 없는 키에 대해 돌려주는 것이 바로 None이고, 검사할 때는 == 대신 if x is None:을 쓰는 것이 관례입니다. 9장에서 data["filters"].get(f"size_{n}")의 결과가 None인지 확인해 스키마 오류를 걸러낼 때 그대로 사용합니다.
중첩 구조 ① — 딕셔너리의 리스트 (미션 2)
퀴즈 여러 개는 "dict를 담은 list"입니다. state.json의 quizzes가 정확히 이 모양입니다.
quizzes = [
{"question": "...", "choices": [...], "answer": 3},
{"question": "...", "choices": [...], "answer": 2},
]
quizzes[0]["question"] # 첫 퀴즈의 문제 텍스트
중첩 구조 ② — 리스트의 리스트 = 2차원 배열 (미션 3)
미션 3의 패턴과 필터는 "행(row)들의 리스트"로 표현합니다. pattern[i][j]는 i행 j열의 값입니다.
cross = [ # 3×3 십자가 필터
[0, 1, 0],
[1, 1, 1],
[0, 1, 0],
]
cross[1][2] # 1행 2열 → 1
len(cross) # 행 수 = 3
len(cross[0]) # 열 수 = 3 → "필터와 패턴 크기 일치" 검증에 사용
미션 3 §1 데이터 구조 요구사항 — "n×n 패턴 저장, 특정 위치 값 읽기/쓰기, 3×3~25×25 처리" — 는 리스트의 리스트와 len()만으로 전부 충족됩니다. 별도의 배열 라이브러리가 필요 없고, 쓰면 오히려 규정 위반(외부 라이브러리 금지)입니다.
① 퀴즈 dict 2개를 담은 quizzes 리스트를 만들고, 두 번째 퀴즈의 3번 선택지를 출력하세요. ② 3×3 X 패턴을 리스트의 리스트로 만들고 가운데 값([1][1])을 출력하세요.
조건문과 값 비교
if / elif / else
메뉴에서 번호에 따라 다른 기능을 실행하는 것이 조건문의 전형적인 쓰임입니다.
if choice == 1:
play_quiz()
elif choice == 2:
add_quiz()
elif choice == 5:
print("저장 후 종료합니다.")
else:
print("⚠️ 잘못된 입력입니다. 1-5 사이의 숫자를 입력하세요.")
파이썬은 들여쓰기(스페이스 4칸)가 블록을 결정합니다. 콜론(:) 다음 줄부터 들여 쓴 부분까지가 해당 조건에 속합니다.
비교·논리 연산자와 범위 검사
answer == 3 # 같다 (= 하나는 대입, 두 개가 비교!)
answer != 3 # 다르다
1 <= n <= 4 # 범위 검사 — "허용 범위 밖 숫자" 걸러내기
raw == "" # 빈 입력 검사
a > b and c > d # 둘 다 참일 때
not quizzes # 리스트가 비었으면 True → "퀴즈가 없는 경우" 처리
부동소수점은 == 로 비교하면 안 된다
float는 이진 소수라서 미세한 오차를 갖습니다. 미션 3이 epsilon 정책을 필수로 요구하는 이유입니다.
print(0.1 + 0.2) 0.30000000000000004
print(0.1 + 0.2 == 0.3) False ← 함정!
그래서 "두 점수가 같다(동점)"는 차이가 아주 작은 허용오차(epsilon)보다 작다로 정의합니다.
EPSILON = 1e-9 # 0.000000001 — 상수는 대문자 이름 관례
if abs(score_a - score_b) < EPSILON:
verdict = "UNDECIDED" # 동점 → 판정 불가
elif score_a > score_b:
verdict = "A"
else:
verdict = "B"
이 여덟 줄이 미션 3 §6 "점수 비교 정책"과 §6-1 "판정 규칙"의 완성형입니다. abs()는 절댓값을 구하는 내장 함수입니다. 판정 결과가 expected와 같으면 PASS, 다르면 FAIL을 출력하는 것도 if verdict == expected: 한 줄입니다.
score_a = 0.9, score_b = 0.8999999999999999일 때 위 epsilon 판정 코드가 무엇을 출력하는지 예측한 뒤 실행해 확인하세요. 그다음 EPSILON을 1e-20으로 바꾸면 결과가 왜 달라지는지 설명해 보세요.
반복문과 2차원 순회
for: 정해진 목록을 순회
for quiz in quizzes: # 퀴즈를 하나씩 꺼내며
print(quiz["question"])
for i in range(4): # i = 0, 1, 2, 3
print(f"{i + 1}. {quiz['choices'][i]}")
for idx, quiz in enumerate(quizzes, start=1): # 번호와 함께
print(f"[{idx}] {quiz['question']}") # → 퀴즈 목록 화면
while: 조건이 참인 동안 반복
"종료를 선택할 때까지 메뉴를 계속 보여준다"는 while True + break가 표준 패턴입니다. for는 횟수·목록이 정해졌을 때, while은 언제 끝날지 사용자에게 달렸을 때 씁니다 — 이 구분이 미션 2 과제 목표 중 하나입니다.
while True:
show_menu()
choice = input("선택: ").strip()
if choice == "5":
break # 반복 탈출 → 프로그램 종료 수순
if choice not in ("1", "2", "3", "4"):
print("⚠️ 잘못된 입력입니다. 1-5 사이의 숫자를 입력하세요.")
continue # 아래를 건너뛰고 다음 반복(재입력)으로
...
중첩 for — 2차원을 훑는 법, 그리고 MAC 연산
바깥 for가 행을, 안쪽 for가 열을 돕니다. 미션 3의 핵심인 MAC(곱하고 누적해서 더하기) 연산은 중첩 for 안에서 곱을 누적하는 것이 전부입니다.
def mac(pattern, filt):
"""같은 위치끼리 곱해 모두 더한 점수를 반환 (외부 라이브러리 금지 → 직접 구현)"""
n = len(pattern)
total = 0.0 # 누적(Accumulate)용 변수
for i in range(n): # 행
for j in range(n): # 열
total += pattern[i][j] * filt[i][j] # 곱하고(Multiply) 더한다
return total
score = mac([[0,1,0],[1,1,1],[0,1,0]],
[[0,1,0],[1,1,1],[0,1,0]])
print(score) 5.0 ← 미션 문서의 Case 1과 동일
N×N 입력이면 곱셈이 N² 번 실행됩니다. 3×3은 9번, 25×25는 625번 — 이것이 미션 3 리포트에 쓸 O(N²)의 근거입니다. 코드에서 직접 세어 보여줄 수 있으니 리포트 작성이 쉬워집니다.
중첩 컴프리헨션과 평탄화 — 2차원을 1차원으로
2장의 [float(p) for p in parts]는 for절이 하나인 기본형이었습니다. for절은 여러 개 이어 쓸 수 있고, 읽는 규칙은 하나입니다: for절들은 왼쪽부터 순서대로 중첩 for문이 되고, 맨 앞의 표현식이 가장 안쪽에 들어간다.
flat = [value for row in matrix for value in row]
# 위 한 줄은 아래 중첩 for문과 완전히 같다
flat = []
for row in matrix: # ① 첫 번째 for절 — 바깥 루프
for value in row: # ② 두 번째 for절 — 안쪽 루프
flat.append(value) # ③ 맨 앞의 표현식 — 가장 안쪽
3×3 십자가에 적용하면 [0,1,0,1,1,1,0,1,0] — 2차원 리스트가 길이 N²의 1차원 리스트로 평탄화(flatten)됩니다. 이때 (i, j) 위치는 flat[i * n + j]에 대응합니다.
[value for value in row for row in matrix]처럼 순서를 뒤집으면 첫 for절이 아직 정의되지 않은 row를 쓰게 되어 NameError가 납니다. 앞의 for절이 만든 변수를 뒤의 for절이 쓴다(바깥→안 순서)고 기억하세요.
미션 3 보너스 1 "2차원 배열을 1차원 배열(길이 N²)로 변환해 접근 패턴 단순화"가 바로 이 문법입니다. 패턴과 필터를 각각 평탄화한 뒤 단일 루프 for k in range(n * n): total += flat_p[k] * flat_f[k]로 MAC을 구현하고, 10장의 measure_ms()로 전/후를 같은 입력·같은 반복 횟수로 측정해 비교하면 보너스 요구사항이 완성됩니다.
위 mac()에 미션 문서의 Case 2(십자가 입력 × X 필터)를 넣어 1.0이 나오는지 확인하세요. 이어서 크기 N을 입력받아 N×N 십자가 패턴(가운데 행·열이 1)을 중첩 for로 자동 생성해 보세요 — 미션 3 보너스 과제 2의 절반이 끝납니다.
함수
미션 2 제약 사항은 "모든 코드를 한 함수에 작성하지 말고 기능별로 분리"를 요구합니다. 함수는 입력(매개변수)을 받아 → 일을 하고 → 결과(반환값)를 돌려주는 이름 붙은 코드 블록입니다.
def calc_score(correct, total): # correct, total = 매개변수
if total == 0:
return 0 # return을 만나면 즉시 종료
return round(correct / total * 100)
score = calc_score(4, 5) # 호출 → 반환값 80을 score에 저장
값을 여러 개 돌려주기
미션 3에서는 "점수와 소요 시간"처럼 결과가 여럿인 경우가 많습니다. 쉼표로 묶어 반환하고, 받을 때도 쉼표로 풀면 됩니다.
def judge(score_cross, score_x, eps=1e-9): # eps는 기본값 있는 매개변수
if abs(score_cross - score_x) < eps:
return "UNDECIDED", False
if score_cross > score_x:
return "Cross", True
return "X", True
verdict, decided = judge(5.0, 1.0) # "Cross", True
함수로 나누는 기준
- 한 함수는 한 가지 일만:
show_menu(),play_quiz(),load_state(),mac(),measure()… - 화면 출력과 계산을 분리하면 테스트와 디버깅이 쉬워집니다.
mac()은 print 없이 값만 반환하고, 출력은 호출한 쪽이 담당하게 하세요. 미션 3의 "시간 측정은 I/O를 제외하고 연산 함수 구간 중심으로" 요구가 자연스럽게 충족됩니다. - 같은 코드를 두 번 쓰게 되면 함수로 뽑을 신호입니다(예: 3×3 필터 A·B 입력 →
read_matrix(3)하나로).
2장 연습의 "한 줄 검증"을 확장해, 크기 n을 받아 n줄을 입력받고 2차원 리스트를 반환하는 read_matrix(n) 함수를 작성하세요(형식 오류 시 재입력은 다음 장에서 붙입니다). 이 함수 하나가 미션 3 모드 1의 필터 A·B·패턴 입력 세 곳에 재사용됩니다.
예외 처리와 안전한 입력
int("abc")는 ValueError라는 예외를 일으키며 프로그램을 즉사시킵니다. try/except는 "일단 시도하고, 사고가 나면 이 코드로 수습하라"는 구조입니다.
try:
n = int("abc") # 여기서 ValueError 발생 →
except ValueError:
print("숫자가 아닙니다.") # → 죽지 않고 여기로 점프
표준 패턴: 유효한 숫자를 받을 때까지 묻는 함수
미션 2 §3의 네 가지 요구(공백 제거, 변환 실패, 범위 밖, 빈 입력)를 전부 처리하는 함수입니다. 이 함수 하나로 메뉴 선택·정답 입력·정답 번호 입력을 모두 해결합니다.
def read_int(prompt, lo, hi):
"""lo~hi 범위의 정수를 받을 때까지 재입력을 유도한다."""
while True:
raw = input(prompt).strip() # ① 공백 제거
if raw == "": # ② 빈 입력
print(f"⚠️ 입력이 비었습니다. {lo}-{hi} 숫자를 입력하세요.")
continue
try:
n = int(raw) # ③ 변환 실패 대비
except ValueError:
print(f"⚠️ 잘못된 입력입니다. {lo}-{hi} 사이의 숫자를 입력하세요.")
continue
if not (lo <= n <= hi): # ④ 범위 밖
print(f"⚠️ 잘못된 입력입니다. {lo}-{hi} 사이의 숫자를 입력하세요.")
continue
return n # 유효하면 반환하고 끝
choice = read_int("선택: ", 1, 5)
answer = read_int("정답 입력: ", 1, 4)
같은 패턴의 변형 — 텍스트 입력과 행렬 입력
"유효할 때까지 while True로 되묻는다"는 뼈대는 어떤 입력에도 재사용됩니다. 미션 2의 퀴즈 추가(문제·선택지는 빈 문자열만 아니면 됨)와 미션 3 모드 1의 행 입력(6장 연습에서 미뤄 둔 재입력 유도)이 각각 이렇게 완성됩니다.
def read_text(prompt):
"""빈 입력이 아닐 때까지 묻는다 — 퀴즈 문제/선택지 입력용."""
while True:
raw = input(prompt).strip()
if raw: # 비어 있지 않으면 참
return raw
print("⚠️ 입력이 비었습니다. 다시 입력하세요.")
def read_row(n):
"""숫자 n개가 든 한 행을 받을 때까지 재입력을 유도한다."""
while True:
parts = input().strip().split()
if len(parts) != n: # 열 개수 검증
print(f"입력 형식 오류: 각 줄에 {n}개의 숫자를 공백으로 구분해 입력하세요.")
continue
try:
return [float(p) for p in parts]
except ValueError: # 숫자 파싱 실패
print("입력 형식 오류: 숫자만 입력하세요.")
def read_matrix(n):
"""n×n 행렬 입력 — 필터 A·B·패턴 세 곳에서 재사용."""
return [read_row(n) for _ in range(n)]
이 세 함수로 미션 3 §2 모드 1 입력 검증(행/열 개수, 숫자 파싱, 재입력 유도)이 전부 충족됩니다. read_text()는 미션 2 퀴즈 추가 화면의 문제·선택지 4개 입력에 그대로 씁니다.
Ctrl+C에도 비정상 종료하지 않기
실행 중 Ctrl+C는 KeyboardInterrupt, 입력 스트림이 끊기면 EOFError 예외가 됩니다. 미션 2는 이때도 "저장 후 안전하게 종료"를 요구합니다. 프로그램의 최상위를 try로 감싸는 것이 정석입니다.
def main():
game = QuizGame() # 8장에서 배울 클래스
try:
game.run() # 메뉴 루프 전체
except (KeyboardInterrupt, EOFError):
print("\n입력이 중단되어 저장 후 종료합니다.")
game.save() # 가능한 범위에서 저장
if __name__ == "__main__": # "직접 실행했을 때만 main()을 호출" 관용구
main()
미션 3에서는 같은 도구를 다르게 씁니다 — 사용자 입력은 read_matrix() 안에서 재입력 유도, data.json의 케이스별 오류(크기 불일치 등)는 프로그램을 멈추지 말고 해당 케이스만 FAIL로 기록하고 다음 케이스로 넘어갑니다. "중단 없이 계속"이 채점 포인트입니다.
read_int()를 직접 타이핑해 실행하고 abc, 9, 빈 Enter, 3 (공백 포함) 네 가지를 차례로 넣어 모두 의도대로 동작하는지 확인하세요. 그대로 미션 2 제출 코드에 쓸 수 있습니다.
클래스
미션 2는 클래스 2개 이상을 요구합니다. 클래스는 "관련된 데이터(속성)와 그 데이터를 다루는 함수(메서드)를 한 덩어리로 묶는 설계도"입니다. 퀴즈 한 문제가 가진 것(문제·선택지·정답)과 할 수 있는 일(출력·채점)을 묶으면 Quiz 클래스가 됩니다.
class Quiz:
def __init__(self, question, choices, answer):
"""인스턴스가 만들어질 때 자동 호출되는 초기화 메서드"""
self.question = question # 속성(attribute)에 저장
self.choices = choices # 선택지 4개 리스트
self.answer = answer # 정답 번호 1~4
def show(self, number):
"""문제와 선택지를 출력한다."""
print(f"[문제 {number}] {self.question}")
for i, choice in enumerate(self.choices, start=1):
print(f" {i}. {choice}")
def check(self, user_answer):
"""정답이면 True를 반환한다."""
return user_answer == self.answer
설계도(클래스)에서 실제 물건(인스턴스)을 찍어내는 것이 호출입니다.
q = Quiz("Python의 창시자는?", ["Guido","Linus","Bjarne","James"], 1)
q.show(1) # 메서드 호출 — self에는 q가 자동으로 들어간다
q.check(1) # True
self는 "지금 이 메서드를 호출한 바로 그 인스턴스 자신"입니다. q.check(1)은 사실 Quiz.check(q, 1)과 같습니다. 그래서 모든 메서드의 첫 매개변수는 self이고, 호출할 때는 적지 않습니다. __init__ 안에서 self.question = question처럼 self에 붙여 저장해야 다른 메서드에서도 꺼내 쓸 수 있습니다.
QuizGame: 게임 전체를 관리하는 클래스
두 번째 클래스는 게임의 상태(퀴즈 목록, 최고 점수)와 흐름(메뉴, 저장/불러오기)을 담당합니다. 역할 분담: Quiz는 문제 한 개, QuizGame은 게임 전체.
class QuizGame:
def __init__(self):
self.quizzes = [] # Quiz 인스턴스들의 리스트
self.best_score = 0
self.load() # 시작하자마자 state.json 불러오기(9장)
def run(self):
while True:
self.show_menu()
choice = read_int("선택: ", 1, 5)
if choice == 1: self.play()
elif choice == 2: self.add_quiz()
elif choice == 3: self.show_list()
elif choice == 4: self.show_score()
else:
self.save()
print("저장 후 종료합니다.")
break
play(), add_quiz(), show_list(), show_score(), save(), load()는 각각 미션 2 기능 요구사항 6~11번과 1:1로 대응합니다. 이 뼈대를 채우는 것이 곧 미션 완수입니다.
Quiz 클래스를 완성하고, 인스턴스 2개를 리스트에 담아 for문으로 show() → read_int()로 답 입력 → check() 결과에 따라 "✅ 정답입니다!" / "❌ 오답입니다!"를 출력하는 미니 게임을 만들어 보세요. 이것이 play() 메서드의 본체입니다.
파일과 JSON
프로그램을 꺼도 데이터가 남으려면 파일에 써야 합니다. JSON은 dict/list를 텍스트로 저장하는 표준 형식이라, 파이썬 자료구조와 1:1로 오갑니다.
쓰기: dict → state.json
import json # 표준 라이브러리 — 파일 맨 위에서 가져온다
state = {
"quizzes": [{"question": "...", "choices": [...], "answer": 1}],
"best_score": 80,
}
with open("state.json", "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
with open(...) as f:— 블록이 끝나면 파일을 자동으로 닫아 줍니다. 항상 이 형태를 쓰세요.encoding="utf-8"— 미션 2 요구사항(한글 깨짐 방지).ensure_ascii=False— 한글을안같은 코드가 아니라 읽을 수 있는 글자로 저장.
읽기: 파일이 없거나 깨졌을 때까지 대비
미션 2는 "파일 없음 → 기본 데이터 사용, 손상 → 안내 후 복구"를 요구합니다. 예외 타입별로 나눠 받으면 됩니다.
def load_state():
try:
with open("state.json", "r", encoding="utf-8") as f:
return json.load(f) # 텍스트 → dict
except FileNotFoundError: # 첫 실행: 파일이 없다
return {"quizzes": DEFAULT_QUIZZES, "best_score": 0}
except json.JSONDecodeError: # 파일이 깨졌다
print("⚠️ 저장 파일이 손상되어 기본 데이터로 시작합니다.")
return {"quizzes": DEFAULT_QUIZZES, "best_score": 0}
JSON은 dict/list만 저장할 수 있고 Quiz 인스턴스는 모릅니다. 저장 시 {"question": q.question, "choices": q.choices, "answer": q.answer}로 풀고, 불러올 때 Quiz(d["question"], d["choices"], d["answer"])로 되살리세요. Quiz에 to_dict() 메서드, QuizGame.load()에 복원 코드를 두면 깔끔합니다.
미션 3: data.json 로드와 키 파싱
data.json은 filters(size_5·size_13·size_25)와 patterns(size_{N}_{idx} 키)로 구성됩니다. 딕셔너리 순회와 문자열 split()으로 키에서 N을 뽑아 맞는 필터를 고릅니다.
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
for name, case in data["patterns"].items(): # 키와 값을 함께 순회
n = int(name.split("_")[1]) # "size_13_1" → ["size","13","1"] → 13
filters = data["filters"].get(f"size_{n}")
pattern = case["input"]
if filters is None or len(pattern) != n: # 크기/스키마 검증
print(f"{name}: FAIL (필터/크기 불일치)") # 케이스만 FAIL, 계속 진행
continue
...
라벨 정규화도 dict 하나로 끝납니다. 요구사항의 "‘+’→Cross, ‘x’→X, ‘cross’→Cross"를 매핑 테이블로 만들면 if 사다리가 필요 없습니다.
LABELS = {"+": "Cross", "cross": "Cross", "x": "X", "X": "X"}
expected = LABELS.get(str(case["expected"]).strip().lower(), None)
① dict 하나를 test.json으로 저장했다가 다시 읽어 같은 내용인지 확인하세요. ② 파일을 텍스트 편집기로 일부러 망가뜨린 뒤(중괄호 삭제) load_state()가 안내 메시지와 함께 기본값을 돌려주는지 확인하세요. 이 실험이 미션 2 §11의 채점 시나리오입니다.
시간 측정과 표준 라이브러리
time.perf_counter — 연산 시간 재기
미션 3은 "각 크기별 MAC 연산을 10회 반복 측정해 평균(ms)"을 요구합니다. time.perf_counter()는 고해상도 시계로, 두 번 읽어 차이를 구하면 경과 시간(초)입니다.
import time
def measure_ms(pattern, filt, repeat=10):
"""MAC 연산만 repeat회 반복해 평균 소요 시간(ms)을 반환."""
start = time.perf_counter()
for _ in range(repeat): # _ : 값을 안 쓰는 반복 변수 관례
mac(pattern, filt) # 측정 구간에는 연산만! (I/O 제외)
elapsed = time.perf_counter() - start
return elapsed / repeat * 1000 # 초 → ms, 평균
성능 표 출력은 f-string 정렬 지시자로 맞춥니다. {n:>2}는 오른쪽 정렬 2칸, {ms:>10.3f}는 10칸에 소수점 3자리.
print(f"{'크기':<8}{'평균 시간(ms)':>14}{'연산 횟수':>10}")
for n, ms in results:
print(f"{n}×{n:<5}{ms:>14.3f}{n * n:>10}")
표에서 N이 5→25로 5배 되면 연산 횟수(N²)는 25배가 되고, 측정 시간도 대략 그 비율로 늘어납니다. "연산 횟수 열과 시간 열이 같은 비율로 증가한다"는 관찰이 README 결과 리포트의 O(N²) 근거 문단이 됩니다.
random — 미션 2 보너스
import random
random.shuffle(quizzes) # 리스트를 제자리에서 섞기 → 랜덤 출제
random.sample(quizzes, 3) # 3개만 뽑기 → 문제 수 선택 보너스
datetime — 날짜/시간 기록 (미션 2 보너스 5)
점수 기록 히스토리 보너스는 "날짜/시간, 푼 문제 수, 점수"를 저장해야 합니다. 표준 라이브러리 datetime으로 현재 시각을 문자열로 만들면 JSON에 그대로 저장할 수 있습니다.
from datetime import datetime
stamp = datetime.now().strftime("%Y-%m-%d %H:%M") # "2026-08-13 14:02"
history.append({"when": stamp, "total": 5, "score": 80})
# history 리스트를 state.json에 함께 저장하면 보너스 5 완성 (9장 참고)
자주 쓰는 문자열·내장 도구 요약
| 도구 | 하는 일 | 미션에서의 쓰임 |
|---|---|---|
s.strip() | 앞뒤 공백 제거 | 모든 입력 전처리 |
s.split("_") | 구분자로 쪼개 리스트로 | size_13_1 키 파싱, 행 입력 |
s.lower() | 소문자로 | 라벨 정규화(x/X) |
abs(x) | 절댓값 | epsilon 비교 |
len(x) | 길이/개수 | 행·열 검증, 퀴즈 개수 |
round(x, 1) | 반올림 | 점수 표시 |
enumerate(xs, 1) | 번호와 함께 순회 | 목록·선택지 출력 |
d.items() / d.get(k) | dict 순회 / 안전 조회 | patterns 순회, 스키마 검증 |
5장에서 만든 십자가 패턴 생성기를 이용해 N = 3, 5, 13, 25 각각의 패턴·필터를 만들고, measure_ms()로 측정한 결과를 위 표 형식으로 출력해 보세요. 완성되면 미션 3 §6-2 성능 분석 요구사항이 그대로 끝납니다.
미션 조립 가이드
배운 조각을 어떤 순서로 붙이면 되는지, 두 미션의 권장 구조와 작업 순서입니다. 코드를 베끼는 게 아니라 각 단계에서 어느 장을 다시 펴면 되는지를 안내합니다.
미션 2 — 퀴즈 게임 조립 순서
- 뼈대:
read_int()(7장) + 메뉴while루프(5장)만으로 "메뉴 → 잘못된 입력 처리 → 종료"가 도는 프로그램을 먼저 완성 → 커밋. - Quiz 클래스(8장) + 기본 퀴즈 5개를 코드에 정의 → 커밋.
- 퀴즈 풀기: 브랜치를 만들어(
git checkout -b feature/play) for 루프로 출제·채점(5·8장), 끝나면 점수 계산(6장) → 커밋 후 main에 병합. - 추가/목록/점수:
append(3장),enumerate(5장), 최고 점수 갱신 비교(4장) → 기능마다 커밋. - 저장/불러오기:
state.json입출력(9장), Ctrl+C 안전 종료(7장) → 커밋. - QuizGame으로 정리(8장) → README 작성 → 커밋 · 푸시, clone/pull 실습.
이 순서 자체가 "최소 10개의 의미 있는 커밋"과 브랜치·병합 요구를 자연스럽게 채워 줍니다.
미션 3 — Mini NPU 조립 순서
mac()함수(5장)를 만들고 미션 문서의 Case 1·2로 검증 — 5.0과 1.0이 나와야 합니다.read_matrix(n)(2·6·7장)으로 모드 1 입력 + 형식 오류 재입력.- epsilon 판정
judge()(4장) + 결과 출력. data.json로드, 키 파싱, 크기 검증, 라벨 정규화(9장). 오류 케이스는 FAIL 기록 후continue.measure_ms()(10장)로 3·5·13·25 성능 표 출력.- 총 테스트/통과/실패 요약(카운터 변수 + f-string), README 리포트 작성(O(N²) 근거는 10장).
제출 전 파이썬 체크리스트
- 모든
input()뒤에.strip()이 있는가? 빈 입력·문자·범위 밖을 넣어도 안 죽는가? - Ctrl+C를 눌러도 안내 후 종료되는가? (M2: 저장까지 되는가?)
state.json을 지우고 실행 / 일부러 깨뜨리고 실행 — 둘 다 살아남는가?- float 비교에
==를 쓴 곳이 없는가? (M3: epsilon 정책) - 클래스가 2개 이상이고, 한 함수가 50줄을 넘지 않는가?
- 외부 라이브러리 import가 없는가? (
json,time,random은 표준이라 허용)
미션 1의 Docker/Git 실습에서 만든 습관 — "명령을 치고, 출력을 확인하고, 기록한다" — 를 파이썬에도 그대로 적용하세요. 한 장을 읽을 때마다 예제를 직접 실행해 출력을 확인하는 것이, 읽기만 하는 것보다 완주 확률을 훨씬 높입니다.