함수형 프로그래밍 (Functional Programming)
함수형 프로그래밍 (Functional Programming)¶
1. 함수형 프로그래밍이란?¶
함수형 프로그래밍은 순수 함수와 불변 데이터를 중심으로 프로그램을 구성하는 패러다임입니다.
핵심 원칙¶
| 원칙 | 설명 |
|---|---|
| 순수 함수 | 같은 입력 → 같은 출력, 부작용 없음 |
| 불변성 | 데이터를 변경하지 않고 새로 생성 |
| 일급 함수 | 함수를 값처럼 전달/반환 |
| 선언적 | "어떻게"보다 "무엇을" 기술 |
명령형 vs 선언적¶
numbers = [1, 2, 3, 4, 5]
# 명령형 (어떻게)
result = []
for n in numbers:
if n % 2 == 0:
result.append(n ** 2)
print(result) # [4, 16]
# 선언적/함수형 (무엇을)
result = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers)))
print(result) # [4, 16]
# 더 파이썬다운 방식 (리스트 컴프리헨션)
result = [n ** 2 for n in numbers if n % 2 == 0]
print(result) # [4, 16]
2. 일급 함수 (First-Class Function)¶
파이썬에서 함수는 일급 객체입니다.
변수에 할당¶
def greet(name):
return f"Hello, {name}!"
# 함수를 변수에 할당
say_hello = greet
print(say_hello("Python")) # Hello, Python!
# 함수도 객체
print(type(greet)) # <class 'function'>
print(greet.__name__) # greet
함수를 인자로 전달¶
def apply_twice(func, value):
return func(func(value))
def add_ten(x):
return x + 10
result = apply_twice(add_ten, 5)
print(result) # 25 (5 + 10 + 10)
함수를 반환¶
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
3. 고차 함수 (Higher-Order Function)¶
함수를 인자로 받거나 함수를 반환하는 함수입니다.
map()¶
모든 요소에 함수를 적용합니다.
numbers = [1, 2, 3, 4, 5]
# 제곱
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]
# 문자열 변환
words = ["hello", "world"]
upper_words = list(map(str.upper, words))
print(upper_words) # ['HELLO', 'WORLD']
# 여러 이터러블
a = [1, 2, 3]
b = [10, 20, 30]
sums = list(map(lambda x, y: x + y, a, b))
print(sums) # [11, 22, 33]
filter()¶
조건을 만족하는 요소만 선택합니다.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 짝수만
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# 빈 문자열 제거
words = ["hello", "", "world", "", "python"]
non_empty = list(filter(None, words)) # falsy 값 제거
print(non_empty) # ['hello', 'world', 'python']
# 사용자 정의 조건
users = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 17},
{"name": "Charlie", "age": 25},
]
adults = list(filter(lambda u: u["age"] >= 18, users))
print(adults) # [{'name': 'Alice', ...}, {'name': 'Charlie', ...}]
reduce()¶
요소들을 누적하여 단일 값으로 줄입니다.
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# 합계
total = reduce(lambda acc, x: acc + x, numbers)
print(total) # 15
# 곱
product = reduce(lambda acc, x: acc * x, numbers)
print(product) # 120
# 초기값 지정
total_with_init = reduce(lambda acc, x: acc + x, numbers, 100)
print(total_with_init) # 115
# 최댓값 찾기
max_val = reduce(lambda a, b: a if a > b else b, numbers)
print(max_val) # 5
map + filter + reduce 조합¶
from functools import reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 짝수의 제곱의 합
result = reduce(
lambda acc, x: acc + x,
map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers))
)
print(result) # 220 (4 + 16 + 36 + 64 + 100)
4. lambda 표현식¶
익명 함수를 간결하게 정의합니다.
기본 문법¶
# lambda 인자: 표현식
add = lambda x, y: x + y
print(add(3, 4)) # 7
# 기본값
greet = lambda name="World": f"Hello, {name}!"
print(greet()) # Hello, World!
print(greet("Python")) # Hello, Python!
# 가변 인자
sum_all = lambda *args: sum(args)
print(sum_all(1, 2, 3, 4)) # 10
정렬에 활용¶
# 튜플 정렬
points = [(1, 2), (3, 1), (5, 4), (2, 2)]
# y 좌표로 정렬
sorted_by_y = sorted(points, key=lambda p: p[1])
print(sorted_by_y) # [(3, 1), (1, 2), (2, 2), (5, 4)]
# 딕셔너리 정렬
users = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35},
]
sorted_by_age = sorted(users, key=lambda u: u["age"])
print([u["name"] for u in sorted_by_age]) # ['Bob', 'Alice', 'Charlie']
lambda 주의사항¶
# 복잡한 로직은 일반 함수로
# 나쁜 예
complex_op = lambda x: (x ** 2 + 3 * x - 5) if x > 0 else (x ** 2 - 3 * x + 5)
# 좋은 예
def complex_operation(x):
if x > 0:
return x ** 2 + 3 * x - 5
return x ** 2 - 3 * x + 5
5. functools 모듈¶
함수형 프로그래밍을 위한 도구 모음입니다.
partial (부분 적용)¶
함수의 일부 인자를 고정합니다.
from functools import partial
def power(base, exponent):
return base ** exponent
# 제곱 함수
square = partial(power, exponent=2)
print(square(5)) # 25
# 세제곱 함수
cube = partial(power, exponent=3)
print(cube(5)) # 125
# 밑이 2인 거듭제곱
power_of_two = partial(power, 2)
print(power_of_two(10)) # 1024
lru_cache (메모이제이션)¶
함수 결과를 캐싱합니다.
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 캐시 덕분에 빠름
print(fibonacci(100)) # 354224848179261915075
# 캐시 정보
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# 캐시 초기화
fibonacci.cache_clear()
cache (Python 3.9+)¶
무제한 캐시 (lru_cache(maxsize=None)와 동일).
from functools import cache
@cache
def factorial(n):
return n * factorial(n - 1) if n else 1
print(factorial(10)) # 3628800
wraps (데코레이터 메타데이터 보존)¶
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper docstring"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello():
"""Say hello function"""
return "Hello!"
# 원본 함수 정보 유지
print(say_hello.__name__) # say_hello
print(say_hello.__doc__) # Say hello function
singledispatch (함수 오버로딩)¶
타입에 따라 다른 함수를 호출합니다.
from functools import singledispatch
@singledispatch
def process(value):
raise NotImplementedError(f"Cannot process {type(value)}")
@process.register(int)
def _(value):
return f"Processing integer: {value * 2}"
@process.register(str)
def _(value):
return f"Processing string: {value.upper()}"
@process.register(list)
def _(value):
return f"Processing list of {len(value)} items"
print(process(10)) # Processing integer: 20
print(process("hello")) # Processing string: HELLO
print(process([1, 2])) # Processing list of 2 items
6. operator 모듈¶
연산자를 함수로 사용할 수 있게 합니다.
산술 연산자¶
from operator import add, sub, mul, truediv, mod, pow
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# lambda 대신 operator 사용
total = reduce(add, numbers)
print(total) # 15
product = reduce(mul, numbers)
print(product) # 120
비교 연산자¶
from operator import lt, le, eq, ne, ge, gt
print(lt(3, 5)) # True (3 < 5)
print(eq(3, 3)) # True (3 == 3)
print(ge(5, 3)) # True (5 >= 3)
itemgetter, attrgetter¶
from operator import itemgetter, attrgetter
# itemgetter - 인덱스/키로 접근
data = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
sorted_data = sorted(data, key=itemgetter(1)) # 나이로 정렬
print(sorted_data) # [('Bob', 25), ('Alice', 30), ('Charlie', 35)]
# 딕셔너리에서 사용
users = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
]
get_name = itemgetter("name")
names = list(map(get_name, users))
print(names) # ['Alice', 'Bob']
# 여러 필드
get_info = itemgetter("name", "age")
print(get_info(users[0])) # ('Alice', 30)
# attrgetter - 속성 접근
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
people = [Person("Alice", 30), Person("Bob", 25)]
sorted_people = sorted(people, key=attrgetter("age"))
print([p.name for p in sorted_people]) # ['Bob', 'Alice']
methodcaller¶
from operator import methodcaller
words = ["hello", "WORLD", "Python"]
# 메서드 호출
upper = methodcaller("upper")
print(list(map(upper, words))) # ['HELLO', 'WORLD', 'PYTHON']
# 인자가 있는 메서드
replace_o = methodcaller("replace", "o", "0")
print(replace_o("hello world")) # hell0 w0rld
7. 순수 함수와 불변성¶
순수 함수¶
# 순수 함수: 부작용 없음
def pure_add(a, b):
return a + b
# 비순수 함수: 외부 상태 변경
total = 0
def impure_add(value):
global total
total += value
return total
# 비순수 함수: 입력 데이터 변경
def impure_append(lst, value):
lst.append(value) # 원본 변경
return lst
# 순수 버전
def pure_append(lst, value):
return lst + [value] # 새 리스트 반환
불변성 유지¶
# 리스트: 새 리스트 생성
original = [1, 2, 3]
modified = original + [4] # 원본 유지
modified = [*original, 4] # 언패킹
# 딕셔너리: 새 딕셔너리 생성
config = {"debug": True, "port": 8080}
new_config = {**config, "port": 9090} # 원본 유지
# 불변 자료구조
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p1 = Point(1, 2)
p2 = p1._replace(x=10) # 새 객체 생성
print(p1) # Point(x=1, y=2)
print(p2) # Point(x=10, y=2)
frozenset과 tuple¶
# 불변 집합
mutable_set = {1, 2, 3}
immutable_set = frozenset([1, 2, 3])
# 딕셔너리 키로 사용 가능
cache = {frozenset([1, 2]): "result"}
# 불변 리스트 (tuple)
mutable_list = [1, 2, 3]
immutable_list = (1, 2, 3)
8. 컴프리헨션¶
파이썬스러운 함수형 패턴입니다.
리스트 컴프리헨션¶
# map 대체
squares = [x ** 2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# filter 대체
evens = [x for x in range(10) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
# map + filter
even_squares = [x ** 2 for x in range(10) if x % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]
# 중첩
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
딕셔너리/집합 컴프리헨션¶
# 딕셔너리 컴프리헨션
names = ["Alice", "Bob", "Charlie"]
name_lengths = {name: len(name) for name in names}
print(name_lengths) # {'Alice': 5, 'Bob': 3, 'Charlie': 7}
# 집합 컴프리헨션
unique_lengths = {len(name) for name in names}
print(unique_lengths) # {3, 5, 7}
# 딕셔너리 키/값 뒤집기
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
제너레이터 표현식¶
# 메모리 효율적인 지연 평가
large_squares = (x ** 2 for x in range(1000000))
print(next(large_squares)) # 0
print(next(large_squares)) # 1
# sum, any, all과 함께
numbers = [1, 2, 3, 4, 5]
total = sum(x ** 2 for x in numbers) # 괄호 생략 가능
print(total) # 55
# 조건 검사
all_positive = all(x > 0 for x in numbers)
any_even = any(x % 2 == 0 for x in numbers)
print(all_positive, any_even) # True True
9. 파이프라인 패턴¶
함수를 연결하여 데이터 처리 파이프라인을 만듭니다.
함수 합성¶
def compose(*functions):
"""오른쪽에서 왼쪽으로 함수 합성"""
def composed(x):
for func in reversed(functions):
x = func(x)
return x
return composed
def pipe(*functions):
"""왼쪽에서 오른쪽으로 함수 합성"""
def piped(x):
for func in functions:
x = func(x)
return x
return piped
# 사용 예
add_one = lambda x: x + 1
double = lambda x: x * 2
square = lambda x: x ** 2
# compose: square(double(add_one(5))) = square(double(6)) = square(12) = 144
composed = compose(square, double, add_one)
print(composed(5)) # 144
# pipe: square(double(add_one(5)))와 동일하지만 읽기 쉬움
piped = pipe(add_one, double, square)
print(piped(5)) # 144
데이터 처리 파이프라인¶
from functools import reduce
# 파이프 연산자 흉내
class Pipe:
def __init__(self, value):
self.value = value
def __or__(self, func):
return Pipe(func(self.value))
def __repr__(self):
return repr(self.value)
# 사용
result = (
Pipe([1, 2, 3, 4, 5])
| (lambda x: [n * 2 for n in x]) # 두 배
| (lambda x: [n for n in x if n > 4]) # 4 초과만
| sum # 합계
)
print(result) # 24 (6 + 8 + 10)
실용적인 파이프라인¶
from functools import partial
def process_data(data):
"""데이터 처리 파이프라인"""
pipeline = [
# 1. 문자열 정리
lambda items: [s.strip().lower() for s in items],
# 2. 빈 문자열 제거
lambda items: [s for s in items if s],
# 3. 중복 제거 (순서 유지)
lambda items: list(dict.fromkeys(items)),
# 4. 정렬
sorted,
]
result = data
for transform in pipeline:
result = transform(result)
return result
raw_data = [" Hello ", "world", " ", "HELLO", "Python", "world"]
print(process_data(raw_data)) # ['hello', 'python', 'world']
10. itertools 활용¶
조합/순열¶
from itertools import permutations, combinations, product
# 순열
print(list(permutations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
# 조합
print(list(combinations([1, 2, 3], 2)))
# [(1, 2), (1, 3), (2, 3)]
# 데카르트 곱
print(list(product([1, 2], ['a', 'b'])))
# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
그룹화¶
from itertools import groupby
data = [
("fruit", "apple"),
("fruit", "banana"),
("vegetable", "carrot"),
("vegetable", "daikon"),
("fruit", "elderberry"),
]
# 정렬 후 그룹화 (연속된 동일 키만 그룹화됨)
sorted_data = sorted(data, key=lambda x: x[0])
for key, group in groupby(sorted_data, key=lambda x: x[0]):
print(f"{key}: {[item[1] for item in group]}")
# fruit: ['apple', 'banana', 'elderberry']
# vegetable: ['carrot', 'daikon']
무한 이터레이터¶
from itertools import count, cycle, repeat, islice
# count: 무한 카운터
for i in islice(count(10, 2), 5):
print(i, end=" ") # 10 12 14 16 18
# cycle: 무한 반복
colors = cycle(["red", "green", "blue"])
for _ in range(5):
print(next(colors), end=" ") # red green blue red green
# repeat: 값 반복
threes = list(repeat(3, 4))
print(threes) # [3, 3, 3, 3]
체이닝과 슬라이싱¶
from itertools import chain, islice, takewhile, dropwhile
# chain: 여러 이터러블 연결
combined = list(chain([1, 2], [3, 4], [5, 6]))
print(combined) # [1, 2, 3, 4, 5, 6]
# islice: 슬라이싱
first_three = list(islice(range(10), 3))
print(first_three) # [0, 1, 2]
# takewhile: 조건이 참인 동안
nums = [1, 3, 5, 8, 2, 4]
result = list(takewhile(lambda x: x < 6, nums))
print(result) # [1, 3, 5]
# dropwhile: 조건이 참인 동안 건너뛰기
result = list(dropwhile(lambda x: x < 6, nums))
print(result) # [8, 2, 4]
11. 실전 예제¶
데이터 변환¶
from functools import reduce
from operator import itemgetter
# 원본 데이터
orders = [
{"product": "apple", "quantity": 10, "price": 1.5},
{"product": "banana", "quantity": 5, "price": 0.8},
{"product": "apple", "quantity": 3, "price": 1.5},
{"product": "orange", "quantity": 8, "price": 2.0},
]
# 제품별 총 매출 계산
from itertools import groupby
def calculate_sales(orders):
# 제품별로 정렬 및 그룹화
sorted_orders = sorted(orders, key=itemgetter("product"))
result = {}
for product, group in groupby(sorted_orders, key=itemgetter("product")):
total = sum(o["quantity"] * o["price"] for o in group)
result[product] = total
return result
print(calculate_sales(orders))
# {'apple': 19.5, 'banana': 4.0, 'orange': 16.0}
함수형 검증¶
from functools import reduce
from typing import Callable, List, Any
def validate(value: Any, *validators: Callable) -> tuple[bool, list[str]]:
"""여러 검증 함수를 순차 적용"""
errors = []
for validator in validators:
result = validator(value)
if result is not None:
errors.append(result)
return len(errors) == 0, errors
# 검증 함수들
def not_empty(s): return "Cannot be empty" if not s else None
def min_length(n): return lambda s: f"Min length is {n}" if len(s) < n else None
def max_length(n): return lambda s: f"Max length is {n}" if len(s) > n else None
# 사용
is_valid, errors = validate(
"hi",
not_empty,
min_length(3),
max_length(10)
)
print(is_valid, errors) # False ['Min length is 3']
커링 (Currying)¶
def curry(func):
"""함수를 커링"""
def curried(*args):
if len(args) >= func.__code__.co_argcount:
return func(*args)
return lambda *more: curried(*args, *more)
return curried
@curry
def add_three(a, b, c):
return a + b + c
# 다양한 호출 방식
print(add_three(1, 2, 3)) # 6
print(add_three(1)(2)(3)) # 6
print(add_three(1, 2)(3)) # 6
print(add_three(1)(2, 3)) # 6
12. 요약¶
| 개념 | 설명 |
|---|---|
| 일급 함수 | 함수를 값처럼 전달/반환 |
| map/filter/reduce | 고차 함수로 컬렉션 변환 |
| lambda | 익명 함수 |
| partial | 부분 적용 |
| lru_cache | 메모이제이션 |
| operator | 연산자 함수화 |
| 순수 함수 | 부작용 없는 함수 |
| 불변성 | 데이터 변경 대신 새로 생성 |
| 파이프라인 | 함수 체이닝 |
13. 연습 문제¶
연습 1: 함수 합성¶
두 함수를 받아 합성된 함수를 반환하는 compose2 함수를 작성하세요.
연습 2: 트랜스듀서¶
map과 filter를 조합하여 한 번의 순회로 처리하는 함수를 작성하세요.
연습 3: 메모이제이션¶
직접 메모이제이션 데코레이터를 구현하세요.
다음 단계¶
10_Performance_Optimization.md에서 파이썬 성능 최적화 기법을 배워봅시다!