PHpullh

PYTHON · 심층 가이드

Python 함수형 완전 정리

functools와 operator, map/filter/reduce, 불변 dataclass, 함수 합성과 커링을 Python의 실제 제약 안에서 언제 쓰고 언제 쓰지 않을지 기준을 세웁니다.

주제 12개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

Python은 함수형 언어가 아닙니다. lambda는 표현식 하나만 담을 수 있고, 꼬리 재귀 최적화도 없으며, reduce는 3.0에서 내장에서 functools로 밀려났습니다. 그럼에도 함수형 스타일이 값을 하는 지점은 분명합니다. 상태를 밖으로 드러내지 않는 순수 함수는 테스트가 쉽고, 부수효과를 경계로 몰아두면 버그의 서식지가 좁아집니다. 목표는 언어를 Haskell처럼 쓰는 게 아니라, 변하는 것과 변하지 않는 것을 코드에서 눈으로 구분되게 만드는 것입니다.

도구부터 익히는 편이 빠릅니다. functools — partial, reduce, cacheoperator 모듈lambda를 걷어내는 실용 도구이고, 커링과 부분 적용은 그 원리를 일반화합니다. map/filter/reduce에서 익힌 변환 사고가 함수 합성으로 이어지면 파이프라인을 조립할 수 있게 됩니다. 데이터 쪽은 불변 데이터 (frozen dataclass)가 축이고, 순수 함수 설계사이드이펙트 관리가 그 원칙을 실제 코드베이스에 적용하는 지침, 모나드 패턴None 처리와 에러 전파를 체이닝으로 접는 응용입니다.

frozen=True가 주는 불변성은 얕다는 점을 기억해야 합니다. 필드 재할당은 막히지만 그 필드가 리스트라면 내용은 얼마든지 바뀝니다. 진짜 불변이 필요하면 튜플이나 frozenset을 담아야 합니다. 캐시 쪽도 함정이 있습니다. functools.cache와 무제한 lru_cache는 항목을 절대 버리지 않으므로 인자 종류가 많은 함수에 걸면 메모리가 계속 늘어납니다. 인자 공간이 넓다면 maxsize를 명시하고, 인자에 가변 객체가 오면 애초에 해시 불가로 실패한다는 점도 미리 알아두면 좋습니다.

01functools — partial, reduce, cache

functools 모듈로 함수형 프로그래밍 패턴을 구현합니다.

Python code

from functools import partial, reduce, lru_cache, cache
from operator import add, mul

# partial — 인자 일부를 고정한 새 함수
def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube   = partial(power, exp=3)
print(square(5))  # 25
print(cube(3))    # 27

# reduce — 누적 연산
numbers = [1, 2, 3, 4, 5]
total   = reduce(add, numbers)         # 15
product = reduce(mul, numbers)         # 120
maximum = reduce(lambda a, b: a if a > b else b, numbers)

# lru_cache — 메모이제이션 (최근 N개 캐시)
@lru_cache(maxsize=128)
def fib(n: int) -> int:
    if n < 2: return n
    return fib(n-1) + fib(n-2)

print(fib(50))               # 빠름
print(fib.cache_info())      # 캐시 통계

# cache (Python 3.9+) — maxsize 제한 없음
@cache
def factorial(n: int) -> int:
    return 1 if n == 0 else n * factorial(n-1)

# map / filter (제너레이터 반환)
doubled = list(map(lambda x: x*2, numbers))
evens   = list(filter(lambda x: x%2==0, numbers))

# 더 Pythonic한 방법
doubled = [x*2 for x in numbers]
evens   = [x for x in numbers if x%2==0]
알아두면 좋은 점

Python에서는 map()/filter()보다 리스트 컴프리헨션이 더 Pythonic하고 읽기 쉽습니다. functoolspartiallru_cache 위주로 활용하세요.

자주 하는 실수

lru_cache는 인자가 hashable해야 합니다. list나 dict를 인자로 받는 함수에는 사용할 수 없습니다.

02Polars 데이터프레임

pandas보다 10~100배 빠른 Polars 기초 사용법

Python code

<span class="cm">// Polars 데이터프레임 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("Polars 데이터프레임") }
알아두면 좋은 점

PYTHON 공식 문서를 함께 참고하세요.

자주 하는 실수

자주 발생하는 실수에 주의하세요.

03map/filter/reduce

함수형 프로그래밍의 기본 도구인 map, filter, reduce를 활용합니다.

Python code

from functools import reduce

# map: 각 요소에 함수 적용
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(f"제곱: {squared}")  # [1, 4, 9, 16, 25]

# filter: 조건에 맞는 요소 선택
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(f"짝수: {evens}")  # [2, 4]

# reduce: 누적 연산
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(f"합계: {total}")  # 15

# 실용적 활용
words = ["hello", "world", "python"]
lengths = list(map(len, words))
print(f"길이: {lengths}")  # [5, 5, 6]

# 체이닝
result = reduce(
    lambda acc, x: acc + x,
    map(lambda x: x ** 2,
        filter(lambda x: x % 2 == 1, range(10)))
)
print(f"홀수 제곱의 합: {result}")  # 165

# 컴프리헨션 대안 (더 Pythonic)
result2 = sum(x**2 for x in range(10) if x % 2 == 1)
print(f"같은 결과: {result2}")  # 165
알아두면 좋은 점

Python에서는 map/filter 대신 컴프리헨션을 사용하는 것이 더 Pythonic합니다.

자주 하는 실수

mapfilter는 이터레이터를 반환합니다. 결과를 여러 번 사용하려면 list()로 변환하세요.

04operator 모듈

operator 모듈로 연산자를 함수로 사용하여 lambda 사용을 줄입니다.

Python code

from operator import (
    add, mul, itemgetter, attrgetter, methodcaller
)
from functools import reduce

# 산술 연산자를 함수로
print(add(3, 4))   # 7
print(mul(3, 4))   # 12
print(reduce(mul, range(1, 6)))  # 120 (5!)

# itemgetter: 인덱스/키로 값 추출
data = [("Alice", 85), ("Bob", 92), ("Carol", 78)]
by_score = sorted(data, key=itemgetter(1), reverse=True)
print(by_score)

# 여러 키 동시 추출
get_name_score = itemgetter(0, 1)
print(get_name_score(("Alice", 85, "A")))  # ('Alice', 85)

# attrgetter: 객체 속성 추출
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

students = [Student("Alice", 3), Student("Bob", 1), Student("Carol", 2)]
by_grade = sorted(students, key=attrgetter("grade"))
for s in by_grade:
    print(f"  {s.name}: {s.grade}학년")

# methodcaller: 메서드 호출
words = ["hello", "WORLD", "Python"]
upper = list(map(methodcaller("upper"), words))
print(upper)  # ['HELLO', 'WORLD', 'PYTHON']
알아두면 좋은 점

operator 함수는 lambda보다 빠르고 pickle 가능합니다.

자주 하는 실수

itemgetter는 키가 존재하지 않으면 KeyError/IndexError를 발생시킵니다. 안전한 접근이 필요하면 lambda를 사용하세요.

05함수형 도구 (toolz)

toolz 라이브러리의 핵심 함수를 순수 Python으로 구현하여 함수형 프로그래밍 도구를 이해합니다.

Python code

from functools import reduce
from itertools import islice

# pipe: 데이터를 함수들에 순서대로 통과
def pipe(data, *funcs):
    return reduce(lambda v, f: f(v), funcs, data)

# curry: 자동 부분 적용
def curry(func):
    import inspect
    n = len(inspect.signature(func).parameters)
    def curried(*args):
        if len(args) >= n:
            return func(*args[:n])
        return lambda *more: curried(*args, *more)
    return curried

# partition: 조건으로 분할
def partition(pred, iterable):
    t, f = [], []
    for item in iterable:
        (t if pred(item) else f).append(item)
    return t, f

# 사용 예시
result = pipe(
    range(20),
    list,
    lambda xs: [x**2 for x in xs],
    lambda xs: [x for x in xs if x > 50],
    sum
)
print(f"50 초과 제곱의 합: {result}")

@curry
def add(a, b, c):
    return a + b + c

add5 = add(5)
add5_3 = add5(3)
print(add5_3(2))  # 10

evens, odds = partition(lambda x: x % 2 == 0, range(10))
print(f"짝수: {evens}, 홀수: {odds}")
알아두면 좋은 점

실제 프로젝트에서는 pip install toolz를 사용하면 최적화된 함수형 도구를 바로 활용할 수 있습니다.

자주 하는 실수

커링된 함수에 인자를 한꺼번에 전달하면 예상과 다른 결과가 나올 수 있습니다. 인자 수를 정확히 파악하세요.

06모나드 패턴

모나드 패턴으로 None 체크, 에러 처리 등을 체이닝 가능한 형태로 추상화합니다.

Python code

from typing import TypeVar, Callable, Optional, Generic

T = TypeVar("T")
U = TypeVar("U")

class Maybe(Generic[T]):
    """None을 안전하게 처리하는 Maybe 모나드"""
    def __init__(self, value: Optional[T]):
        self._value = value

    @classmethod
    def of(cls, value: T) -> "Maybe[T]":
        return cls(value)

    def bind(self, func: Callable[[T], "Maybe[U]"]) -> "Maybe[U]":
        if self._value is None:
            return Maybe(None)
        return func(self._value)

    def map(self, func: Callable[[T], U]) -> "Maybe[U]":
        if self._value is None:
            return Maybe(None)
        return Maybe(func(self._value))

    def or_else(self, default: T) -> T:
        return self._value if self._value is not None else default

    def __repr__(self):
        return f"Maybe({self._value!r})"

# 안전한 딕셔너리 접근
def safe_get(d, key):
    return Maybe(d.get(key))

data = {"user": {"address": {"city": "서울"}}}
result = (
    Maybe.of(data)
    .bind(lambda d: safe_get(d, "user"))
    .bind(lambda d: safe_get(d, "address"))
    .bind(lambda d: safe_get(d, "city"))
    .map(str.upper)
    .or_else("Unknown")
)
print(result)  # 서울 → SEOUL
알아두면 좋은 점

Maybe 모나드는 중첩된 if x is not None: 체크를 우아하게 대체합니다.

자주 하는 실수

모나드 패턴을 남용하면 Python의 관용적 스타일과 맞지 않아 다른 개발자가 이해하기 어려워집니다.

07불변 데이터 (frozen dataclass)

frozen=True 데이터클래스와 함수형 업데이트 패턴으로 불변 데이터를 다룹니다.

Python code

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Point:
    x: float
    y: float

    def translate(self, dx: float, dy: float) -> "Point":
        return replace(self, x=self.x + dx, y=self.y + dy)

    def scale(self, factor: float) -> "Point":
        return replace(self, x=self.x * factor, y=self.y * factor)

# 불변 업데이트 체인
p = Point(1, 2)
p2 = p.translate(3, 4).scale(2)
print(f"원본: {p}")      # Point(x=1, y=2)
print(f"변환: {p2}")     # Point(x=8, y=12)

# 불변 리스트 연산
@dataclass(frozen=True)
class TodoList:
    items: tuple[str, ...]

    def add(self, item: str) -> "TodoList":
        return TodoList(self.items + (item,))

    def remove(self, item: str) -> "TodoList":
        return TodoList(tuple(i for i in self.items if i != item))

todos = TodoList(()).add("공부").add("운동").add("독서")
print(todos)            # TodoList(items=('공부', '운동', '독서'))
print(todos.remove("운동"))  # TodoList(items=('공부', '독서'))
print(todos)            # 원본 변경 없음
알아두면 좋은 점

dataclasses.replace()로 특정 필드만 변경한 새 인스턴스를 만들 수 있습니다.

자주 하는 실수

frozen=True라도 내부에 mutable 객체(리스트)가 있으면 그 내용은 변경 가능합니다. 튜플을 사용하세요.

08패턴 매칭 심화

Python 3.10+의 구조적 패턴 매칭(match/case)의 고급 패턴을 다룹니다.

Python code

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

@dataclass
class Rect:
    origin: Point
    w: float
    h: float

def describe(shape):
    match shape:
        case Circle(center=Point(0, 0), radius=r):
            return f"원점 중심 원 (반지름={r})"
        case Circle(radius=r) if r > 100:
            return f"거대한 원 (반지름={r})"
        case Circle(center=Point(x, y), radius=r):
            return f"원 at ({x},{y}) r={r}"
        case Rect(w=w, h=h) if w == h:
            return f"정사각형 ({w}x{h})"
        case Rect(origin=Point(x, y), w=w, h=h):
            return f"직사각형 at ({x},{y}) {w}x{h}"
        case _:
            return "알 수 없는 도형"

shapes = [
    Circle(Point(0, 0), 5),
    Circle(Point(3, 4), 150),
    Rect(Point(1, 1), 10, 10),
    Rect(Point(0, 0), 5, 3),
]
for s in shapes:
    print(f"  {describe(s)}")
알아두면 좋은 점

패턴 매칭에서 가드 조건(if)을 사용하면 더 세밀한 조건 분기가 가능합니다.

자주 하는 실수

패턴에서 변수명은 항상 새 바인딩을 생성합니다. 기존 변수와 비교하려면 case에 리터럴이나 가드를 사용하세요.

09커링과 부분 적용

커링(currying)으로 다인자 함수를 단일 인자 함수 체인으로 변환합니다.

Python code

from functools import wraps
import inspect

def auto_curry(func):
    """자동 커링 데코레이터"""
    n = len(inspect.signature(func).parameters)
    @wraps(func)
    def curried(*args):
        if len(args) >= n:
            return func(*args[:n])
        def inner(*more):
            return curried(*args, *more)
        return inner
    return curried

@auto_curry
def add(a, b, c):
    return a + b + c

# 다양한 호출 방식
print(add(1, 2, 3))     # 6
print(add(1)(2)(3))     # 6
print(add(1, 2)(3))     # 6

# 부분 적용 활용
add10 = add(10)
add10_20 = add10(20)
print(add10_20(30))      # 60

# 실용적 예시
@auto_curry
def format_log(level, module, message):
    return f"[{level}] {module}: {message}"

error_log = format_log("ERROR")
auth_error = error_log("AUTH")
print(auth_error("인증 실패"))    # [ERROR] AUTH: 인증 실패
print(auth_error("세션 만료"))    # [ERROR] AUTH: 세션 만료

db_info = format_log("INFO", "DB")
print(db_info("연결 성공"))       # [INFO] DB: 연결 성공
알아두면 좋은 점

커링은 로깅, 설정 주입 등 인자의 일부가 미리 결정되는 상황에서 유용합니다.

자주 하는 실수

가변 인자(*args, **kwargs)를 사용하는 함수는 커링이 어렵습니다. 명시적 매개변수를 사용하세요.

10함수 합성

여러 함수를 합성하여 새로운 함수를 만드는 함수형 프로그래밍 기법입니다.

Python code

from functools import reduce
from typing import Callable

def compose(*funcs: Callable) -> Callable:
    """오른쪽에서 왼쪽으로 함수 합성: (f ∘ g)(x) = f(g(x))"""
    def composed(x):
        return reduce(lambda v, f: f(v), reversed(funcs), x)
    return composed

# 개별 함수 정의
double = lambda x: x * 2
increment = lambda x: x + 1
square = lambda x: x ** 2
to_str = lambda x: f"결과: {x}"

# 합성
transform = compose(to_str, square, increment, double)
print(transform(3))  # 결과: 49  (3→6→7→49→"결과: 49")

# 리스트 처리 파이프라인
process_names = compose(
    ", ".join,
    sorted,
    lambda names: [n.title() for n in names],
    lambda names: [n.strip() for n in names],
)
names = ["  alice  ", "charlie", "  bob  "]
print(process_names(names))  # Alice, Bob, Charlie

# compose 데코레이터
def composable(func):
    func.then = lambda other: compose(other, func)
    return func

@composable
def add_one(x): return x + 1

result_func = add_one.then(lambda x: x * 3)
print(result_func(5))  # 18
알아두면 좋은 점

compose는 수학적 합성 순서(오른쪽→왼쪽), pipe는 데이터 흐름 순서(왼쪽→오른쪽)입니다.

자주 하는 실수

합성할 함수의 입출력 타입이 일치하지 않으면 런타임 에러가 발생합니다. 타입 힌트를 활용하세요.

11순수 함수 설계

순수 함수(pure function)의 원칙과 Python에서 실천하는 방법입니다. 동일 입력에 항상 같은 출력을 보장합니다.

Python code

# 비순수 함수 → 순수 함수 변환

# 비순수: 외부 상태에 의존
tax_rate = 0.1
def calc_tax_impure(price):
    return price * tax_rate  # 외부 변수 참조

# 순수: 모든 의존성을 인자로 전달
def calc_tax(price: float, rate: float) -> float:
    return price * rate

print(calc_tax(10000, 0.1))  # 1000.0

# 비순수: 리스트 변경
def add_item_impure(lst, item):
    lst.append(item)  # 원본 변경!
    return lst

# 순수: 새 리스트 반환
def add_item(lst: list, item) -> list:
    return [*lst, item]

original = [1, 2, 3]
new_list = add_item(original, 4)
print(original)   # [1, 2, 3] (변경 없음)
print(new_list)   # [1, 2, 3, 4]

# 순수 함수의 이점: 테스트 용이
def test_calc_tax():
    assert calc_tax(100, 0.1) == 10.0
    assert calc_tax(0, 0.1) == 0.0
    assert calc_tax(100, 0) == 0.0
    print("모든 테스트 통과!")

test_calc_tax()
알아두면 좋은 점

순수 함수는 테스트, 캐싱(memoization), 병렬 실행이 쉽습니다. 가능한 한 순수 함수로 설계하세요.

자주 하는 실수

mutable 기본 인자(def f(lst=[]))는 호출 간 상태를 공유합니다. None을 기본값으로 사용하세요.

12사이드이펙트 관리

함수형 프로그래밍에서 사이드이펙트를 격리하고 관리하는 패턴입니다.

Python code

from typing import Callable, TypeVar
from dataclasses import dataclass

T = TypeVar("T")

@dataclass
class IO:
    """사이드이펙트를 캡슐화하는 IO 모나드"""
    _effect: Callable

    def run(self):
        return self._effect()

    def map(self, func: Callable) -> "IO":
        return IO(lambda: func(self.run()))

    def bind(self, func: Callable) -> "IO":
        return IO(lambda: func(self.run()).run())

# 순수 비즈니스 로직
def format_greeting(name: str) -> str:
    return f"안녕하세요, {name}님!"

def to_upper(text: str) -> str:
    return text.upper()

# IO로 사이드이펙트 래핑
def get_input(prompt: str) -> IO:
    return IO(lambda: prompt)  # 실제: input(prompt)

def put_output(text: str) -> IO:
    return IO(lambda: print(text))

# 사이드이펙트 격리
program = (
    get_input("이름: ")
    .map(format_greeting)
    .map(to_upper)
)

result = program.run()
print(result)  # 안녕하세요, 이름:님! → UPPER

# 실용적 패턴: 의존성 주입
def process_data(data, *, reader=open, writer=print):
    """I/O를 주입받아 테스트 가능하게 만듦"""
    content = f"처리됨: {data}"
    writer(content)
    return content

print(process_data("테스트 데이터"))
알아두면 좋은 점

의존성 주입으로 I/O를 인자로 전달하면 테스트 시 mock으로 쉽게 교체할 수 있습니다.

자주 하는 실수

모든 사이드이펙트를 제거하려 하면 실용적이지 않습니다. 핵심은 순수 로직과 I/O를 분리하는 것입니다.

정리하며

  • lambda x: x[0] 같은 코드는 operator.itemgetter로 바꿔 의도를 드러냅니다.
  • frozen=True는 얕은 불변입니다. 내부 컬렉션까지 얼리려면 튜플·frozenset을 씁니다.
  • 무제한 캐시는 사실상 누수입니다. 인자 공간이 넓으면 maxsize를 반드시 지정합니다.
  • 부수효과는 경계 함수로 몰고, 계산 로직은 인자와 반환값만으로 완결되게 유지합니다.

더 깊이 들어가고 싶다면 Python 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.