IT /파이썬 강의

파이썬 강의 6편: 함수

· 6분 읽기
#파이썬 #함수 #function #모듈화 #재사용

파이썬 강의 6편: 함수

반복문이랑 조건문으로 코드를 쭉 쓰다 보면 어느 순간 같은 코드를 복붙하고 있는 자신을 발견하게 됩니다. 저는 점수 계산 로직을 세 군데에 복붙해놓고 하나만 수정했다가 나머지 두 군데에서 버그가 나서, 그때 함수를 왜 쓰는지 체감했습니다.

함수가 뭔지

함수는 코드 덩어리에 이름을 붙여놓은 겁니다. 한 번 만들어두면 이름만 불러서 쓸 수 있습니다.

재사용 말고도 이유가 있는데, 코드를 읽을 때 calculate_average(scores) 같은 함수 호출 한 줄이 for문 5줄보다 의미 파악이 빠릅니다. 코드를 짧게 만드는 것보다 읽기 쉽게 만드는 쪽이 함수의 진짜 가치라고 느꼈습니다.

함수 쓰면 뭐가 좋은지

  1. 코드 재사용 — 같은 걸 여러 번 안 써도 됨
  2. 모듈화 — 큰 프로그램을 작은 단위로 쪼갤 수 있음
  3. 가독성 — 함수 이름만 봐도 뭐하는 코드인지 알 수 있음
  4. 유지보수 — 고칠 때 한 군데만 고치면 됨

기본 형태

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만 고치면 됩니다.

함수 만들 때 신경 쓸 것들

  1. 이름을 명확하게 — 함수 이름만 보고 뭘 하는지 알 수 있어야 합니다
  2. 한 가지 일만 — 하나의 함수가 너무 많은 걸 하면 나중에 고치기 어려워집니다
  3. 너무 길면 쪼개기 — 스크롤해야 할 정도면 함수를 나눌 타이밍입니다
  4. 설명 달아두기 — 복잡한 로직이면 docstring 쓰기

함수를 쓰기 시작하면 코드 보는 눈이 좀 달라집니다. 남이 쓴 코드를 볼 때도 일단 함수 이름부터 훑어보게 되고, 전체 구조가 먼저 눈에 들어옵니다. 지금까지 그냥 쭉 나열해서 썼던 코드를 함수로 한번 쪼개보면 그 차이를 바로 느낄 수 있습니다.