파이썬(Python) 공부 6편
함수(Function) 정의와 활용 완전 정복
함수는 코드를 재사용 가능한 단위로 묶는 도구입니다. 같은 코드를 여러 번 복사하는 대신, 함수로 정의하고 필요할 때 호출하면 됩니다. def·return부터 기본값 매개변수·*args·**kwargs·lambda·재귀함수까지 파이썬 함수의 모든 것을 정리합니다.
🔨 함수(Function)란 — 재사용 가능한 코드 블록
함수는 특정 작업을 수행하는 코드 블록에 이름을 붙인 것입니다. 한 번 정의하면 여러 곳에서 호출(실행)할 수 있어 코드 중복을 줄이고 유지보수를 쉽게 만듭니다.
📝 함수 정의와 호출 — def, return
# 가장 단순한 함수 — 매개변수 없음, 반환값 없음
def say_hello():
print("안녕하세요!")
say_hello() # 함수 호출 → 안녕하세요!
say_hello() # 여러 번 호출 가능
# 매개변수가 있는 함수
def greet(name):
print(f"안녕하세요, {name}님!")
greet("Alice") # 안녕하세요, Alice님!
greet("Bob") # 안녕하세요, Bob님!
# return으로 값 반환하는 함수
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 8
print(add(10, 20)) # 30 (바로 사용도 가능)
# 여러 값 반환 (튜플로 묶어서 반환)
def min_max(numbers):
return min(numbers), max(numbers)
lo, hi = min_max([3, 1, 9, 4, 7])
print(lo, hi) # 1 9
함수 첫 줄에 삼중 따옴표로 설명을 작성하면 help(함수명)으로 조회할 수 있습니다. 협업 및 유지보수 시 큰 도움이 됩니다.
def add(a, b):
"""두 수를 더해 반환합니다.
Args:
a: 첫 번째 숫자
b: 두 번째 숫자
Returns:
두 수의 합
"""
return a + b
help(add) # 독스트링 출력
📋 매개변수의 종류 — 5가지 총정리
| 종류 | 문법 | 예시 | 설명 |
|---|---|---|---|
| 위치 인수 | def f(a, b) | f(1, 2) | 순서대로 매칭 |
| 키워드 인수 | def f(a, b) | f(b=2, a=1) | 이름으로 매칭 (순서 무관) |
| 기본값 매개변수 | def f(a, b=10) | f(5) → b=10 | 생략 시 기본값 사용 |
| 가변 위치 인수 | def f(*args) | f(1, 2, 3, 4) | 개수 제한 없이 튜플로 수집 |
| 가변 키워드 인수 | def f(**kwargs) | f(x=1, y=2) | 키=값 쌍을 딕셔너리로 수집 |
# 기본값 매개변수 — 생략하면 기본값 사용
def greet(name, greeting="안녕하세요"):
print(f"{greeting}, {name}님!")
greet("Alice") # 안녕하세요, Alice님!
greet("Bob", "반갑습니다") # 반갑습니다, Bob님!
# 키워드 인수 — 이름으로 지정하면 순서 무관
def create_user(name, age, city):
print(f"{name} ({age}세) - {city}")
create_user(age=25, city="서울", name="Alice")
# Alice (25세) - 서울
# ⚠️ 기본값 매개변수는 반드시 일반 매개변수 뒤에 위치
# def f(a=1, b): → SyntaxError! (기본값이 앞에 오면 안 됨)
def f(a, b=1): ... # ✅ 올바른 순서
⚡ *args와 **kwargs — 가변 인수 처리
- *args: 위치 인수를 개수 제한 없이 받아 튜플로 묶음. 별표(*)가 핵심이고 args는 관례적인 이름
- **kwargs: 키워드 인수를 개수 제한 없이 받아 딕셔너리로 묶음. 별표 두 개(**)가 핵심이고 kwargs는 관례적인 이름
# *args — 위치 인수를 튜플로 수집
def total(*args):
print(type(args), args) # <class 'tuple'> (1, 2, 3, 4, 5)
return sum(args)
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, 4, 5)) # 15
print(total()) # 0 (인수 없어도 가능)
# **kwargs — 키워드 인수를 딕셔너리로 수집
def show_info(**kwargs):
print(type(kwargs), kwargs)
for key, value in kwargs.items():
print(f" {key}: {value}")
show_info(name="Alice", age=25, city="서울")
# <class 'dict'> {'name': 'Alice', 'age': 25, 'city': '서울'}
# name: Alice / age: 25 / city: 서울
# 모두 조합 — 순서 규칙: 일반 → *args → 기본값 → **kwargs
def mixed(first, *args, sep=", ", **kwargs):
print(f"first={first}")
print(f"args={args}")
print(f"sep={sep!r}")
print(f"kwargs={kwargs}")
mixed(1, 2, 3, sep=" | ", x=10, y=20)
# 리스트/딕셔너리를 언패킹해서 전달
def power(base, exp):
return base ** exp
params = [2, 10]
print(power(*params)) # 1024 (리스트 언패킹)
kparams = {"base": 3, "exp": 3}
print(power(**kparams)) # 27 (딕셔너리 언패킹)
🌐 변수 스코프(Scope) — LEGB 규칙
파이썬은 변수를 찾을 때 LEGB 순서로 탐색합니다. Local(지역) → Enclosing(중첩함수의 바깥) → Global(전역) → Built-in(내장) 순입니다.
# 전역 변수 vs 지역 변수
x = 10 # 전역 변수 (Global)
def func():
x = 20 # 지역 변수 (Local) — 전역 x와 별개
print(x) # 20
func()
print(x) # 10 (전역 x는 그대로)
# global 키워드 — 함수 안에서 전역 변수 수정
count = 0
def increment():
global count # 전역 변수임을 명시
count += 1
increment()
increment()
print(count) # 2
# Enclosing — 중첩 함수에서 바깥 함수 변수 접근
def outer():
msg = "바깥 함수"
def inner():
print(msg) # Enclosing 스코프에서 msg 접근
inner()
outer() # 바깥 함수
global 키워드를 남용하면 코드 흐름을 추적하기 어려워집니다. 함수는 필요한 값을 매개변수로 받아 return으로 반환하는 구조가 좋습니다. global은 정말 필요한 경우에만 최소한으로 사용하는 것이 권장됩니다.
🔍 타입 힌트(Type Hint) — 코드 가독성 향상
파이썬 3.5+에서 도입된 타입 힌트는 매개변수와 반환값의 자료형을 명시합니다. 실행에는 영향이 없지만 IDE 자동완성·오류 감지·협업 코드 가독성에 큰 도움이 됩니다.
# 타입 힌트 없는 버전
def add(a, b):
return a + b
# 타입 힌트 있는 버전 — 파라미터: 타입, -> 반환타입
def add(a: int, b: int) -> int:
return a + b
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
def get_scores() -> list[int]: # 반환값이 리스트
return [90, 85, 92]
def find_user(uid: int) -> str | None: # None이거나 str (3.10+)
if uid == 1:
return "Alice"
return None
λ lambda — 익명 함수
lambda는 이름 없이 한 줄로 작성하는 간단한 함수입니다. lambda 매개변수: 표현식 형태이며, 표현식 하나만 작성할 수 있고 return을 명시하지 않습니다.
# 일반 함수 vs lambda
def square(x):
return x ** 2
square_lambda = lambda x: x ** 2
print(square(5)) # 25
print(square_lambda(5)) # 25
# 인수 여러 개
add = lambda a, b: a + b
print(add(3, 7)) # 10
# lambda의 진가 — 고차 함수와 조합 (sorted, map, filter)
students = [
{"name": "Alice", "score": 85},
{"name": "Bob", "score": 92},
{"name": "Carol", "score": 78},
]
# 점수 기준으로 정렬
sorted_s = sorted(students, key=lambda s: s["score"], reverse=True)
for s in sorted_s:
print(f"{s['name']}: {s['score']}")
# Bob: 92 / Alice: 85 / Carol: 78
# map() — 모든 요소에 함수 적용
nums = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, nums))
print(squared) # [1, 4, 9, 16, 25]
# filter() — 조건에 맞는 요소만 남김
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
🔁 재귀 함수(Recursive Function) — 자기 자신을 호출
재귀 함수는 함수 안에서 자기 자신을 호출하는 함수입니다. 반드시 종료 조건(base case)이 있어야 하며, 없으면 무한 재귀 → RecursionError가 발생합니다.
# 팩토리얼 — 재귀의 대표 예제
# 5! = 5 × 4 × 3 × 2 × 1 = 120
def factorial(n: int) -> int:
if n <= 1: # ① 종료 조건 (base case)
return 1
return n * factorial(n - 1) # ② 재귀 호출
print(factorial(5)) # 120
print(factorial(10)) # 3628800
# 피보나치 수열
# 0, 1, 1, 2, 3, 5, 8, 13, 21 ...
def fib(n: int) -> int:
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
for i in range(10):
print(fib(i), end=" ") # 0 1 1 2 3 5 8 13 21 34
- 파이썬 기본 최대 재귀 깊이: 1000회 (sys.getrecursionlimit()으로 확인) → 초과 시 RecursionError
- 피보나치 재귀의 성능 문제: fib(40)이면 수십억 번 중복 계산 → 실무에서는 반복문이나 메모이제이션(lru_cache) 활용
- 언제 재귀를 쓰나: 트리 탐색, 폴더 구조 순회, 분할정복 알고리즘 등 구조가 재귀적인 문제에 적합
# functools.lru_cache로 성능 개선
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2) # 결과 캐싱으로 중복 계산 방지
💻 실전 예제 — 함수로 구성한 학점 계산기
# 각 기능을 함수로 분리 → 읽기 쉽고 재사용 가능
def get_grade(score: int) -> str:
"""점수를 받아 학점 문자열을 반환합니다."""
if score >= 95: return "A+"
elif score >= 90: return "A"
elif score >= 85: return "B+"
elif score >= 80: return "B"
elif score >= 70: return "C"
elif score >= 60: return "D"
else: return "F"
def calc_average(*scores: int) -> float:
"""가변 개수의 점수를 받아 평균을 반환합니다."""
if not scores:
return 0.0
return sum(scores) / len(scores)
def print_report(name: str, **subjects) -> None:
"""학생 이름과 과목별 점수를 받아 성적표를 출력합니다."""
print(f"\n{'='*30}")
print(f" {name} 성적표")
print(f"{'='*30}")
for subj, score in subjects.items():
grade = get_grade(score)
print(f" {subj:8}: {score:3}점 ({grade})")
avg = calc_average(*subjects.values())
print(f" {'평균':8}: {avg:.1f}점 ({get_grade(int(avg))})")
print(f"{'='*30}")
# 함수 호출
print_report("Alice", Python=92, Math=88, English=95)
📝 6편 실습 문제
- calculate(a, b, op="+") 함수를 만드세요
- op가 "+", "-", "*", "/" 일 때 각각 계산 결과 반환
- 0으로 나누기 시도 시 "0으로 나눌 수 없습니다" 반환
- 지원하지 않는 op 입력 시 "지원하지 않는 연산자" 반환
- stats(*numbers) 함수: 최솟값·최댓값·합계·평균을 딕셔너리로 반환
- 단어 리스트를 lambda로 글자 수 기준 오름차순 정렬
# 실습 1 — 계산기 함수
def calculate(a: float, b: float, op: str = "+") -> float | str:
if op == "+": return a + b
elif op == "-": return a - b
elif op == "*": return a * b
elif op == "/":
if b == 0: return "0으로 나눌 수 없습니다"
return a / b
return "지원하지 않는 연산자"
print(calculate(10, 3, "+")) # 13
print(calculate(10, 0, "/")) # 0으로 나눌 수 없습니다
# 실습 2 — stats 함수 & lambda 정렬
def stats(*numbers: float) -> dict:
return {
"min": min(numbers),
"max": max(numbers),
"sum": sum(numbers),
"avg": sum(numbers) / len(numbers),
}
print(stats(3, 1, 9, 4, 7))
# {'min': 1, 'max': 9, 'sum': 24, 'avg': 4.8}
words = ["python", "is", "fun", "and", "powerful"]
sorted_words = sorted(words, key=lambda w: len(w))
print(sorted_words)
# ['is', 'fun', 'and', 'python', 'powerful']
- 함수 정의: def 이름(매개변수): 콜론 필수 / return 생략 시 None 반환
- 여러 값 반환: return a, b → 튜플로 묶여 반환 / 언패킹으로 받기
- 기본값 매개변수: def f(a, b=10) / 기본값은 일반 매개변수 뒤에
- 키워드 인수: f(b=2, a=1) 이름으로 지정 / 순서 무관
- *args: 가변 위치 인수 → 튜플로 수집
- **kwargs: 가변 키워드 인수 → 딕셔너리로 수집
- 스코프 LEGB: Local → Enclosing → Global → Built-in 순 탐색 / global 키워드 남용 금지
- 타입 힌트: def f(a: int) -> str: 실행에 영향 없음 / 가독성·IDE 지원
- lambda: lambda 매개변수: 표현식 / sorted·map·filter와 조합
- 재귀 함수: 종료 조건(base case) 필수 / 재귀 깊이 제한 1000회 / 성능 주의
리스트 메서드 / 컴프리헨션 / 딕셔너리 심화 / 집합 연산 / 불변 vs 가변