return의 정확한 동작과 순수 함수·부수 효과 개념으로 견고한 함수를 설계하는 방법을 배운다.
🎯 이 강의에서 배우는 것
함수 설계의 핵심은 무엇을 반환하고 무엇을 하지 않느냐입니다. 순수 함수(pure function)와 부수 효과(side effect)의 개념은 버그가 적고 테스트하기 쉬운 코드를 작성하는 데 결정적인 기준이 됩니다. 이 강의에서 return의 정확한 동작과 함수 설계의 원칙을 배웁니다.
↩️ return 문의 정확한 동작
# return은 두 가지 일을 함
# 1. 값을 호출자에게 전달
# 2. 함수를 즉시 종료 (이후 코드 실행 안 됨)
def early_return(x):
if x < 0:
return "음수" # 여기서 함수 종료
if x == 0:
return "영" # 여기서 함수 종료
return "양수" # 위 조건 모두 해당 안 될 때
print(early_return(-5)) # 음수
print(early_return(0)) # 영
print(early_return(7)) # 양수
# return 없으면 None 반환
def no_return():
x = 42
# return 없음 → 암묵적으로 return None
result = no_return()
print(result) # None
print(result is None) # True
# 명시적 return None (의도를 명확히)
def find_first_negative(lst):
for x in lst:
if x < 0:
return x # 찾으면 반환
return None # 없으면 None 명시적 반환 (의도 명확)
print(find_first_negative([1, 2, -3, 4])) # -3
print(find_first_negative([1, 2, 3])) # None
📦 여러 값 반환 — 파이썬의 튜플 반환
# 파이썬의 return a, b 는 사실 return (a, b) — 튜플
def min_max(lst):
return min(lst), max(lst) # 튜플 반환
result = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(type(result)) # <class 'tuple'>
print(result) # (1, 9)
# 언패킹으로 받기 (파이썬다운 방법)
minimum, maximum = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(f"최솟값: {minimum}, 최댓값: {maximum}")
# 여러 값 반환 vs 딕셔너리 반환 비교
def get_stats_tuple(data):
return min(data), max(data), sum(data) / len(data)
def get_stats_dict(data):
return {
"min": min(data),
"max": max(data),
"avg": sum(data) / len(data),
}
# 딕셔너리: 반환 필드가 많거나 이름이 중요할 때
stats = get_stats_dict([10, 20, 30, 40, 50])
print(stats["avg"]) # 30.0
# Python 3.7+ namedtuple / dataclass로 이름 있는 튜플 반환
from collections import namedtuple
Stats = namedtuple("Stats", ["min", "max", "avg"])
def get_stats_named(data):
return Stats(min(data), max(data), sum(data) / len(data))
s = get_stats_named([10, 20, 30, 40, 50])
print(s.avg) # 30.0
print(s[0]) # 10 (인덱스 접근도 가능)
min_val, max_val, avg = s # 언패킹도 가능
✨ 순수 함수 — 예측 가능한 코드의 토대
# 순수 함수의 2가지 조건:
# 1. 동일한 입력 → 항상 동일한 출력 (결정적)
# 2. 외부 상태를 변경하지 않음 (no side effects)
# ✅ 순수 함수 예시
def add(a, b):
return a + b # 항상 a+b, 외부 변경 없음
def celsius_to_fahrenheit(c):
return c * 9/5 + 32 # 항상 같은 결과
def filter_positive(nums):
return [x for x in nums if x > 0] # 새 리스트 반환, 원본 불변
# ❌ 순수하지 않은 함수 (부수 효과 있음)
total = 0
def add_to_total(x):
global total # 전역 변수 수정 → 부수 효과!
total += x
import random
def random_greeting():
greetings = ["안녕", "반가워", "잘 지내?"]
return random.choice(greetings) # 같은 입력인데 다른 출력 가능 → 비결정적
# 순수 함수의 장점
# 1. 테스트 쉬움: 입력/출력만 확인하면 됨
# 2. 추론 쉬움: 외부 상태 모르고도 이해 가능
# 3. 캐싱 가능: 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(50)) # 즉각 계산 (캐시 덕분)
⚡ 부수 효과 — 언제 필요하고 어떻게 관리할까
# 부수 효과의 종류:
# - 전역/외부 변수 변경
# - 파일/DB/네트워크 I/O
# - 화면 출력 (print)
# - 인자로 받은 가변 객체 수정
# 부수 효과는 피할 수 없지만, 분리하고 명확히 해야 함
# 나쁜 설계: 계산과 I/O를 섞음
def calculate_and_save_bad(data, filepath):
total = sum(data)
average = total / len(data)
result = {"total": total, "average": average}
# 계산 + 파일 저장을 한 함수에서 — 테스트 어려움!
with open(filepath, "w") as f:
import json
json.dump(result, f)
return result
# 좋은 설계: 계산(순수)과 I/O를 분리
def calculate_stats(data): # 순수 함수 — 테스트 쉬움
total = sum(data)
return {"total": total, "average": total / len(data)}
def save_stats(stats, filepath): # I/O 전담 — 부수 효과 명확
with open(filepath, "w") as f:
import json
json.dump(stats, f)
# 사용
stats = calculate_stats([10, 20, 30])
# save_stats(stats, "output.json")
# 인자 변이 — 조심해야 할 부수 효과
def sort_inplace(lst):
lst.sort() # ← 원본 수정 (부수 효과!)
def sort_new(lst):
return sorted(lst) # ← 새 리스트 반환 (순수)
data = [3, 1, 2]
sort_inplace(data)
print(data) # [1, 2, 3] ← 원본이 변경됨!
data2 = [3, 1, 2]
result = sort_new(data2)
print(data2) # [3, 1, 2] ← 원본 보존
print(result) # [1, 2, 3]
📐 함수 설계 체크리스트
# 좋은 함수의 기준:
# ✅ 이름이 동사로 시작 (무엇을 하는지 명확)
def calculate_total_price(items): # ✅ calculate
pass
def user_data(): # ❌ 동사 없음 → get_user_data()
pass
# ✅ 단일 책임 원칙 (한 가지 일만)
def validate_email(email: str) -> bool: # ✅ 검증만
return "@" in email and "." in email.split("@")[-1]
def validate_and_send_email(email: str) -> bool: # ❌ 두 가지 일
pass
# ✅ 타입 힌트로 인터페이스 명확화
def parse_date(date_str: str) -> tuple[int, int, int]:
year, month, day = date_str.split("-")
return int(year), int(month), int(day)
# ✅ 일관적인 반환 타입 (섞이면 사용하기 어려움)
def get_user_bad(user_id: int):
if user_id < 0:
return None # None 반환
elif user_id == 0:
return False # False 반환 — 타입 불일치!
return {"id": user_id} # dict 반환
def get_user_good(user_id: int) -> dict | None: # 반환 타입이 명확
if user_id <= 0:
return None
return {"id": user_id}
# ✅ 매개변수 5개 이하 (많으면 dataclass/dict로 묶기)
from dataclasses import dataclass
@dataclass
class EmailConfig:
to: str
subject: str
body: str
cc: list = None
bcc: list = None
def send_email(config: EmailConfig) -> bool: # 매개변수 1개로 압축
pass
⚠️ 자주 하는 실수
- 반환 타입 불일치: 함수가 때는 int, 때는 None, 때는 False를 반환하면 호출부에서 타입 확인이 복잡해집니다. 일관된 타입을 반환하거나 Optional[type]으로 타입 힌트를 명시하세요.
- 인자 변이를 부수 효과로 사용: 함수가 받은 리스트를 정렬하거나 수정하면, 호출부에서 원본이 변경됐는지 모릅니다. 함수 이름에 "inplace" 또는 "sort_" 등으로 명시하거나 새 객체를 반환하세요.
- print()가 있는 함수를 "계산 함수"로 착각: print()는 부수 효과입니다. 로직 함수에 print를 넣으면 테스트 시 출력이 섞여 검증하기 어렵습니다.
📝 정리 및 다음 강의 예고
return은 값 반환과 함수 즉시 종료 두 가지 역할을 합니다.return a, b는 튜플 반환 — 언패킹으로 받거나 namedtuple로 이름을 붙이세요.- 순수 함수: 같은 입력 → 같은 출력, 외부 변경 없음. 테스트와 추론이 쉽습니다.
- 부수 효과는 피할 수 없지만 명확히 분리하고, 함수의 책임을 단순하게 유지하세요.
다음 강의: 스코프와 LEGB 규칙 — 변수를 찾는 순서(Local → Enclosing → Global → Built-in)와 global/nonlocal 키워드를 완전히 이해합니다.
관련 주제
- return 다중값 반환(튜플)
- 단일 책임 원칙(SRP)
- 순수 함수(Pure Function)
- 부수 효과(Side Effect)
- None vs 예외 반환기준
- 함수 설계 체크리스트
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
