파이썬 강의 6편: 함수
파이썬 강의 6편: 함수
반복문이랑 조건문으로 코드를 쭉 쓰다 보면 어느 순간 같은 코드를 복붙하고 있는 자신을 발견하게 됩니다. 저는 점수 계산 로직을 세 군데에 복붙해놓고 하나만 수정했다가 나머지 두 군데에서 버그가 나서, 그때 함수를 왜 쓰는지 체감했습니다.
함수가 뭔지
함수는 코드 덩어리에 이름을 붙여놓은 겁니다. 한 번 만들어두면 이름만 불러서 쓸 수 있습니다.
재사용 말고도 이유가 있는데, 코드를 읽을 때 calculate_average(scores) 같은 함수 호출 한 줄이 for문 5줄보다 의미 파악이 빠릅니다. 코드를 짧게 만드는 것보다 읽기 쉽게 만드는 쪽이 함수의 진짜 가치라고 느꼈습니다.
함수 쓰면 뭐가 좋은지
- 코드 재사용 — 같은 걸 여러 번 안 써도 됨
- 모듈화 — 큰 프로그램을 작은 단위로 쪼갤 수 있음
- 가독성 — 함수 이름만 봐도 뭐하는 코드인지 알 수 있음
- 유지보수 — 고칠 때 한 군데만 고치면 됨
기본 형태
def 함수명(매개변수):
# 함수 본문
return 값
def 키워드로 시작하고, 콜론 찍고, 들여쓰기 한 블록이 함수 본문입니다.
제일 간단한 함수
def greet():
print("Hello, World!")
# 함수 호출
greet() # Hello, World!
greet()이라고 쓰면 함수 안의 코드가 실행됩니다. 괄호 안 쓰면 함수가 호출이 안 되는데, 처음에 괄호 빼먹고 왜 안 되지 하다가 깨달은 적이 있습니다.
매개변수 넣기
def greet(name):
print(f"안녕하세요, {name}님!")
greet("홍길동") # 안녕하세요, 홍길동님!
함수를 호출할 때 넣어주는 값을 인자(argument)라고 하고, 함수 정의에서 받는 변수를 매개변수(parameter)라고 합니다. 처음에 이 둘이 뭐가 다른지 좀 헷갈렸는데, 실질적으로는 거의 같은 의미로 쓰이니까 크게 신경 안 써도 됩니다.
값 돌려주기 (return)
def add(a, b):
return a + b
result = add(5, 3)
print(result) # 8
return을 쓰면 함수가 값을 돌려줍니다. print()랑 return이 처음에 헷갈릴 수 있는데, print()는 화면에 찍는 것이고 return은 함수 바깥으로 값을 넘기는 겁니다. for문 안에서 return 쓰면 어떻게 되는지 한참 실험했는데, return이 실행되면 함수가 바로 끝납니다. for문이 다 안 돌아도요.
매개변수 여러 개
def introduce(name, age, city):
print(f"이름: {name}, 나이: {age}, 거주지: {city}")
introduce("홍길동", 25, "서울")
기본값 (Default Arguments)
매개변수에 기본값을 지정할 수 있습니다. 호출할 때 안 넣으면 기본값이 쓰입니다.
def greet(name, greeting="안녕하세요"):
print(f"{greeting}, {name}님!")
greet("홍길동") # 안녕하세요, 홍길동님!
greet("홍길동", "반갑습니다") # 반갑습니다, 홍길동님!
키워드 인자
호출할 때 매개변수 이름을 직접 지정할 수 있습니다. 이러면 순서를 안 지켜도 됩니다.
def introduce(name, age, city):
print(f"이름: {name}, 나이: {age}, 거주지: {city}")
# 순서 상관없이 호출 가능
introduce(age=25, city="서울", name="홍길동")
매개변수가 많아지면 이렇게 이름을 지정하는 게 실수를 줄여줍니다. 순서 헷갈려서 값이 뒤바뀌는 실수가 은근히 잦거든요.
가변 인자 (*args)
인자 개수를 미리 정하지 않고 받을 수 있습니다.
def sum_all(*args):
total = 0
for num in args:
total += num
return total
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
*args는 튜플로 들어옵니다. 처음에 리스트인 줄 알았는데 type(args) 찍어보니까 tuple이었습니다.
키워드 가변 인자 (**kwargs)
키워드 인자를 딕셔너리로 받습니다.
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="홍길동", age=25, city="서울")
출력:
name: 홍길동
age: 25
city: 서울
*args랑 **kwargs를 같이 쓸 수도 있는데, 순서는 *args가 먼저 와야 합니다.
예제: 계산기 함수
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b != 0:
return a / b
else:
return "0으로 나눌 수 없습니다!"
# 사용
print(add(10, 5)) # 15
print(subtract(10, 5)) # 5
print(multiply(10, 5)) # 50
print(divide(10, 5)) # 2.0
divide에서 0으로 나누는 경우를 따로 처리한 부분이 포인트입니다. 이런 식으로 예외 상황을 함수 안에서 한 번만 처리해두면, 호출하는 쪽에서는 매번 체크 안 해도 됩니다.
예제: 최대값/최소값 찾기
def find_max(numbers):
max_num = numbers[0]
for num in numbers:
if num > max_num:
max_num = num
return max_num
def find_min(numbers):
min_num = numbers[0]
for num in numbers:
if num < min_num:
min_num = num
return min_num
numbers = [5, 2, 8, 1, 9]
print(f"최대값: {find_max(numbers)}") # 9
print(f"최소값: {find_min(numbers)}") # 1
물론 파이썬에는 max(), min() 내장 함수가 있어서 실무에서는 그걸 쓰면 됩니다. 이건 내부 동작을 이해하려고 만들어본 겁니다.
예제: 팩토리얼
def factorial(n):
if n <= 1:
return 1
result = 1
for i in range(2, n + 1):
result *= i
return result
print(factorial(5)) # 120
재귀 함수
함수가 자기 자신을 호출하는 방식입니다. 팩토리얼을 재귀로 쓰면 이렇게 됩니다:
def factorial_recursive(n):
if n <= 1:
return 1
return n * factorial_recursive(n - 1)
print(factorial_recursive(5)) # 120
재귀는 처음에 머리로 따라가기가 좀 힘듭니다. 종이에 factorial_recursive(5) -> 5 * factorial_recursive(4) -> 5 * 4 * factorial_recursive(3) 이런 식으로 직접 펼쳐보니까 그제야 흐름이 이해됐습니다. 종료 조건 (if n <= 1: return 1) 없으면 무한히 호출되니까 이것도 빼먹으면 안 됩니다.
람다 함수
한 줄짜리 간단한 함수를 만들 때 씁니다.
# 일반 함수
def add(a, b):
return a + b
# 람다 함수
add_lambda = lambda a, b: a + b
print(add(5, 3)) # 8
print(add_lambda(5, 3)) # 8
map()이랑 같이 쓸 때 편합니다:
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]
복잡한 로직이면 그냥 일반 함수 쓰는 게 낫습니다. 람다는 진짜 한 줄로 끝나는 것만 쓰는 게 좋습니다.
지역 변수 vs 전역 변수
지역 변수
함수 안에서 만든 변수는 함수 밖에서 못 씁니다.
def function():
local_var = 10
print(local_var)
function() # 10
# print(local_var) # 오류! 함수 밖에서 접근 불가
전역 변수
함수 밖에서 만든 변수는 함수 안에서 읽을 수는 있습니다.
global_var = 100
def function():
print(global_var) # 읽기는 가능
function() # 100
global 키워드
함수 안에서 전역 변수를 수정하려면 global을 써야 합니다.
count = 0
def increment():
global count
count += 1
increment()
print(count) # 1
근데 global을 많이 쓰면 코드가 꼬이기 쉽습니다. 가능하면 매개변수로 받고 return으로 돌려주는 방식이 안전합니다. 저도 초반에 전역 변수 남발했다가 어디서 값이 바뀌는지 추적이 안 돼서 고생한 적 있습니다.
docstring
함수에 설명을 달아둘 수 있습니다.
def add(a, b):
"""
두 숫자를 더하는 함수
매개변수:
a: 첫 번째 숫자
b: 두 번째 숫자
반환값:
두 숫자의 합
"""
return a + b
# 도움말 보기
help(add)
나중에 코드가 길어지면 함수마다 docstring 적어두는 게 진짜 도움이 됩니다. 몇 주 뒤에 다시 보면 내가 짠 코드도 기억 안 나니까요.
예제: 학생 성적 관리
def calculate_average(scores):
"""점수 리스트의 평균을 계산"""
if len(scores) == 0:
return 0
return sum(scores) / len(scores)
def get_grade(score):
"""점수에 따른 등급 반환"""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
# 사용
student_scores = [85, 90, 78, 92, 88]
average = calculate_average(student_scores)
grade = get_grade(average)
print(f"평균: {average:.2f}, 등급: {grade}")
이렇게 나눠놓으면 평균 계산 로직이 바뀌어도 calculate_average만 고치면 되고, 등급 기준이 바뀌어도 get_grade만 고치면 됩니다.
함수 만들 때 신경 쓸 것들
- 이름을 명확하게 — 함수 이름만 보고 뭘 하는지 알 수 있어야 합니다
- 한 가지 일만 — 하나의 함수가 너무 많은 걸 하면 나중에 고치기 어려워집니다
- 너무 길면 쪼개기 — 스크롤해야 할 정도면 함수를 나눌 타이밍입니다
- 설명 달아두기 — 복잡한 로직이면 docstring 쓰기
함수를 쓰기 시작하면 코드 보는 눈이 좀 달라집니다. 남이 쓴 코드를 볼 때도 일단 함수 이름부터 훑어보게 되고, 전체 구조가 먼저 눈에 들어옵니다. 지금까지 그냥 쭉 나열해서 썼던 코드를 함수로 한번 쪼개보면 그 차이를 바로 느낄 수 있습니다.