본문으로 건너뛰기
Advertisement

5.1 함수 정의와 반환 — def, return, docstring

함수(Function)는 재사용 가능한 코드 블록으로, 복잡한 프로그램을 작고 명확한 단위로 나눌 수 있게 해줍니다. Python에서 함수는 일급 객체(first-class object)이며, 변수에 할당하거나 인자로 전달할 수 있습니다.


def 키워드와 기본 문법

# 기본 함수 정의
def greet(name: str) -> str:
"""사용자에게 인사 메시지를 반환합니다."""
return f"안녕하세요, {name}님!"


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

# 매개변수 없는 함수
def get_separator() -> str:
return "-" * 40

print(get_separator()) # ----------------------------------------

# 함수 객체 자체 (호출하지 않음)
print(greet) # <function greet at 0x...>
print(type(greet)) # <class 'function'>

return 문: 다양한 반환 방식

# 단일 값 반환
def square(n: int | float) -> float:
return n ** 2

print(square(5)) # 25
print(square(2.5)) # 6.25


# 다중 값 반환 (실제로는 튜플)
def min_max(numbers: list[int | float]) -> tuple[float, float]:
return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(f"최솟값: {lo}, 최댓값: {hi}") # 최솟값: 1, 최댓값: 9

# 반환값을 튜플로 받을 수도 있음
result = min_max([10, 20, 30])
print(result) # (10, 30)
print(type(result)) # <class 'tuple'>


# 암시적 None 반환 — return이 없거나 return만 있는 경우
def print_hello():
print("Hello!")
# 암시적으로 return None

def early_exit(value: int):
if value < 0:
return # None 반환 후 함수 종료
print(f"양수: {value}")

result1 = print_hello()
result2 = early_exit(-1)
print(result1, result2) # None None


# 조기 반환 (early return) 패턴
def safe_divide(a: float, b: float) -> float | None:
if b == 0:
return None # 0 나눔 방지
return a / b

print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # None

docstring 작성법

docstring은 함수, 클래스, 모듈의 첫 번째 문장에 작성하는 문서화 문자열입니다. help() 함수와 IDE의 자동완성에 활용됩니다.

# 단일 행 docstring — 간단한 함수에 적합
def add(a: int, b: int) -> int:
"""두 정수를 더한 값을 반환합니다."""
return a + b


# 다중 행 docstring — 복잡한 함수에 적합
def process_data(data: list[dict], threshold: float = 0.5) -> list[dict]:
"""
데이터 목록을 처리하여 임계값 이상의 항목만 반환합니다.

Parameters
----------
data : list[dict]
처리할 데이터 목록. 각 항목은 'value' 키를 포함해야 합니다.
threshold : float, optional
필터링 임계값 (기본값: 0.5)

Returns
-------
list[dict]
threshold 이상의 value를 가진 항목만 포함한 새 리스트

Raises
------
ValueError
data가 비어 있을 때

Examples
--------
>>> process_data([{"value": 0.3}, {"value": 0.7}], threshold=0.5)
[{'value': 0.7}]
"""
if not data:
raise ValueError("데이터가 비어 있습니다")
return [item for item in data if item.get("value", 0) >= threshold]


# docstring 접근
print(process_data.__doc__)
help(add)

Google 스타일 docstring

def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""BMI(체질량지수)를 계산합니다.

Args:
weight_kg: 체중 (킬로그램)
height_m: 키 (미터)

Returns:
BMI 값 (체중 / 키²)

Raises:
ValueError: 키나 체중이 0 이하인 경우

Example:
>>> calculate_bmi(70, 1.75)
22.86
"""
if weight_kg <= 0 or height_m <= 0:
raise ValueError("체중과 키는 양수여야 합니다")
return round(weight_kg / (height_m ** 2), 2)


print(calculate_bmi(70, 1.75)) # 22.86

타입 힌트와 함수 시그니처

from typing import Optional, Union


# 기본 타입 힌트
def greet_user(name: str, age: int) -> str:
return f"{name}님 ({age}세)"


# Optional: None이 될 수 있는 경우
def find_user(user_id: int) -> Optional[dict]:
db = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return db.get(user_id)


# Union: 여러 타입 허용 (Python 3.10+ 에서는 X | Y 문법)
def stringify(value: Union[int, float, str]) -> str:
return str(value)

# Python 3.10+ 문법
def stringify_new(value: int | float | str) -> str:
return str(value)


# 컬렉션 타입 힌트
from typing import Any

def process_items(
items: list[str],
mapping: dict[str, Any],
flags: tuple[bool, ...]
) -> list[str]:
return [item for item in items if item in mapping]


# Callable 타입 힌트
from typing import Callable

def apply_twice(func: Callable[[int], int], value: int) -> int:
"""함수를 두 번 적용"""
return func(func(value))

print(apply_twice(lambda x: x * 2, 3)) # 12


# 복잡한 반환 타입
from typing import TypeAlias

Matrix: TypeAlias = list[list[float]]

def identity_matrix(n: int) -> Matrix:
"""n×n 단위 행렬 생성"""
return [[1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]

for row in identity_matrix(3):
print(row)
# [1.0, 0.0, 0.0]
# [0.0, 1.0, 0.0]
# [0.0, 0.0, 1.0]

중첩 함수

# 내부 함수 정의 — 외부 함수 안에서만 유효
def make_greeting(language: str):
"""언어별 인사 함수를 생성"""

def korean_greeting(name: str) -> str:
return f"안녕하세요, {name}님!"

def english_greeting(name: str) -> str:
return f"Hello, {name}!"

def japanese_greeting(name: str) -> str:
return f"こんにちは、{name}さん!"

greetings = {
"ko": korean_greeting,
"en": english_greeting,
"ja": japanese_greeting,
}
return greetings.get(language, english_greeting)


greet_ko = make_greeting("ko")
greet_en = make_greeting("en")

print(greet_ko("김철수")) # 안녕하세요, 김철수님!
print(greet_en("Alice")) # Hello, Alice!


# 내부 함수로 복잡성 분리
def validate_and_process(data: list[int]) -> dict:
"""데이터 유효성 검사 후 처리"""

def validate(items: list[int]) -> list[str]:
errors = []
if not items:
errors.append("리스트가 비어 있음")
if any(x < 0 for x in items):
errors.append("음수 값 포함")
if len(items) > 1000:
errors.append("최대 1000개 초과")
return errors

def compute_stats(items: list[int]) -> dict:
return {
"count": len(items),
"sum": sum(items),
"mean": sum(items) / len(items),
"min": min(items),
"max": max(items),
}

errors = validate(data)
if errors:
return {"success": False, "errors": errors}

return {"success": True, "stats": compute_stats(data)}


result = validate_and_process([3, 7, 2, 9, 1])
print(result)

실전 예제: 좋은 함수 설계

# 단일 책임 원칙 (Single Responsibility Principle)

# 나쁜 예: 너무 많은 일을 하는 함수
def process_user_data_bad(user_data: dict) -> None:
# 유효성 검사
if not user_data.get("name"):
raise ValueError("이름 없음")
if not user_data.get("email"):
raise ValueError("이메일 없음")
if "@" not in user_data.get("email", ""):
raise ValueError("이메일 형식 오류")
# DB 저장 (시뮬레이션)
print(f"DB 저장: {user_data}")
# 이메일 발송 (시뮬레이션)
print(f"이메일 발송: {user_data['email']}")
# 로깅
print(f"로그: 사용자 {user_data['name']} 등록")


# 좋은 예: 각 함수가 하나의 책임
def validate_user(user_data: dict) -> list[str]:
"""사용자 데이터 유효성 검사. 오류 목록 반환."""
errors = []
if not user_data.get("name"):
errors.append("이름이 필요합니다")
if not user_data.get("email"):
errors.append("이메일이 필요합니다")
elif "@" not in user_data["email"]:
errors.append("올바른 이메일 형식이 아닙니다")
return errors


def save_user(user_data: dict) -> int:
"""사용자를 DB에 저장. 생성된 ID 반환."""
print(f"DB 저장: {user_data['name']}")
return 42 # 시뮬레이션: 새 사용자 ID


def send_welcome_email(email: str, name: str) -> bool:
"""환영 이메일 발송. 성공 여부 반환."""
print(f"환영 이메일 발송: {name} <{email}>")
return True


def register_user(user_data: dict) -> dict:
"""사용자 등록 전체 흐름 조율."""
errors = validate_user(user_data)
if errors:
return {"success": False, "errors": errors}

user_id = save_user(user_data)
send_welcome_email(user_data["email"], user_data["name"])

return {"success": True, "user_id": user_id}


result = register_user({"name": "Alice", "email": "alice@example.com"})
print(result)

고수 팁

1. 함수 어노테이션 동적 접근

def example(x: int, y: float = 1.0) -> str:
"""예시 함수"""
return str(x + y)

# __annotations__ 접근
print(example.__annotations__)
# {'x': <class 'int'>, 'y': <class 'float'>, 'return': <class 'str'>}

# __defaults__: 기본값 튜플
print(example.__defaults__) # (1.0,)

# __name__, __qualname__
print(example.__name__) # example
print(example.__qualname__) # example

2. 함수를 반환하는 함수 맛보기 (팩토리)

def multiplier(factor: int):
"""factor를 곱하는 함수를 반환"""
def multiply(x: int) -> int:
return x * factor
return multiply

double = multiplier(2)
triple = multiplier(3)

print(double(5)) # 10
print(triple(5)) # 15
print([double(i) for i in range(1, 6)]) # [2, 4, 6, 8, 10]

3. 순수 함수(Pure Function) 지향

# 순수 함수: 동일 입력 → 항상 동일 출력, 부수 효과 없음
# 테스트하기 쉽고, 버그가 적음

# 비순수: 전역 상태 의존
count = 0
def increment_bad():
global count
count += 1
return count

# 순수: 입력만으로 출력 결정
def increment(current: int) -> int:
return current + 1

# 날짜 의존성 (비순수 → 순수로 개선)
from datetime import date

def get_age_bad(birth_year: int) -> int:
return date.today().year - birth_year # 실행 시점에 따라 결과 달라짐

def get_age(birth_year: int, current_year: int) -> int:
return current_year - birth_year # 테스트하기 쉬움

print(get_age(1990, 2024)) # 34
Advertisement