파이썬 공부 Day 4: 제어문 if · while · for와 함수 시작C의 for는 세고, 파이썬의 for는 꺼냅니다
C를 먼저 배운 사람에게 파이썬 제어문은 쉬워 보입니다. if, while, for, break, continue. 이름이 전부 같습니다. 그런데 실습 코드를 짜 보니 같은 이름 아래 다른 규칙이 숨어 있었습니다.
4일차에는 제어문 세 가지와 입력 검사용 isnumeric(), C와의 반복문 비교, 그리고 함수의 첫 부분(*args, **kwargs)까지 공부했습니다. 실습하면서 제가 낸 실수도 같이 적어 둡니다.
🔀 if: 들여쓰기가 곧 문법
조건이 참일 때만 블록을 실행합니다. 파이썬은 중괄호가 없어서 들여쓰기로 블록을 나눕니다. 칸을 맞추는 일 자체가 문법입니다.
my_score = 76
if my_score >= 90:
grade = 'A'
elif my_score >= 80:
grade = 'B'
elif my_score >= 70:
grade = 'C'
else:
grade = 'F'
print(grade) # C
C의 else if는 elif입니다. 처음 True가 나온 블록 하나만 실행하고, 비워 둘 블록은 pass로 채웁니다.
조건 자리에는 값을 그대로 넣어도 됩니다. if score_list:는 리스트에 요소가 하나라도 있으면 True입니다. False로 취급되는 값은 False, None, 숫자 0(0, 0.0), 빈 값("", [], (), {}, set())이고 나머지는 전부 True입니다.
if를 쓰면서 본 에러는 전부 콜론과 들여쓰기에서 나왔습니다.
if search == '콜라'
print("A 코너로 가세요.")
# SyntaxError: 콜론 빠짐
if search == '콜라':
print("A 코너로 가세요.")
# IndentationError: 들여쓰기 안 함
if search == '콜라':
print("A 코너로 가세요.")
print("BBBB")
# IndentationError: 칸이 안 맞음
요일 문제: elif 일곱 번 대신 리스트 한 줄
오늘 요일을 한글로 출력하는 문제였습니다. weekday()는 요일을 숫자로 돌려줍니다(0이 월요일, 6이 일요일). if-elif를 일곱 번 쓰는 대신 요일 이름을 리스트에 넣고 인덱스로 꺼내면 끝납니다.
from datetime import datetime
# 0=월, 1=화, ..., 6=일
week_num = datetime.now().weekday()
day_name = ['월', '화', '수', '목',
'금', '토', '일']
print(day_name[week_num] + '요일')
# 10월 27일에 돌리면 화요일
제 실습 코드는 day_name = week_num이라고 써 버렸습니다. 리스트를 숫자로 덮어써서 요일 이름이 통째로 사라진 겁니다. 꺼내려면 day_name[week_num]처럼 인덱스로 접근해야 합니다.
내 실습 코드에서 찾은 or 함정
세 과목 중 하나라도 60점 이하이거나 평균이 70점 이하이면 Fail을 출력하는 문제였습니다. 저는 이렇게 썼습니다.
score1, score2, score3 = 61, 100, 76
avg = (score1 + score2 + score3) / 3
# 틀린 코드: score1, score2가 덜렁 있음
if (avg <= 70
or score1 or score2 or score3 < 60):
print("Fail") # Fail (Pass여야 하는데)
파이썬은 이 조건을 score1 or score2 or (score3 < 60)으로 읽습니다. score1이 0만 아니면 그 자체로 True라서 결과는 항상 Fail입니다. 실습 데이터가 (60, 100, 76)일 때는 원래 답이 Fail이라 우연히 맞았습니다. (61, 100, 76)으로 바꾸자 Pass여야 할 결과가 Fail로 나왔습니다.
C에서 a || b || c < 60이라고 써도 똑같이 동작합니다. 0이 아니면 참이라는 규칙은 C와 같은데, or라는 영어 단어로 쓰면 사람 말처럼 읽혀서 놓치기 쉽습니다.
고치는 방법은 둘입니다.
score1, score2, score3 = 61, 100, 76
avg = (score1 + score2 + score3) / 3
# 방법 1: 하나씩 비교 ("60점 이하"라서 <=)
if (avg <= 70 or score1 <= 60
or score2 <= 60 or score3 <= 60):
print("Fail")
else:
print("Pass") # Pass
# 방법 2: 제일 낮은 점수만 본다
low = min(score1, score2, score3)
if avg <= 70 or low <= 60:
print("Fail")
else:
print("Pass") # Pass
🔁 while: 조건이 True인 동안
while은 조건이 True인 동안 반복합니다. 안에서 조건을 바꾸는 줄(i += 1 같은)을 빼먹으면 영원히 끝나지 않습니다.
break와 continue
break는 반복문을 즉시 빠져나가고, continue는 아래 코드를 건너뛰고 다음 반복으로 갑니다. 1부터 100까지 짝수만 더하는 코드로 둘을 같이 써 봤습니다.
total, i = 0, 0
while True: # 일부러 만든 무한루프
i += 1
if i > 100:
break # 100을 넘으면 탈출
if i % 2 == 1:
continue # 홀수는 건너뜀
total += i
print(total) # 2550
아래 코드에서 필요 없는 두 줄을 찾는 연습문제도 있었습니다.
total, i = 0, 0
while True:
i += 1
if i > 100:
break
if i % 2 == 0:
total += i
else: # 필요 없음
continue # 필요 없음
print(total) # 2550
답은 맨 끝의 else와 continue입니다. 반복문의 마지막 줄이라 continue를 하지 않아도 어차피 다음 반복으로 넘어갑니다.
실수로 생긴 무한루프는 주피터에서는 정지 버튼, 터미널에서는 Ctrl+C로 끕니다. 참고한 자료에는 Ctrl+Z라고 나와 있었습니다. 그런데 맥 터미널에서 Ctrl+Z는 프로그램을 일시정지할 뿐이라 뒤에서 계속 살아 있습니다.
🔢 isnumeric(): int()로 바꾸기 전에 확인
input()은 무엇을 입력하든 항상 문자열을 돌려줍니다. 이걸 int()로 바꾸는데 "abc"가 들어오면 ValueError로 프로그램이 멈춥니다. 그래서 바꾸기 전에 isnumeric()으로 모든 글자가 숫자인지 먼저 확인합니다.
print("123".isnumeric()) # True
print("12a".isnumeric()) # False
print("-5".isnumeric()) # False
print("3.14".isnumeric()) # False
print("".isnumeric()) # False
마이너스와 소수점도 숫자가 아니라서, 키보드로 치는 숫자로는 0 이상의 정수만 통과합니다. 음수와 소수는 다음 날 공부한 try/except의 몫입니다.
예제는 while과 묶여 있었습니다. 문자를 넣으면 다시 입력받고, 0이면 종료하고, 숫자면 10으로 나눈 나머지를 출력합니다.
prompt = '숫자를 입력하세요(0 = 종료):'
msg = "{} 나누기 10의 나머지는 {}입니다."
while True:
response = input(prompt)
if not response.isnumeric():
continue # 숫자가 아니면 다시 입력
if int(response) == 0:
break
result = int(response) % 10
print(msg.format(response, result))
abc, 17, 0을 차례로 넣으면 결과는 17 나누기 10의 나머지는 7입니다. 한 줄만 찍힙니다.
형제 메서드 isdecimal(), isdigit()과는 "²" 같은 특수 숫자에서 갈립니다. "²".isnumeric()은 True인데 int("²")는 ValueError라서, int()로 바꿀 거면 isdecimal()이 가장 안전합니다. 세 메서드 비교표와 문제 풀이용 sys.stdin.readline 정리는 파이썬 치트시트에 모아 뒀습니다.
🔂 for: 하나씩 꺼내는 반복
for는 반복 대상(리스트, 문자열, range 등)에서 값을 하나씩 꺼내 변수에 담고, 꺼낼 게 없으면 끝납니다. range(1, 11)은 끝 숫자가 빠진 1~10이라, 연습문제는 범위 싸움이었습니다.
for ch in "hi":
print(ch) # h, i 한 줄씩
# 1부터 10까지 짝수 합 (2칸씩)
total = 0
for i in range(2, 11, 2):
total += i
print(total) # 30
# 1부터 100까지 합
total = 0
for i in range(1, 101):
total += i
print(total) # 5050
# 한 줄로: sum(range(1, 101))
# 1부터 100 중 7의 배수
for i in range(1, 101):
if i % 7 == 0:
print(i, end=' ')
# 7 14 21 ... 98
enumerate()와 zip(): 꺼내면서 짝짓기
몇 번째 요소인지 알고 싶으면 enumerate()를 씁니다. (순번, 요소) 튜플을 꺼내 주니 for i, m in은 Day 3에서 공부한 튜플 언패킹 그대로입니다. zip()은 리스트 여러 개를 같은 순번끼리 묶어 꺼냅니다.
member = ['유재석', '박명수', '정준하',
'노홍철', '정형돈']
# 순번이 짝수인 경우만 '순번: 이름'
for i, m in enumerate(member):
if i % 2 == 0:
print(f'{i}: {m}')
# 0: 유재석
# 2: 정준하
# 4: 정형돈
names = ['홍길동', '김철수']
scores = [85, 95]
for name, s in zip(names, scores):
print(name, s)
# 홍길동 85
# 김철수 95
헷갈린 건 그다음이었습니다. range(), enumerate(), zip(), map()은 함수처럼 부르지만 사실 클래스입니다. 그래서 print(zip(...))을 찍으면 리스트 대신 <zip object at 0x...>만 나오고, 안을 보려면 list()로 감쌉니다.
게다가 range를 뺀 나머지는 한 번 꺼내면 소진되는 이터레이터입니다. nums = map(int, ['1', '2', '3'])을 sum(nums)에 두 번 넣으면 6, 0이 나옵니다. 두 번 돌 거면 처음부터 list()로 바꿔 둡니다.
Day 2의 a, b = map(int, input().split())이 list() 없이 되는 것도 언패킹이 map에서 하나씩 꺼내 담기 때문입니다. 내장 클래스별 결과는 파이썬 치트시트에 정리했습니다.
리스트 컴프리헨션
for와 append 몇 줄을 한 줄로 줄이는 문법입니다. 형태는 [표현식 for 변수 in 반복대상 if 조건]이고, for를 먼저 읽고 if로 거른 뒤 맨 앞 표현식으로 바꿔 담는다고 읽습니다.
# 일반 for
squares = []
for n in range(1, 6):
squares.append(n ** 2)
# 컴프리헨션 (결과 같음)
squares = [n ** 2 for n in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
점수 리스트에서 음수와 100 초과를 걸러 내고 10으로 나누는 연습문제는 for와 append로 열 줄이 넘었습니다. 컴프리헨션으로는 세 줄입니다.
score = [75, 46, 93, -82, -77,
130, 100, 75, -10]
score01 = [x for x in score if x >= 0]
score02 = [x for x in score01 if x <= 100]
print(score02)
# [75, 46, 93, 100, 75]
score03 = [x / 10 for x in score02]
print(score03)
# [7.5, 4.6, 9.3, 10.0, 7.5]
any()와 all(): or 함정을 한 줄로
대괄호를 빼고 함수 괄호 안에 바로 쓰면 제너레이터 식이 됩니다. any()는 하나라도 True면 True, all()은 전부 True여야 True입니다. 앞의 or 함정도 이걸로 과목 수와 상관없이 풀립니다.
scores = [61, 100, 76]
avg = sum(scores) / len(scores)
has_low = any(s <= 60 for s in scores)
print(has_low) # False
if avg <= 70 or has_low:
print("Fail")
else:
print("Pass") # Pass
딕셔너리도 for로
딕셔너리를 그냥 돌리면 키만 나옵니다. 키와 값을 같이 꺼내려면 .items()가 정석이고, 학생별 점수 합을 구하는 연습문제도 이것 하나로 풀렸습니다.
score = {'홍길동': [85, 80, 90, 95],
'한사랑': [95, 90, 80, 90],
'일지매': [90, 70, 60, 80]}
for name, scores in score.items():
print(f'{name}: {sum(scores)}')
# 홍길동: 350
# 한사랑: 355
# 일지매: 300
🆚 C와 나란히 놓고 보면
1부터 10까지 더하는 코드를 두 언어로 써 봤습니다.
int total = 0;
for (int i = 1; i <= 10; i++) {
total += i;
}
printf("%d\n", total); // 55
int j = 1;
total = 0;
while (j <= 10) {
total += j;
j++;
}
printf("%d\n", total); // 55
total = 0
for i in range(1, 11):
total += i
print(total) # 55
total, j = 0, 1
while j <= 10:
total += j
j += 1
print(total) # 55
while은 두 언어가 거의 같고, 차이는 for에서 벌어집니다.
- 반복 중 i 수정: C는 반영되지만, 파이썬 for는 다음 반복에서 다음 값으로 덮어씁니다. C 습관대로
i를 건드려 건너뛰려 하면 아무 일도 안 일어납니다. - 반복 끝난 뒤 i: C의
for (int i ...)는 밖에서 못 쓰는데, 파이썬은 남아 있습니다(위 예시면 10). - 없는 것:
i++도 do-while도 없습니다.i += 1, 그리고while True:안의if 조건: break로 대신합니다.
C의 for는 숫자를 세는 반복이고, 파이썬의 for는 값을 꺼내는 반복입니다. 그래서 파이썬에서는
for i in range(len(lst)):보다for x in lst:가 기본이고, 순번까지 필요하면enumerate()를 붙입니다.
🧩 함수: *args는 튜플, **kwargs는 딕셔너리
4일차 끝에는 함수를 시작했습니다. 이날 붙잡은 질문은 하나였습니다. *args는 왜 튜플이고 **kwargs는 왜 딕셔너리인가. 둘 다 몇 개가 들어올지 모를 때 쓰는 문법입니다.
cafe('라떼', '톨')처럼 순서로 짝짓는 값이 위치 인자, cafe(size='톨', drink='라떼')처럼 이름표를 붙인 값이 키워드 인자입니다. 남는 위치 인자는 *args가, 남는 키워드 인자는 **kwargs가 받습니다.
*args: 남는 위치 인자를 튜플로
별 하나는 "남는 위치 인자 전부 이리 와"라는 뜻입니다. 받은 값은 튜플 하나로 묶이니 for로 꺼내면 몇 개가 오든 처리할 수 있습니다. 이름은 관례일 뿐이고 중요한 건 별(*)입니다.
def order(*args):
print(args)
order('라떼', '케이크') # ('라떼', '케이크')
order('물') # ('물',) 1개여도 튜플
order() # () 0개면 빈 튜플
def add_all(*nums):
total = 0
for n in nums:
total += n
return total
print(add_all(1, 2, 3)) # 6
**kwargs: 남는 이름=값을 딕셔너리로
별 두 개는 "남는 이름=값 전부 이리 와"입니다(keyword arguments). 딕셔너리니까 for 문에서 쓴 .items()로 꺼냅니다.
def option(**kwargs):
print(kwargs)
option(size='톨', shot=2)
# {'size': '톨', 'shot': 2}
option() # {} 0개면 빈 딕셔너리
def profile(**info):
for key, value in info.items():
print(f'{key}: {value}')
profile(name='홍길동', age=20)
# name: 홍길동
# age: 20
왜 하필 튜플이고, 왜 하필 딕셔너리인가
카페 주문으로 생각하니 바로 정리됐습니다. "아메리카노, 라떼, 케이크 주세요"는 이름표 없이 순서대로 나열한 주문이라 줄 세워 담는 튜플입니다. "사이즈는 톨, 샷은 두 번"은 항목마다 이름이 붙어 있으니 이름표째 담는 딕셔너리입니다.
*args가 튜플인 이유는 두 가지입니다.
- 위치 인자는 이름 없이 순서만 있습니다. 순서를 지켜 담는 자료형(리스트 아니면 튜플)이 필요합니다.
- 그중 튜플인 건 함수가 받은 입력을 받은 그대로 두는 게 안전해서입니다. 튜플은 바꿀 수 없으니 안에서 실수로 append하거나 값을 덮어쓸 일이 없습니다.
**kwargs 쪽은 모양이 답입니다. 키워드 인자는 이름=값 쌍이라 딕셔너리의 키: 값과 똑같습니다. 같은 이름을 두 번 넘길 수 없는 것(option(size=1, size=2)는 SyntaxError)도 키가 중복될 수 없는 딕셔너리와 같습니다.
딕셔너리는 바꿀 수 있는데 괜찮을까 싶었습니다. kwargs는 부를 때마다 새로 만들어져서 안에서 바꿔도 밖은 그대로입니다. 반대로 args.append(4)는 AttributeError라서 list(args)로 바꿔 씁니다.
대입문의 별은 또 다릅니다. first, *rest = [1, 2, 3, 4]에서 rest는 튜플이 아니라 리스트 [2, 3, 4]입니다.
같이 쓸 때의 순서
def cafe(menu, *args, **kwargs):
print(menu, args, kwargs)
cafe('주문', '라떼', '케이크', ice=True)
# 주문 ('라떼', '케이크') {'ice': True}
정의할 때는 일반 매개변수, *args, **kwargs 순서이고 **kwargs는 무조건 맨 끝입니다. 호출할 때는 위치 인자를 먼저, 키워드 인자를 뒤에 씁니다.
사실 이미 써 본 문법이었습니다. print()가 인자를 몇 개든 받는 것도 별 덕분이고, sep='-'는 키워드 인자입니다. C의 printf가 가변 인자(...)로 하던 일을 파이썬에서는 별이 합니다.
반대로 풀기: 호출할 때의 별
별은 호출할 때도 씁니다. 이때는 묶는 게 아니라 풉니다.
# 앞에서 만든 add_all, profile을 사용
nums = [1, 2, 3]
print(add_all(*nums)) # 6
print(*nums) # 1 2 3
info = {'name': '홍길동', 'age': 20}
profile(**info)
# name: 홍길동
# age: 20
add_all(*nums)는 add_all(1, 2, 3)과 같습니다. 정리하면 정의할 때 별은 묶기, 호출할 때 별은 풀기입니다.
return은 그 자리에서 함수를 끝냅니다
return은 값을 돌려주는 동시에 함수를 끝냅니다. 반복문 안에 두면 break와 반환을 한 번에 하는 셈입니다. 소수 판별 함수가 딱 이 모양입니다.
import math
def is_prime(n):
if n < 2:
return False
# √n까지만 나눠 본다
for i in range(2, math.isqrt(n) + 1):
if n % i == 0:
return False # 그 자리에서 끝
return True
print([n for n in range(30) if is_prime(n)])
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
약수는 √n을 기준으로 작은 쪽과 큰 쪽이 짝을 이루니 √n까지만 나눠 보면 됩니다. math.isqrt()는 정수 제곱근(내림)입니다.
이름 없는 함수 lambda
Day 3에서 딕셔너리를 정렬할 때 key=lambda x: x[1]을 썼습니다. lambda x: x[1]은 def f(x): return x[1]을 한 줄로 쓴 이름 없는 함수입니다. 한 번 쓰고 버릴 함수라 sorted()의 key 자리에 바로 넘기고, 기준이 둘이면 튜플을 돌려주게 합니다.
data = [('홍길동', 90), ('김철수', 95),
('이지수', 90)]
# 점수 내림차순, 동점은 이름순
print(sorted(data,
key=lambda x: (-x[1], x[0])))
# [('김철수', 95), ('이지수', 90),
# ('홍길동', 90)]
튜플은 첫 값부터 비교하고, 같을 때만 다음 값을 봅니다. 숫자에 마이너스를 붙이면 그 기준만 거꾸로 됩니다.
4일차를 한 줄로
if는 갈림길, while은 조건이 참인 동안, for는 하나씩 꺼내기입니다. isnumeric()은 input() 값을 int()로 바꾸기 전에 쓰는 검사이고, 음수와 소수는 False입니다. *args는 순서만 지키면 되고 바뀔 일이 없으니 튜플, **kwargs는 이름이 붙어 있으니 딕셔너리입니다.
문법보다 오래 남은 건 score1 or score2 실수입니다. 답이 맞게 나온 코드도 테스트 값 하나 바꾸면 틀릴 수 있다는 걸 이날 배웠습니다. isnumeric()으로 못 막는 음수·소수 입력은 Day 5의 예외 처리에서 다룹니다.