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_decorator는 say_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)
좋은 함수의 특징:
- 명확한 이름: 함수가 하는 일을 정확히 표현
- 독스트링: 사용법과 예제 포함
- 단일 책임: 한 가지 일만 수행
- 적절한 길이: 20-30줄 이내 (복잡하면 분리)
- 타입 힌트: 매개변수와 반환값 타입 명시 (선택)
타입 힌트 (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
정리
핵심 요약
- 함수 정의:
def 함수명(매개변수):형태로 정의,return으로 값 반환 - 매개변수 타입: 위치 인자, 키워드 인자, 기본값,
*args,**kwargs - 람다 함수:
lambda x: x ** 2형태의 익명 함수, 간단한 연산에 사용 - 클로저: 외부 함수의 변수를 기억하는 내부 함수,
nonlocal로 수정 - 데코레이터:
@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 함수는 프로그래밍의 핵심 빌딩 블록입니다. 함수를 잘 설계하면 코드 재사용성, 가독성, 유지보수성이 모두 향상됩니다.
학습 로드맵:
- 기본 함수: def, return, 매개변수 익히기
- 고급 기능: *args, **kwargs, 람다 연습
- 클로저: 상태를 유지하는 함수 만들기
- 데코레이터: 기존 함수에 기능 추가하기
- 실전 적용: 프로젝트에서 함수 분리 연습
함수 단위로 로직을 나누는 데 익숙해지셨다면, Python 클래스와 객체에서 데이터와 함수를 묶는 방법(객체지향)을 이어서 읽어 보세요. 관련 동작을 클래스로 묶으면 큰 프로그램도 설계도에 맞춰 확장하기 쉬워집니다.
관련 글
- Python 데코레이터 | @decorator 완벽 정리
- C++ std::function vs 함수 포인터 |
- C++ 기본 인자 |
- C++ 람다 캡처 에러 |
- C++ 함수 |