[Python 100일 챌린지] Day 67 - 함수형 프로그래밍 심화
functools는 함수형 프로그래밍의 보물 상자예요! 🎁✨
어제 배운 map, filter, reduce는 시작일 뿐이에요! functools 모듈에는
partial로 함수를 미리 설정하고,lru_cache로 성능을 몇백 배 높이고,singledispatch로 함수를 타입별로 분기할 수 있는 강력한 도구들이 가득해요! Django, Flask 같은 프레임워크도 이런 기법을 활발히 사용하고 있답니다. 😊오늘 배울 내용들은 실무에서 코드를 획기적으로 개선할 수 있는 비법이에요!
(40분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 66: 함수형 프로그래밍 기초 - map, filter, reduce
- Day 61-62: 데코레이터 - 데코레이터 개념
🎯 학습 목표 1: functools 모듈 마스터하기
한 줄 설명
functools = 함수를 더 똑똑하게 만드는 도구 상자 🧰✨
함수에 슈퍼파워를 더해주는 파이썬 내장 모듈이에요!
1.1 partial: 부분 적용 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from functools import partial
# 기본 함수
def power(base, exponent):
return base ** exponent
# 지수를 고정한 함수 생성
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
# 실전: 로깅 함수
def log(level, message):
print(f"[{level}] {message}")
info = partial(log, "INFO")
error = partial(log, "ERROR")
info("프로그램 시작") # [INFO] 프로그램 시작
error("오류 발생") # [ERROR] 오류 발생
💡 실생활 비유: partial은 설정이 저장된 리모컨 같아요! TV 리모컨에 “넷플릭스 버튼”을 누르면 자동으로 HDMI 3번으로 전환되는 것처럼, 자주 쓰는 설정을 미리 저장해두는 거예요!
partial이 왜 유용할까요? 🤔
- 같은 인자를 반복해서 전달할 필요 없어요
- 함수를 특정 용도로 맞춤 제작할 수 있어요
- 코드가 훨씬 깔끔해져요!
1.2 wraps: 함수 메타데이터 보존
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import wraps
def my_decorator(func):
@wraps(func) # 원본 함수의 정보 보존
def wrapper(*args, **kwargs):
"""래퍼 함수"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""인사하는 함수"""
return f"Hello, {name}!"
print(greet.__name__) # greet (wraps 없으면 wrapper)
print(greet.__doc__) # 인사하는 함수
💡 핵심: Day 61에서 배운 것처럼, 데코레이터 만들 때는 항상
@wraps(func)를 사용하세요! 그래야 원본 함수의 이름과 문서가 보존돼요!
1.3 reduce: 누적 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from functools import reduce
# 합계
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda x, y: x + y, numbers)
print(total) # 15
# 곱셈
product = reduce(lambda x, y: x * y, numbers)
print(product) # 120
# 실전: 딕셔너리 병합
dicts = [
{'a': 1, 'b': 2},
{'b': 3, 'c': 4},
{'c': 5, 'd': 6}
]
merged = reduce(lambda acc, d: {**acc, **d}, dicts, {})
print(merged) # {'a': 1, 'b': 3, 'c': 5, 'd': 6}
💡 실생활 비유: reduce는 여러 사람의 의견을 하나로 모으는 회의 같아요! 처음엔 첫 번째 의견으로 시작해서, 두 번째 의견을 합치고, 세 번째 의견을 합치고… 결국 하나의 결론이 나오죠!
1.4 total_ordering: 비교 연산자 자동 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
return self.grade == other.grade
def __lt__(self, other):
return self.grade < other.grade
# __eq__와 __lt__만 정의하면
# __le__, __gt__, __ge__ 자동 생성!
alice = Student("Alice", 85)
bob = Student("Bob", 90)
print(alice < bob) # True
print(alice <= bob) # True
print(alice > bob) # False
print(alice >= bob) # False
💡 실생활 비유: total_ordering은 자동 완성 기능 같아요! “크다”와 “같다”만 정의하면, “작다”, “크거나 같다”, “작거나 같다”를 자동으로 만들어줘요!
왜 유용할까요? 🌟
- 비교 연산자를 일일이 다 만들 필요 없어요
- 코드가 훨씬 간결해져요
- 실수로 비교 로직이 엇갈릴 위험이 없어요!
🎯 학습 목표 2: 파셜 함수와 커링
한 줄 설명
파셜 = 함수의 일부 인자를 미리 채워두기 🍕
피자 주문할 때 “치즈 피자, 라지 사이즈”는 항상 고정하고 토핑만 바꾸는 것처럼!
2.1 파셜 함수 심화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import partial
# 복잡한 함수
def greet(greeting, name, punctuation):
return f"{greeting}, {name}{punctuation}"
# 다양한 파셜 함수 생성
hello = partial(greet, "Hello")
hello_name = partial(hello, punctuation="!")
print(hello("Alice", "!")) # Hello, Alice!
print(hello_name("Bob")) # Hello, Bob!
# 실전: API 요청 함수
def api_request(base_url, endpoint, method, data=None):
return f"{method} {base_url}/{endpoint}"
# 특정 API 서버에 대한 함수들
my_api = partial(api_request, "https://api.example.com")
get_users = partial(my_api, "users", "GET")
create_user = partial(my_api, "users", "POST")
print(get_users()) # GET https://api.example.com/users
print(create_user(data={})) # POST https://api.example.com/users
실무에서 이렇게 써요! 💼
- API 클라이언트에서 base URL 고정해서 사용
- 로깅 함수에서 로그 레벨 고정
- 이벤트 핸들러에서 특정 설정 고정
💡 핵심: partial은 함수를 재사용 가능한 작은 조각들로 만들어요! DRY(Don’t Repeat Yourself) 원칙의 완벽한 구현이에요!
2.2 커링 (Currying)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 커링: 여러 인자를 받는 함수를 하나씩 받는 함수로 변환
def curry(func):
"""함수를 커리된 버전으로 변환"""
def curried(*args):
if len(args) >= func.__code__.co_argcount:
return func(*args)
return lambda *more_args: curried(*(args + more_args))
return curried
# 일반 함수
def add_three(a, b, c):
return a + b + c
# 커리된 버전
curried_add = curry(add_three)
print(curried_add(1)(2)(3)) # 6
print(curried_add(1, 2)(3)) # 6
print(curried_add(1)(2, 3)) # 6
💡 실생활 비유: 커링은 양파 껍질 벗기기 같아요! 한 번에 하나씩 인자를 받아서, 층층이 함수를 만들어가는 거예요. Haskell 같은 함수형 언어에서 아주 흔하게 쓰여요!
커링이 뭐예요? 🤔
- 여러 인자를 한 번에 받는 대신, 하나씩 받아요
- 부분 적용이 더 자연스러워져요
- 함수 조합이 쉬워져요!
🎯 학습 목표 3: 메모이제이션과 캐싱
한 줄 설명
메모이제이션 = 계산 결과를 저장해두고 재사용 💾✨
같은 계산을 반복하지 말고, 한 번 계산한 건 저장해뒀다 꺼내 쓰는 거예요!
3.1 lru_cache: 자동 캐싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from functools import lru_cache
# 캐싱 없이
def fibonacci_slow(n):
"""느린 피보나치 (지수 시간)"""
if n < 2:
return n
return fibonacci_slow(n - 1) + fibonacci_slow(n - 2)
# 캐싱 적용
@lru_cache(maxsize=None)
def fibonacci_fast(n):
"""빠른 피보나치 (선형 시간)"""
if n < 2:
return n
return fibonacci_fast(n - 1) + fibonacci_fast(n - 2)
import time
# 비교
start = time.time()
print(fibonacci_slow(30)) # 832040
print(f"시간: {time.time() - start:.4f}초") # ~0.2초
start = time.time()
print(fibonacci_fast(30)) # 832040
print(f"시간: {time.time() - start:.4f}초") # ~0.0001초
# 캐시 정보 확인
print(fibonacci_fast.cache_info())
💡 실생활 비유: lru_cache는 전화번호 최근 통화 목록 같아요! 자주 전화하는 사람은 목록에 저장되어 있어서 다시 번호를 찾을 필요 없죠. LRU는 “Least Recently Used”로, 오래 안 쓴 건 지워요!
lru_cache의 마법! 🎩✨
- 피보나치(30) 계산: 캐시 없으면 0.2초 → 캐시 있으면 0.0001초!
- 수백 배 빨라져요!
- 복잡한 계산, API 호출, 데이터베이스 쿼리 등에 최고예요!
3.2 캐시 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_calculation(n):
"""비용이 큰 계산"""
print(f"계산 중: {n}")
return n ** 2
# 첫 호출: 계산 수행
print(expensive_calculation(5)) # 계산 중: 5 \n 25
# 두 번째 호출: 캐시에서 가져옴
print(expensive_calculation(5)) # 25 (출력 없음)
# 캐시 정보
print(expensive_calculation.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
# 캐시 지우기
expensive_calculation.cache_clear()
print(expensive_calculation(5)) # 계산 중: 5 \n 25
3.3 커스텀 메모이제이션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def memoize(func):
"""커스텀 메모이제이션 데코레이터"""
cache = {}
def wrapper(*args):
if args not in cache:
print(f"캐시 미스: {args}")
cache[args] = func(*args)
else:
print(f"캐시 히트: {args}")
return cache[args]
wrapper.cache = cache
return wrapper
@memoize
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
print(f"캐시 크기: {len(factorial.cache)}")
print(factorial.cache)
💡 캐시 팁:
maxsize=None으로 설정하면 무제한 캐시! 하지만 메모리를 많이 쓸 수 있으니 주의하세요!
🎯 학습 목표 4: 고급 함수형 패턴
한 줄 설명
함수 조합 = 레고 블록처럼 함수를 연결하기 🧱🔗
작은 함수들을 파이프로 연결해서 복잡한 작업을 만들어요!
4.1 함수 파이프라인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import reduce
def pipe(*functions):
"""함수 파이프라인 (왼쪽에서 오른쪽)"""
return lambda x: reduce(lambda v, f: f(v), functions, x)
# 함수들
add_10 = lambda x: x + 10
multiply_2 = lambda x: x * 2
square = lambda x: x ** 2
# 파이프라인 구성
process = pipe(add_10, multiply_2, square)
print(process(5)) # ((5 + 10) * 2) ** 2 = 900
💡 실생활 비유: 파이프라인은 공장 생산 라인 같아요! 원자재가 들어가서 → 가공하고 → 조립하고 → 포장해서 → 완제품이 나오는 것처럼, 데이터가 함수들을 거쳐 변환돼요!
파이프라인이 왜 좋을까요? 🌟
- 코드가 읽기 쉬워요 (왼쪽에서 오른쪽으로)
- 각 단계가 명확해요
- 재사용하기 쉬워요!
4.2 모나드 패턴 (Maybe Monad)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Maybe:
"""값이 있을 수도, 없을 수도 있는 컨테이너"""
def __init__(self, value):
self.value = value
def map(self, func):
"""값이 있으면 함수 적용"""
if self.value is None:
return Maybe(None)
return Maybe(func(self.value))
def get_or_else(self, default):
"""값이 없으면 기본값 반환"""
return self.value if self.value is not None else default
# 사용
result = (Maybe(5)
.map(lambda x: x * 2)
.map(lambda x: x + 10)
.get_or_else(0))
print(result) # 20
# None 처리
result = (Maybe(None)
.map(lambda x: x * 2)
.map(lambda x: x + 10)
.get_or_else(0))
print(result) # 0
💡 Maybe Monad: 함수형 프로그래밍의 고급 패턴이에요! None 값을 안전하게 처리할 수 있어요. 자바의 Optional, 스칼라의 Option과 같은 개념이랍니다!
4.3 실전: 데이터 검증 파이프라인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from functools import reduce
def validate_email(email):
"""이메일 검증"""
if '@' not in email:
return None, "이메일에 @가 없습니다"
if '.' not in email.split('@')[1]:
return None, "도메인이 유효하지 않습니다"
return email, None
def validate_age(age):
"""나이 검증"""
if age < 0:
return None, "나이는 0 이상이어야 합니다"
if age > 150:
return None, "나이가 너무 큽니다"
return age, None
def validate_name(name):
"""이름 검증"""
if len(name) < 2:
return None, "이름은 2글자 이상이어야 합니다"
return name, None
# 검증 파이프라인
def validate_user(data):
"""사용자 데이터 검증"""
validators = [
('email', validate_email),
('age', validate_age),
('name', validate_name)
]
errors = []
for field, validator in validators:
value, error = validator(data.get(field))
if error:
errors.append(f"{field}: {error}")
return (data, None) if not errors else (None, errors)
# 테스트
user1 = {'email': '[email protected]', 'age': 25, 'name': 'Alice'}
data, error = validate_user(user1)
print(f"유효: {data is not None}") # True
user2 = {'email': 'invalid', 'age': 200, 'name': 'A'}
data, errors = validate_user(user2)
print(f"오류: {errors}")
4.4 실전: 함수 조합
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from functools import reduce
class Composer:
"""함수 조합 유틸리티"""
def __init__(self, *functions):
self.functions = functions
def __call__(self, arg):
return reduce(lambda v, f: f(v), self.functions, arg)
def __rshift__(self, other):
""">> 연산자로 파이프라인 구성"""
if isinstance(other, Composer):
return Composer(*self.functions, *other.functions)
return Composer(*self.functions, other)
# 사용
add_10 = lambda x: x + 10
multiply_2 = lambda x: x * 2
square = lambda x: x ** 2
# 방법 1
pipeline = Composer(add_10, multiply_2, square)
print(pipeline(5)) # 900
# 방법 2: >> 연산자
pipeline = Composer(add_10) >> multiply_2 >> square
print(pipeline(5)) # 900
실무에서 이렇게 써요! 💼
- 사용자 입력 검증 (회원가입, 로그인)
- API 요청 데이터 검증
- 설정 파일 검증
💡 오늘의 핵심 요약
- functools.partial - 인자 미리 채우기 🍕
1 2
from functools import partial square = partial(pow, exp=2)
- functools.lru_cache - 자동 캐싱으로 성능 폭발! ⚡
1 2 3
@lru_cache(maxsize=128) def expensive_func(n): return n ** 2
- functools.reduce - 여러 값을 하나로 합치기 📉
1
total = reduce(lambda x, y: x + y, numbers)
- 함수형 패턴:
- 🔗 파이프라인: 함수를 연결해요
- 🧅 커링: 인자를 하나씩 받아요
- 💾 메모이제이션: 결과를 저장해요
- 🧱 함수 조합: 작은 함수를 조립해요
🧪 연습 문제
문제 1: 캐싱된 API 호출 함수
API 호출 결과를 캐싱하는 함수를 작성하세요. 같은 user_id로 두 번 호출하면 두 번째는 캐시에서 가져와야 해요!
1
2
3
4
5
6
7
# 여기에 코드 작성
def fetch_user_data(user_id):
pass
# 테스트
print(fetch_user_data(1)) # API 호출
print(fetch_user_data(1)) # 캐시에서 (빠름!)
💡 힌트
단계별 힌트:
@lru_cache데코레이터 사용하기maxsize=100정도로 설정하기time.sleep(0.5)로 API 호출 시뮬레이션cache_info()로 캐시 통계 확인하기
핵심 키워드: lru_cache, maxsize, cache_info()
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import lru_cache
import time
@lru_cache(maxsize=100)
def fetch_user_data(user_id):
"""사용자 데이터 가져오기 (캐싱됨)"""
print(f"🔄 API 호출: user_id={user_id}")
time.sleep(0.5) # API 호출 시뮬레이션
return {
'id': user_id,
'name': f'User {user_id}',
'email': f'user{user_id}@example.com'
}
# 테스트
start = time.time()
print(fetch_user_data(1)) # API 호출
print(fetch_user_data(1)) # 캐시에서 가져옴 (빠름)
print(f"⏱️ 총 시간: {time.time() - start:.4f}초")
print(f"📊 캐시 정보:", fetch_user_data.cache_info())
출력:
1
2
3
4
5
🔄 API 호출: user_id=1
{'id': 1, 'name': 'User 1', 'email': '[email protected]'}
{'id': 1, 'name': 'User 1', 'email': '[email protected]'}
⏱️ 총 시간: 0.5021초
📊 캐시 정보: CacheInfo(hits=1, misses=1, maxsize=100, currsize=1)
설명:
- 첫 호출: API 호출 (0.5초)
- 두 번째 호출: 캐시에서 가져옴 (즉시!)
- hits=1: 캐시 적중 1회
- misses=1: 캐시 미스 1회
문제 2: 함수 파이프라인 만들기
pipe 함수를 사용해서 “10 더하고 → 2배 곱하고 → 제곱하기” 파이프라인을 만드세요!
💡 힌트
단계별 힌트:
reduce를 사용해서 함수들을 순차적으로 적용- 람다 함수 3개 만들기 (add_10, multiply_2, square)
pipe(*functions)형태로 조합하기
핵심 키워드: reduce, lambda, 함수 조합
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import reduce
def pipe(*functions):
"""함수 파이프라인 (왼쪽에서 오른쪽)"""
return lambda x: reduce(lambda v, f: f(v), functions, x)
# 함수들 정의
add_10 = lambda x: x + 10
multiply_2 = lambda x: x * 2
square = lambda x: x ** 2
# 파이프라인 구성
process = pipe(add_10, multiply_2, square)
print(process(5)) # ((5 + 10) * 2) ** 2 = 900
계산 과정:
- 5 → add_10 → 15
- 15 → multiply_2 → 30
- 30 → square → 900
📝 오늘 배운 내용 정리
- functools.partial: 함수의 인자를 미리 채워서 새로운 함수를 만들어요
- functools.lru_cache: 함수 결과를 자동으로 캐싱해서 성능을 획기적으로 높여요
- 커링: 여러 인자를 한 번에 받는 대신 하나씩 받는 함수로 변환해요
- 함수 파이프라인: 작은 함수들을 연결해서 복잡한 작업을 처리해요
- 메모이제이션: 한 번 계산한 결과를 저장해서 재사용해요
🔗 관련 자료
📚 이전 학습
Day 66: 함수형 프로그래밍 기초 ⭐⭐⭐
어제는 map, filter, reduce로 함수형 프로그래밍의 기초를 배웠어요!
📚 다음 학습
Day 68: 고급 컬렉션 (collections 모듈) ⭐⭐⭐
내일은 Counter, defaultdict, deque 등 파이썬의 강력한 컬렉션 타입들을 배워요!
“작은 함수들을 조합하면 놀라운 일을 할 수 있어요!” 🚀
Day 67/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
