Python 함수 | 매개변수, 반환값, 람다, 데코레이터 완벽 정리

Python 함수 | 매개변수, 반환값, 람다, 데코레이터 완벽 정리

이 글의 핵심

Python 함수에 대한 실전 가이드입니다. 매개변수, 반환값, 람다, 데코레이터 완벽 정리 등을 예제와 함께 상세히 설명합니다.

들어가며

”함수는 코드의 기본 단위”

함수는 재사용 가능한 코드 블록입니다. Python에서 함수는 일급 객체(First-class Object)로, 변수에 할당하고 인자로 전달할 수 있습니다.


1. 함수 기본

함수란?

함수(Function)는 특정 작업을 수행하는 재사용 가능한 코드 블록입니다. 함수를 사용하면 코드 중복을 줄이고, 가독성을 높이며, 유지보수가 쉬워집니다.

함수의 장점:

  • 재사용성: 같은 코드를 여러 곳에서 호출 가능
  • 모듈화: 복잡한 프로그램을 작은 단위로 분리
  • 테스트 용이: 함수 단위로 테스트 가능
  • 가독성: 코드의 의도를 명확하게 표현

함수 정의와 호출

Python에서 함수는 def 키워드로 정의합니다. 재료(인자)를 넣으면 정해진 순서로 요리(본문)를 하고, 필요하면 결과를 return으로 돌려주는 레시피 카드처럼 쓰면 팀원이 읽기에도 좋습니다.

# 기본 함수 정의
def greet(name):
    """
    사용자에게 인사하는 함수
    
    Args:
        name (str): 인사할 사람의 이름
    
    Returns:
        str: 인사 메시지
    """
    return f"안녕하세요, {name}님!"

# 함수 호출
message = greet("철수")
print(message)  # 출력: 안녕하세요, 철수님!

# 직접 출력
print(greet("영희"))  # 출력: 안녕하세요, 영희님!

함수 구조 설명:

  • def: 함수 정의 키워드
  • greet: 함수 이름 (소문자와 언더스코어 사용 권장)
  • (name): 매개변수 (파라미터)
  • """…""": 독스트링 (함수 설명, 선택사항)
  • return: 반환값 (없으면 None 반환)

여러 값 반환하기

Python 함수는 튜플을 이용해 여러 값을 동시에 반환할 수 있습니다. 이는 다른 언어에서는 구조체나 객체를 반환해야 하는 상황을 간단하게 처리합니다.

# 여러 반환값 (튜플 언패킹)
def get_user_info():
    """사용자 정보를 반환합니다."""
    name = "철수"
    age = 25
    city = "서울"
    return name, age, city  # 튜플로 반환: ("철수", 25, "서울")

# 반환값을 각각의 변수에 할당
name, age, city = get_user_info()
print(f"이름: {name}, 나이: {age}, 도시: {city}")
# 출력: 이름: 철수, 나이: 25, 도시: 서울

# 튜플로 받기
user_info = get_user_info()
print(user_info)  # 출력: ('철수', 25, '서울')
print(user_info[0])  # 출력: 철수

# 일부만 받기 (나머지는 _로 무시)
name, _, city = get_user_info()
print(f"{name}님은 {city}에 삽니다.")

실전 활용 예제:

def calculate_statistics(numbers):
    """숫자 리스트의 통계를 계산합니다."""
    if not numbers:
        return 0, 0, 0, 0
    
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    maximum = max(numbers)
    minimum = min(numbers)
    
    return total, average, maximum, minimum

# 사용
scores = [85, 90, 78, 92, 88]
total, avg, max_score, min_score = calculate_statistics(scores)

print(f"총점: {total}")
print(f"평균: {avg:.2f}")
print(f"최고점: {max_score}")
print(f"최저점: {min_score}")

반환값이 없는 함수

return 문이 없거나 값 없이 return만 쓰면 None을 반환합니다.

# 반환값 없음 (None 반환)
def print_hello():
    """화면에 인사만 출력하고 값을 반환하지 않습니다."""
    print("Hello")
    # return 문 없음 → 암묵적으로 None 반환

result = print_hello()
# 출력: Hello

print(result)  # 출력: None
print(type(result))  # 출력: <class 'NoneType'>

# 조건부 반환
def check_positive(num):
    """양수인지 확인하고, 음수면 조기 반환합니다."""
    if num < 0:
        print("음수입니다.")
        return  # 값 없이 반환 → None
    
    print(f"{num}은 양수입니다.")
    return True

result1 = check_positive(10)  # 출력: 10은 양수입니다.
print(result1)  # 출력: True

result2 = check_positive(-5)  # 출력: 음수입니다.
print(result2)  # 출력: None

None 활용 패턴:

def find_user(user_id):
    """사용자를 찾아 반환합니다. 없으면 None."""
    users = {1: "철수", 2: "영희", 3: "민수"}
    return users.get(user_id)  # 없으면 None 반환

# None 체크
user = find_user(5)
if user is None:
    print("사용자를 찾을 수 없습니다.")
else:
    print(f"사용자: {user}")

2. 매개변수 (Parameters)

위치 인자 (Positional Arguments)

위치 인자는 함수 호출 시 순서대로 매개변수에 전달됩니다. 가장 기본적인 인자 전달 방식입니다.

def add(a, b):
    """두 숫자를 더합니다."""
    return a + b

# 위치 인자로 호출
result = add(3, 5)  # a=3, b=5
print(result)  # 출력: 8

# 순서가 중요!
def subtract(a, b):
    """a에서 b를 뺍니다."""
    return a - b

print(subtract(10, 3))  # 7 (10 - 3)
print(subtract(3, 10))  # -7 (3 - 10) ← 순서가 바뀌면 결과도 바뀜

실전 예제:

def calculate_bmi(weight, height):
    """
    BMI(체질량지수)를 계산합니다.
    
    Args:
        weight (float): 체중 (kg)
        height (float): 키 (m)
    
    Returns:
        float: BMI 값
    """
    bmi = weight / (height ** 2)
    return round(bmi, 2)

# 사용
my_bmi = calculate_bmi(70, 1.75)  # 70kg, 1.75m
print(f"BMI: {my_bmi}")  # 출력: BMI: 22.86

키워드 인자 (Keyword Arguments)

키워드 인자는 매개변수 이름을 명시하여 전달합니다. 순서에 상관없이 호출할 수 있어 가독성이 높습니다.

def introduce(name, age, city):
    """자기소개를 출력합니다."""
    print(f"{name}({age}세) - {city}")

# 위치 인자로 호출 (순서 중요)
introduce("철수", 25, "서울")
# 출력: 철수(25세) - 서울

# 키워드 인자로 호출 (순서 무관)
introduce(age=25, name="철수", city="서울")
# 출력: 철수(25세) - 서울

introduce(city="부산", age=30, name="영희")
# 출력: 영희(30세) - 부산

# 위치 + 키워드 혼합 (위치 인자가 먼저)
introduce("민수", age=28, city="대전")
# 출력: 민수(28세) - 대전

키워드 인자의 장점:

# ❌ 위치 인자만 사용 - 의미 불명확
send_email("[email protected]", "제목", "내용", True, False, "high")

# ✅ 키워드 인자 사용 - 의미 명확
send_email(
    to="[email protected]",
    subject="제목",
    body="내용",
    html=True,
    cc=False,
    priority="high"
)

기본값 (Default Arguments)

기본값을 설정하면 인자를 생략할 수 있습니다. 선택적 매개변수를 구현할 때 유용합니다.

def greet(name, greeting="안녕하세요"):
    """
    인사 메시지를 생성합니다.
    
    Args:
        name (str): 이름
        greeting (str): 인사말 (기본값: "안녕하세요")
    """
    return f"{greeting}, {name}님!"

# 기본값 사용
print(greet("철수"))  # 출력: 안녕하세요, 철수님!

# 기본값 오버라이드
print(greet("영희", "반갑습니다"))  # 출력: 반갑습니다, 영희님!
print(greet("민수", greeting="좋은 아침입니다"))  # 출력: 좋은 아침입니다, 민수님!

실전 예제 - API 요청 함수:

def fetch_data(url, timeout=30, retry=3, verify_ssl=True):
    """
    API에서 데이터를 가져옵니다.
    
    Args:
        url (str): API 엔드포인트 URL
        timeout (int): 타임아웃 시간 (초, 기본값: 30)
        retry (int): 재시도 횟수 (기본값: 3)
        verify_ssl (bool): SSL 인증서 검증 여부 (기본값: True)
    """
    print(f"URL: {url}")
    print(f"Timeout: {timeout}초")
    print(f"Retry: {retry}회")
    print(f"SSL 검증: {verify_ssl}")

# 기본값으로 호출
fetch_data("https://api.example.com/users")

# 일부 기본값 오버라이드
fetch_data("https://api.example.com/posts", timeout=60)

# 모든 인자 명시
fetch_data(
    url="https://api.example.com/data",
    timeout=10,
    retry=5,
    verify_ssl=False
)

기본값 사용 시 주의사항:

# ❌ 위험: 가변 객체를 기본값으로 사용
def add_item(item, items=[]):  # 리스트는 함수 정의 시 한 번만 생성됨
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] ← 이전 호출의 리스트가 유지됨!
print(add_item(3))  # [1, 2, 3]

# ✅ 올바른 방법: None을 기본값으로
def add_item_safe(item, items=None):
    if items is None:
        items = []  # 매 호출마다 새 리스트 생성
    items.append(item)
    return items

print(add_item_safe(1))  # [1]
print(add_item_safe(2))  # [2] ← 독립적인 리스트
print(add_item_safe(3))  # [3]

기본값 규칙:

  • 기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 합니다.
  • 가변 객체(리스트, 딕셔너리)를 기본값으로 사용하지 마세요.
  • 기본값은 함수 정의 시점에 한 번만 평가됩니다.
# ❌ 문법 오류
def wrong_order(a=10, b):  # SyntaxError: non-default argument follows default argument
    pass

# ✅ 올바른 순서
def correct_order(b, a=10):
    pass

가변 인자 (*args, **kwargs)

Python 함수는 가변 개수의 인자를 받을 수 있습니다. 이는 유연한 API 설계에 매우 유용합니다.

*args: 가변 위치 인자

*args임의 개수의 위치 인자를 튜플로 받습니다. 함수 내부에서는 일반 튜플처럼 사용합니다.

# *args 기본 사용
def sum_all(*numbers):
    """
    모든 숫자의 합을 계산합니다.
    
    Args:
        *numbers: 임의 개수의 숫자
    
    Returns:
        int/float: 합계
    """
    print(f"받은 인자: {numbers}")  # 튜플로 출력
    print(f"인자 개수: {len(numbers)}")
    return sum(numbers)

print(sum_all(1, 2, 3))  # 출력: 6
print(sum_all(1, 2, 3, 4, 5))  # 출력: 15
print(sum_all(10))  # 출력: 10
print(sum_all())  # 출력: 0 (빈 튜플)

실전 예제 - 로깅 함수:

def log(level, *messages):
    """
    여러 메시지를 로그로 출력합니다.
    
    Args:
        level (str): 로그 레벨 (INFO, WARNING, ERROR)
        *messages: 출력할 메시지들
    """
    timestamp = "2026-03-29 10:30:00"
    combined_message = " ".join(str(msg) for msg in messages)
    print(f"[{timestamp}] [{level}] {combined_message}")

# 사용
log("INFO", "서버 시작됨")
log("WARNING", "연결 지연:", 5, "초")
log("ERROR", "파일을 찾을 수 없음:", "config.json")

# 출력:
# [2026-03-29 10:30:00] [INFO] 서버 시작됨
# [2026-03-29 10:30:00] [WARNING] 연결 지연: 5 초
# [2026-03-29 10:30:00] [ERROR] 파일을 찾을 수 없음: config.json

**kwargs: 가변 키워드 인자

**kwargs임의 개수의 키워드 인자를 딕셔너리로 받습니다.

# **kwargs 기본 사용
def print_info(**kwargs):
    """
    모든 키워드 인자를 출력합니다.
    
    Args:
        **kwargs: 임의 개수의 키워드 인자
    """
    print(f"받은 인자: {kwargs}")  # 딕셔너리로 출력
    
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="철수", age=25, city="서울")
# 출력:
# 받은 인자: {'name': '철수', 'age': 25, 'city': '서울'}
# name: 철수
# age: 25
# city: 서울

print_info(product="노트북", price=1500000, brand="Apple")
# 출력:
# 받은 인자: {'product': '노트북', 'price': 1500000, 'brand': 'Apple'}
# product: 노트북
# price: 1500000
# brand: Apple

실전 예제 - 데이터베이스 쿼리 빌더:

def build_query(table, **conditions):
    """
    SQL WHERE 조건을 생성합니다.
    
    Args:
        table (str): 테이블 이름
        **conditions: 검색 조건 (컬럼=값)
    
    Returns:
        str: SQL 쿼리
    """
    query = f"SELECT * FROM {table}"
    
    if conditions:
        where_clauses = [f"{key}='{value}'" for key, value in conditions.items()]
        query += " WHERE " + " AND ".join(where_clauses)
    
    return query

# 사용
print(build_query("users"))
# SELECT * FROM users

print(build_query("users", name="철수"))
# SELECT * FROM users WHERE name='철수'

print(build_query("users", age=25, city="서울"))
# SELECT * FROM users WHERE age='25' AND city='서울'

print(build_query("products", category="전자제품", price=1000000, stock=10))
# SELECT * FROM products WHERE category='전자제품' AND price='1000000' AND stock='10'

혼합 사용: 위치 + *args + **kwargs

모든 인자 타입을 함께 사용할 수 있습니다. 순서가 중요합니다.

def complex_func(a, b, *args, **kwargs):
    """
    모든 타입의 인자를 받는 함수
    
    Args:
        a: 필수 위치 인자 1
        b: 필수 위치 인자 2
        *args: 추가 위치 인자들
        **kwargs: 키워드 인자들
    """
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

# 호출 예제
complex_func(1, 2, 3, 4, x=5, y=6)
# 출력:
# a=1, b=2
# args=(3, 4)
# kwargs={'x': 5, 'y': 6}

complex_func(10, 20)
# 출력:
# a=10, b=20
# args=()
# kwargs={}

complex_func(1, 2, 3, 4, 5, name="철수", age=25)
# 출력:
# a=1, b=2
# args=(3, 4, 5)
# kwargs={'name': '철수', 'age': 25}

매개변수 순서 규칙:

# ✅ 올바른 순서
def func(pos1, pos2, *args, default=10, **kwargs):
    pass

# 1. 일반 위치 인자
# 2. *args (가변 위치 인자)
# 3. 기본값이 있는 인자
# 4. **kwargs (가변 키워드 인자)

# ❌ 잘못된 순서
def wrong_func(**kwargs, *args):  # SyntaxError
    pass

실전 활용 - 래퍼 함수:

def retry_on_error(func, *args, max_retries=3, **kwargs):
    """
    함수 실행 실패 시 재시도하는 래퍼
    
    Args:
        func: 실행할 함수
        *args: func에 전달할 위치 인자
        max_retries: 최대 재시도 횟수
        **kwargs: func에 전달할 키워드 인자
    """
    for attempt in range(max_retries):
        try:
            result = func(*args, **kwargs)
            print(f"성공 (시도 {attempt + 1})")
            return result
        except Exception as e:
            print(f"실패 (시도 {attempt + 1}): {e}")
            if attempt == max_retries - 1:
                raise
    
# 사용 예제
def unstable_api_call(endpoint, timeout=10):
    """불안정한 API 호출 시뮬레이션"""
    import random
    if random.random() < 0.7:  # 70% 확률로 실패
        raise ConnectionError("네트워크 오류")
    return f"데이터: {endpoint}"

# retry_on_error로 감싸서 재시도
result = retry_on_error(
    unstable_api_call,
    "/users",
    timeout=30,
    max_retries=5
)

3. 람다 함수 (Lambda Functions)

람다 함수란?

람다 함수이름 없는 익명 함수로, 간단한 연산을 한 줄로 표현할 때 사용합니다. JavaScript의 화살표 함수(=>)와 유사합니다.

문법: lambda 매개변수: 표현식

일반 함수 vs 람다 함수

# 일반 함수 정의
def square(x):
    """숫자를 제곱합니다."""
    return x ** 2

# 람다 함수 (동일한 기능)
square_lambda = lambda x: x ** 2

# 사용
print(square(5))  # 출력: 25
print(square_lambda(5))  # 출력: 25

# 람다는 변수에 할당하지 않고 바로 사용 가능
print((lambda x: x ** 2)(5))  # 출력: 25

람다 함수의 특징:

  • 한 줄 표현식만 가능: 여러 줄 코드는 불가능
  • return 키워드 없음: 표현식의 결과가 자동으로 반환
  • 익명 함수: 이름이 없어 일회성 사용에 적합
  • 간결함: 간단한 연산을 짧게 표현

언제 람다를 사용할까?:

  • sorted(), map(), filter() 등의 key 함수로 사용
  • ✅ 간단한 콜백 함수
  • ❌ 복잡한 로직 (일반 함수 사용)
  • ❌ 재사용이 많은 함수 (일반 함수로 정의)

람다 활용 예제

sorted()와 함께 사용

sorted()key 매개변수로 정렬 기준을 지정할 수 있습니다. 람다 함수로 간단하게 표현합니다.

# 예제 1: 튜플 리스트 정렬
students = [("철수", 85), ("영희", 90), ("민수", 80)]

# 점수 기준 내림차순 정렬
sorted_students = sorted(students, key=lambda x: x[1], reverse=True)
print(sorted_students)
# 출력: [('영희', 90), ('철수', 85), ('민수', 80)]

# lambda x: x[1]의 의미:
# - x: 각 튜플 (예: ("철수", 85))
# - x[1]: 튜플의 두 번째 요소 (점수)
# - sorted는 이 값을 기준으로 정렬

# 예제 2: 딕셔너리 리스트 정렬
products = [
    {"name": "노트북", "price": 1500000},
    {"name": "마우스", "price": 30000},
    {"name": "키보드", "price": 80000}
]

# 가격 기준 오름차순 정렬
sorted_by_price = sorted(products, key=lambda x: x["price"])
print(sorted_by_price)
# 출력: [{'name': '마우스', 'price': 30000}, ...]

# 이름 길이 기준 정렬
sorted_by_name_length = sorted(products, key=lambda x: len(x["name"]))
print(sorted_by_name_length)

map()과 함께 사용

map()은 리스트의 각 요소에 함수를 적용합니다.

# 예제: 숫자 리스트를 제곱
numbers = [1, 2, 3, 4, 5]

# 람다로 제곱 연산
squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # 출력: [1, 4, 9, 16, 25]

# 여러 리스트 동시 처리
list1 = [1, 2, 3]
list2 = [10, 20, 30]
result = list(map(lambda x, y: x + y, list1, list2))
print(result)  # 출력: [11, 22, 33]

# 실전 예제: 문자열 리스트 처리
names = ["  철수  ", "영희", "  민수"]
cleaned = list(map(lambda x: x.strip().upper(), names))
print(cleaned)  # 출력: ['철수', '영희', '민수']

map() 설명: map(함수, 리스트)는 리스트의 각 요소에 함수를 적용하고, 결과를 반환합니다. list()로 감싸야 실제 리스트로 변환됩니다.

filter()와 함께 사용

filter()는 조건을 만족하는 요소만 걸러냅니다.

# 예제: 짝수만 필터링
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # 출력: [2, 4, 6, 8, 10]

# lambda x: x % 2 == 0의 의미:
# - x % 2 == 0: 짝수 판별 (True/False 반환)
# - filter는 True인 요소만 통과

# 실전 예제: 조건 필터링
products = [
    {"name": "노트북", "price": 1500000, "stock": 5},
    {"name": "마우스", "price": 30000, "stock": 0},
    {"name": "키보드", "price": 80000, "stock": 10}
]

# 재고가 있는 상품만 필터링
in_stock = list(filter(lambda x: x["stock"] > 0, products))
print(in_stock)
# 출력: [{'name': '노트북', ...}, {'name': '키보드', ...}]

# 가격이 50만원 이상인 상품
expensive = list(filter(lambda x: x["price"] >= 500000, products))
print(expensive)

filter() 설명: filter(조건함수, 리스트)는 조건함수가 True를 반환하는 요소만 남깁니다.

람다 vs 리스트 컴프리헨션

많은 경우 리스트 컴프리헨션이 람다보다 읽기 쉽습니다.

numbers = [1, 2, 3, 4, 5]

# map + lambda
squared_map = list(map(lambda x: x ** 2, numbers))

# 리스트 컴프리헨션 (더 읽기 쉬움)
squared_comp = [x ** 2 for x in numbers]

print(squared_map)   # [1, 4, 9, 16, 25]
print(squared_comp)  # [1, 4, 9, 16, 25]

# filter + lambda
even_filter = list(filter(lambda x: x % 2 == 0, numbers))

# 리스트 컴프리헨션 (더 읽기 쉬움)
even_comp = [x for x in numbers if x % 2 == 0]

print(even_filter)  # [2, 4]
print(even_comp)    # [2, 4]

선택 가이드:

  • 람다 사용: sorted(), max(), min()의 key 함수
  • 리스트 컴프리헨션 사용: map(), filter() 대체

4. 클로저 (Closure)

클로저란?

클로저(Closure)외부 함수의 변수를 기억하는 내부 함수입니다. JavaScript의 클로저, C++의 람다 캡처와 유사한 개념입니다.

클로저의 핵심:

  • 내부 함수가 외부 함수의 지역 변수를 기억합니다.
  • 외부 함수가 종료되어도 내부 함수는 그 변수에 접근 가능합니다.
  • 상태를 유지하는 함수를 만들 수 있습니다.

기본 클로저 예제

def make_multiplier(n):
    """
    n배를 계산하는 함수를 생성합니다.
    
    Args:
        n (int): 곱할 숫자
    
    Returns:
        function: x를 n배 하는 함수
    """
    def multiply(x):
        return x * n  # n을 기억! (클로저)
    return multiply

# 3배 함수 생성
times_3 = make_multiplier(3)
# 이 시점에서 make_multiplier는 종료되었지만,
# times_3은 여전히 n=3을 기억합니다.

# 5배 함수 생성
times_5 = make_multiplier(5)

# 사용
print(times_3(10))  # 출력: 30 (10 * 3)
print(times_3(7))   # 출력: 21 (7 * 3)
print(times_5(10))  # 출력: 50 (10 * 5)
print(times_5(4))   # 출력: 20 (4 * 5)

클로저 동작 원리:

def outer(x):
    """외부 함수"""
    print(f"외부 함수: x={x}")
    
    def inner(y):
        """내부 함수 (클로저)"""
        print(f"내부 함수: x={x}, y={y}")
        return x + y
    
    return inner

# outer 호출 → inner 함수 반환
func = outer(10)
# 출력: 외부 함수: x=10

# outer는 종료되었지만, func은 x=10을 기억
result = func(5)
# 출력: 내부 함수: x=10, y=5
# 반환: 15

nonlocal 키워드

nonlocal은 내부 함수에서 외부 함수의 변수를 수정할 때 사용합니다.

def make_counter():
    """카운터 함수를 생성합니다."""
    count = 0  # 외부 함수의 지역 변수
    
    def increment():
        nonlocal count  # count를 외부 함수의 변수로 지정
        count += 1
        return count
    
    return increment

# 독립적인 카운터 생성
counter1 = make_counter()
counter2 = make_counter()

# counter1 사용
print(counter1())  # 출력: 1
print(counter1())  # 출력: 2
print(counter1())  # 출력: 3

# counter2는 독립적 (별도의 count 변수)
print(counter2())  # 출력: 1
print(counter2())  # 출력: 2

# counter1은 여전히 자신의 상태 유지
print(counter1())  # 출력: 4

nonlocal 없이 시도하면?:

def make_counter_wrong():
    count = 0
    
    def increment():
        count += 1  # ❌ UnboundLocalError: local variable 'count' referenced before assignment
        return count
    
    return increment

# Python은 count += 1을 보고 count를 지역 변수로 간주하지만,
# 할당 전에 읽으려고 하므로 에러 발생

실전 클로저 예제

예제 1: 설정 값을 기억하는 함수 생성기

def create_logger(prefix):
    """
    특정 접두사를 가진 로거 함수를 생성합니다.
    
    Args:
        prefix (str): 로그 메시지 앞에 붙을 접두사
    
    Returns:
        function: 로그 출력 함수
    """
    def log(message):
        print(f"[{prefix}] {message}")
    
    return log

# 각 모듈별 로거 생성
db_logger = create_logger("DATABASE")
api_logger = create_logger("API")
auth_logger = create_logger("AUTH")

# 사용
db_logger("연결 성공")  # 출력: [DATABASE] 연결 성공
api_logger("요청 수신")  # 출력: [API] 요청 수신
auth_logger("로그인 성공")  # 출력: [AUTH] 로그인 성공

예제 2: 누적 계산기

def make_accumulator(initial=0):
    """
    값을 누적하는 함수를 생성합니다.
    
    Args:
        initial (int): 초기값
    
    Returns:
        function: 값을 누적하는 함수
    """
    total = initial
    
    def add(value):
        nonlocal total
        total += value
        return total
    
    return add

# 독립적인 누적기 생성
account1 = make_accumulator(1000)  # 초기 잔액 1000원
account2 = make_accumulator(5000)  # 초기 잔액 5000원

# account1 사용
print(account1(500))   # 출력: 1500 (1000 + 500)
print(account1(200))   # 출력: 1700 (1500 + 200)
print(account1(-300))  # 출력: 1400 (1700 - 300)

# account2는 독립적
print(account2(1000))  # 출력: 6000 (5000 + 1000)
print(account2(500))   # 출력: 6500 (6000 + 500)

예제 3: 함수 팩토리 (다양한 연산)

def make_operation(operation):
    """
    연산 타입에 따라 다른 함수를 반환합니다.
    
    Args:
        operation (str): 'add', 'multiply', 'power'
    
    Returns:
        function: 해당 연산을 수행하는 함수
    """
    if operation == "add":
        return lambda x, y: x + y
    elif operation == "multiply":
        return lambda x, y: x * y
    elif operation == "power":
        return lambda x, y: x ** y
    else:
        return lambda x, y: None

# 연산 함수 생성
add_func = make_operation("add")
multiply_func = make_operation("multiply")
power_func = make_operation("power")

# 사용
print(add_func(5, 3))       # 출력: 8
print(multiply_func(5, 3))  # 출력: 15
print(power_func(5, 3))     # 출력: 125 (5^3)

클로저 vs 클래스

클로저는 간단한 상태 유지에 적합하고, 복잡한 상태는 클래스가 더 적합합니다.

# 클로저 방식
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

# 클래스 방식 (더 명확)
class Counter:
    def __init__(self):
        self.count = 0
    
    def increment(self):
        self.count += 1
        return self.count

# 사용
closure_counter = make_counter()
class_counter = Counter()

print(closure_counter())  # 1
print(class_counter.increment())  # 1

선택 가이드:

  • 클로저: 간단한 상태 유지, 함수형 프로그래밍 스타일
  • 클래스: 여러 메서드와 속성, 복잡한 상태 관리

5. 데코레이터 기본 (Decorators)

데코레이터란?

데코레이터(Decorator)함수를 수정하지 않고 기능을 추가하는 패턴입니다. 함수를 인자로 받아 새로운 함수를 반환합니다.

데코레이터의 용도:

  • 로깅 (함수 호출 기록)
  • 실행 시간 측정
  • 권한 검사
  • 캐싱 (결과 저장)
  • 입력 검증

데코레이터 없이 구현:

def say_hello():
    print("Hello!")

def add_logging(func):
    """함수 실행 전후에 로그를 추가합니다."""
    def wrapper():
        print("함수 실행 전")
        func()
        print("함수 실행 후")
    return wrapper

# 수동으로 감싸기
say_hello = add_logging(say_hello)
say_hello()
# 출력:
# 함수 실행 전
# Hello!
# 함수 실행 후

데코레이터 문법으로 간단하게:

def my_decorator(func):
    """함수를 꾸며주는 데코레이터"""
    def wrapper():
        print("함수 실행 전")
        func()
        print("함수 실행 후")
    return wrapper

# @ 문법으로 데코레이터 적용
@my_decorator
def say_hello():
    print("Hello!")

# 호출
say_hello()
# 출력:
# 함수 실행 전
# Hello!
# 함수 실행 후

@my_decoratorsay_hello = my_decorator(say_hello)와 동일합니다. 더 읽기 쉽고 간결합니다.

인자가 있는 함수에 데코레이터 적용

*args, **kwargs를 사용하면 모든 인자를 받을 수 있습니다.

def my_decorator(func):
    """모든 함수에 적용 가능한 범용 데코레이터"""
    def wrapper(*args, **kwargs):
        print(f"함수 호출: {func.__name__}")
        print(f"인자: args={args}, kwargs={kwargs}")
        
        result = func(*args, **kwargs)
        
        print(f"결과: {result}")
        return result
    
    return wrapper

@my_decorator
def add(a, b):
    """두 숫자를 더합니다."""
    return a + b

@my_decorator
def greet(name, greeting="안녕하세요"):
    """인사 메시지를 생성합니다."""
    return f"{greeting}, {name}님!"

# 사용
result1 = add(3, 5)
# 출력:
# 함수 호출: add
# 인자: args=(3, 5), kwargs={}
# 결과: 8

result2 = greet("철수", greeting="반갑습니다")
# 출력:
# 함수 호출: greet
# 인자: args=('철수',), kwargs={'greeting': '반갑습니다'}
# 결과: 반갑습니다, 철수님!

실전 데코레이터 예제

예제 1: 실행 시간 측정

import time

def measure_time(func):
    """함수 실행 시간을 측정합니다."""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        
        result = func(*args, **kwargs)
        
        end_time = time.time()
        elapsed = end_time - start_time
        
        print(f"{func.__name__} 실행 시간: {elapsed:.4f}초")
        return result
    
    return wrapper

@measure_time
def slow_function():
    """시간이 오래 걸리는 함수 시뮬레이션"""
    time.sleep(2)
    return "완료"

result = slow_function()
# 출력: slow_function 실행 시간: 2.0023초

예제 2: 권한 검사

def require_auth(func):
    """로그인 여부를 확인하는 데코레이터"""
    def wrapper(user, *args, **kwargs):
        if not user.get("is_logged_in"):
            print("오류: 로그인이 필요합니다.")
            return None
        
        return func(user, *args, **kwargs)
    
    return wrapper

@require_auth
def view_profile(user):
    """사용자 프로필을 조회합니다."""
    return f"{user['name']}의 프로필"

@require_auth
def delete_account(user):
    """계정을 삭제합니다."""
    return f"{user['name']}의 계정이 삭제되었습니다."

# 테스트
logged_in_user = {"name": "철수", "is_logged_in": True}
guest_user = {"name": "손님", "is_logged_in": False}

print(view_profile(logged_in_user))  # 출력: 철수의 프로필
print(view_profile(guest_user))      # 출력: 오류: 로그인이 필요합니다.

예제 3: 결과 캐싱 (메모이제이션)

def cache(func):
    """함수 결과를 캐싱하여 중복 계산을 방지합니다."""
    cached_results = {}
    
    def wrapper(*args):
        if args in cached_results:
            print(f"캐시에서 반환: {args}")
            return cached_results[args]
        
        print(f"계산 중: {args}")
        result = func(*args)
        cached_results[args] = result
        return result
    
    return wrapper

@cache
def fibonacci(n):
    """피보나치 수를 계산합니다 (재귀)."""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 사용
print(fibonacci(5))
# 출력:
# 계산 중: (5,)
# 계산 중: (4,)
# 계산 중: (3,)
# 계산 중: (2,)
# 계산 중: (1,)
# 계산 중: (0,)
# 캐시에서 반환: (1,)
# 캐시에서 반환: (2,)
# 캐시에서 반환: (3,)
# 5

print(fibonacci(5))  # 두 번째 호출
# 출력: 캐시에서 반환: (5,)
# 5

Python 내장 캐싱: functools.lru_cache 사용 권장

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 자동으로 캐싱됨
print(fibonacci(100))  # 매우 빠름

예제 4: 재시도 로직

def retry(max_attempts=3):
    """실패 시 재시도하는 데코레이터"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"시도 {attempt + 1} 실패: {e}")
                    if attempt == max_attempts - 1:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=3)
def unstable_api_call():
    """불안정한 API 호출 시뮬레이션"""
    import random
    if random.random() < 0.7:
        raise ConnectionError("네트워크 오류")
    return "성공"

# 사용 (최대 3번 재시도)
result = unstable_api_call()

고급 주제

재귀 함수 (Recursive Functions)

재귀 함수는 자기 자신을 호출하는 함수입니다.

def factorial(n):
    """
    팩토리얼을 재귀로 계산합니다.
    n! = n × (n-1) × (n-2) × ... × 1
    """
    # 기저 조건 (재귀 종료)
    if n <= 1:
        return 1
    
    # 재귀 호출
    return n * factorial(n - 1)

print(factorial(5))  # 120 (5 × 4 × 3 × 2 × 1)

# 실행 과정:
# factorial(5) = 5 × factorial(4)
# factorial(4) = 4 × factorial(3)
# factorial(3) = 3 × factorial(2)
# factorial(2) = 2 × factorial(1)
# factorial(1) = 1
# 결과: 5 × 4 × 3 × 2 × 1 = 120

재귀 vs 반복:

# 재귀 방식
def sum_recursive(n):
    if n <= 0:
        return 0
    return n + sum_recursive(n - 1)

# 반복 방식 (더 효율적)
def sum_iterative(n):
    total = 0
    for i in range(1, n + 1):
        total += i
    return total

print(sum_recursive(100))  # 5050
print(sum_iterative(100))  # 5050

일급 객체로서의 함수

Python에서 함수는 일급 객체(First-class Object)입니다. 변수에 할당하고, 인자로 전달하고, 반환할 수 있습니다.

# 함수를 변수에 할당
def greet(name):
    return f"안녕하세요, {name}님!"

say_hello = greet  # 함수를 변수에 할당
print(say_hello("철수"))  # 출력: 안녕하세요, 철수님!

# 함수를 인자로 전달
def apply_operation(func, value):
    """함수를 인자로 받아 실행합니다."""
    return func(value)

def square(x):
    return x ** 2

def cube(x):
    return x ** 3

print(apply_operation(square, 5))  # 25
print(apply_operation(cube, 5))    # 125

# 함수를 리스트에 저장
operations = [square, cube, lambda x: x * 2]

for op in operations:
    print(op(3))
# 출력: 9, 27, 6

고차 함수 (Higher-order Functions)

고차 함수는 함수를 인자로 받거나 함수를 반환하는 함수입니다.

def create_multiplier(factor):
    """특정 배수를 계산하는 함수를 반환합니다."""
    return lambda x: x * factor

def create_adder(n):
    """특정 값을 더하는 함수를 반환합니다."""
    return lambda x: x + n

# 함수 생성
double = create_multiplier(2)
triple = create_multiplier(3)
add_10 = create_adder(10)

# 사용
print(double(5))   # 10
print(triple(5))   # 15
print(add_10(5))   # 15

# 함수 조합
def compose(f, g):
    """두 함수를 조합합니다: f(g(x))"""
    return lambda x: f(g(x))

# double(add_10(x)) 함수 생성
double_then_add = compose(double, add_10)
print(double_then_add(5))  # 30 (double(add_10(5)) = double(15) = 30)

함수를 설계할 때 자주 쓰는 패턴

함수는 같은 재료(인자)를 넣으면 같은 규칙으로 결과를 내는 레시피와 비슷합니다. 이름·독스트링·한 가지 일에 집중하는 습관을 두면, 나중에 테스트와 수정이 훨씬 수월합니다.

# ✅ 좋은 함수 - 명확한 이름, 독스트링, 단일 책임
def calculate_total_price(items, tax_rate=0.1, discount=0):
    """
    총 가격을 계산합니다 (세금 포함, 할인 적용).
    
    Args:
        items (list): 상품 리스트, 각 항목은 {'name': str, 'price': float}
        tax_rate (float): 세율 (기본값: 0.1 = 10%)
        discount (float): 할인율 (기본값: 0 = 할인 없음)
    
    Returns:
        float: 최종 가격 (세금 포함, 할인 적용)
    
    Examples:
        >>> items = [{'name': '노트북', 'price': 1000000}]
        >>> calculate_total_price(items, tax_rate=0.1)
        1100000.0
    """
    if not items:
        return 0.0
    
    # 소계 계산
    subtotal = sum(item['price'] for item in items)
    
    # 할인 적용
    discounted = subtotal * (1 - discount)
    
    # 세금 적용
    total = discounted * (1 + tax_rate)
    
    return round(total, 2)

# ❌ 나쁜 함수 - 이름 불명확, 문서화 없음, 의미 불명
def calc(x, y=0.1):
    return sum(i['price'] for i in x) * (1 + y)

좋은 함수의 특징:

  1. 명확한 이름: 함수가 하는 일을 정확히 표현
  2. 독스트링: 사용법과 예제 포함
  3. 단일 책임: 한 가지 일만 수행
  4. 적절한 길이: 20-30줄 이내 (복잡하면 분리)
  5. 타입 힌트: 매개변수와 반환값 타입 명시 (선택)

타입 힌트 (Type Hints)

Python 3.5+에서는 타입 힌트로 코드 가독성을 높일 수 있습니다.

from typing import List, Dict, Optional

def calculate_average(numbers: List[float]) -> float:
    """
    숫자 리스트의 평균을 계산합니다.
    
    Args:
        numbers: 숫자 리스트
    
    Returns:
        평균값
    """
    if not numbers:
        return 0.0
    return sum(numbers) / len(numbers)

def find_user(user_id: int) -> Optional[Dict[str, any]]:
    """
    사용자를 찾습니다.
    
    Args:
        user_id: 사용자 ID
    
    Returns:
        사용자 정보 딕셔너리 또는 None
    """
    users = {1: {"name": "철수", "age": 25}}
    return users.get(user_id)

# 타입 힌트는 실행에 영향을 주지 않지만,
# IDE의 자동완성과 타입 체커(mypy)가 활용합니다.

함수 설계 원칙

1. 단일 책임 원칙

# ❌ 나쁜 예: 여러 책임
def process_user_data(user):
    # 검증
    if not user.get("email"):
        raise ValueError("이메일 필요")
    
    # 데이터베이스 저장
    db.save(user)
    
    # 이메일 발송
    send_email(user["email"], "가입 완료")
    
    # 로그 기록
    log(f"사용자 등록: {user['name']}")

# ✅ 좋은 예: 책임 분리
def validate_user(user):
    """사용자 데이터 검증"""
    if not user.get("email"):
        raise ValueError("이메일 필요")

def save_user(user):
    """데이터베이스에 저장"""
    db.save(user)

def send_welcome_email(user):
    """환영 이메일 발송"""
    send_email(user["email"], "가입 완료")

def log_user_registration(user):
    """등록 로그 기록"""
    log(f"사용자 등록: {user['name']}")

# 각 함수는 하나의 책임만 가짐
def register_user(user):
    """사용자 등록 전체 프로세스"""
    validate_user(user)
    save_user(user)
    send_welcome_email(user)
    log_user_registration(user)

2. 순수 함수 (Pure Functions)

순수 함수는 같은 입력에 항상 같은 출력을 반환하고, 부작용이 없습니다.

# ✅ 순수 함수
def add(a, b):
    return a + b

# 항상 같은 결과
print(add(3, 5))  # 8
print(add(3, 5))  # 8

# ❌ 순수하지 않은 함수 (외부 상태 의존)
counter = 0

def increment():
    global counter
    counter += 1
    return counter

# 호출할 때마다 다른 결과
print(increment())  # 1
print(increment())  # 2

정리

핵심 요약

  1. 함수 정의: def 함수명(매개변수): 형태로 정의, return으로 값 반환
  2. 매개변수 타입: 위치 인자, 키워드 인자, 기본값, *args, **kwargs
  3. 람다 함수: lambda x: x ** 2 형태의 익명 함수, 간단한 연산에 사용
  4. 클로저: 외부 함수의 변수를 기억하는 내부 함수, nonlocal로 수정
  5. 데코레이터: @decorator 문법으로 함수에 기능 추가, 로깅/캐싱/권한 검사 등

함수 설계 체크리스트

함수 작성 전:

  • 함수 이름이 하는 일을 명확히 표현하는가?
  • 하나의 책임만 가지는가?
  • 매개변수가 5개 이하인가? (많으면 딕셔너리나 클래스 고려)

함수 작성 중:

  • 독스트링을 작성했는가?
  • 기본값이 있는 매개변수가 뒤에 있는가?
  • 가변 객체를 기본값으로 사용하지 않았는가?
  • 에러 처리가 적절한가?

함수 작성 후:

  • 함수 이름만 보고 기능을 유추할 수 있는가?
  • 테스트 케이스를 작성했는가?
  • 재사용 가능한가?

다음 단계

함수를 마스터했다면, 객체지향 프로그래밍을 배울 차례입니다.

  • Python 클래스와 객체 | 객체지향 프로그래밍 입문
  • Python 모듈과 패키지 | import, init.py, 패키지 구조
  • Python 데코레이터 심화 | 매개변수, functools.wraps, 클래스 데코레이터

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Python 기본 문법 | 변수, 연산자, 조건문, 반복문
  • Python 자료형 | 리스트, 딕셔너리, 튜플, 세트
  • Python 클래스와 객체 | 객체지향 프로그래밍
  • Python 예외 처리 | try-except, 사용자 정의 예외

이 글에서 다루는 키워드 (관련 검색어)

Python 함수, def 키워드, 매개변수, return, 람다 함수, lambda, 클로저, closure, 데코레이터, decorator, *args, **kwargs, 가변 인자, 함수형 프로그래밍, 고차 함수, 재귀 함수, 타입 힌트 등으로 검색하시면 이 글이 도움이 됩니다.


실전 연습 문제

문제 1: 온도 변환 함수

섭씨를 화씨로, 화씨를 섭씨로 변환하는 함수를 작성하세요.

def celsius_to_fahrenheit(celsius):
    """섭씨를 화씨로 변환합니다."""
    # 공식: F = C × 9/5 + 32
    pass

def fahrenheit_to_celsius(fahrenheit):
    """화씨를 섭씨로 변환합니다."""
    # 공식: C = (F - 32) × 5/9
    pass

# 테스트
print(celsius_to_fahrenheit(0))    # 32.0
print(celsius_to_fahrenheit(100))  # 212.0
print(fahrenheit_to_celsius(32))   # 0.0

문제 2: 리스트 필터링 함수

조건을 만족하는 요소만 반환하는 범용 필터 함수를 작성하세요.

def filter_by_condition(items, condition):
    """
    조건을 만족하는 항목만 반환합니다.
    
    Args:
        items: 항목 리스트
        condition: 조건 함수 (True/False 반환)
    """
    pass

# 테스트
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = filter_by_condition(numbers, lambda x: x % 2 == 0)
print(evens)  # [2, 4, 6, 8, 10]

문제 3: 데코레이터 만들기

함수 실행 횟수를 세는 데코레이터를 작성하세요.

def count_calls(func):
    """함수 호출 횟수를 세는 데코레이터"""
    # 여기에 코드 작성
    pass

@count_calls
def say_hello():
    print("Hello!")

# 테스트
say_hello()  # 호출 1회: Hello!
say_hello()  # 호출 2회: Hello!
say_hello()  # 호출 3회: Hello!

정답은 다음 글에서 확인하세요!


마치며

Python 함수는 프로그래밍의 핵심 빌딩 블록입니다. 함수를 잘 설계하면 코드 재사용성, 가독성, 유지보수성이 모두 향상됩니다.

학습 로드맵:

  1. 기본 함수: def, return, 매개변수 익히기
  2. 고급 기능: *args, **kwargs, 람다 연습
  3. 클로저: 상태를 유지하는 함수 만들기
  4. 데코레이터: 기존 함수에 기능 추가하기
  5. 실전 적용: 프로젝트에서 함수 분리 연습

함수 단위로 로직을 나누는 데 익숙해지셨다면, Python 클래스와 객체에서 데이터와 함수를 묶는 방법(객체지향)을 이어서 읽어 보세요. 관련 동작을 클래스로 묶으면 큰 프로그램도 설계도에 맞춰 확장하기 쉬워집니다.


관련 글

  • Python 데코레이터 | @decorator 완벽 정리
  • C++ std::function vs 함수 포인터 |
  • C++ 기본 인자 |
  • C++ 람다 캡처 에러 |
  • C++ 함수 |