문서 읽는 데 49분 · C2

C-2: 함수형 — 함수를 값처럼 다루기

목차 31
전체 22강 중 9강 · 파이썬 기초
난이도 · 입문

ℹ️프로그래밍 경험이 없어도 시작할 수 있는 파이썬 입문 — ‘개발 입문’을 먼저 들으면 더 수월해요. AI·데이터 분석으로 이어지는 출발점이에요.

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 def 로 나만의 함수를 만들어 봤죠. 반복되던 코드에 이름을 붙이고, 매개변수로 값을 받아 return 으로 돌려줬어요. 카테고리 C 의 문을 잘 열었습니다.

그런데 C-1 마지막에 제가 살짝 흘린 말, 기억하시나요? "파이썬에서는 함수도 일종의 값이라, 다른 함수에 넘기거나 변수에 담을 수 있다" 고 했어요. 오늘은 바로 그 감각을 잡습니다.

숫자 8500 을 변수에 담고 함수에 넘기듯, 함수 자체를 변수에 담고 다른 함수에 넘기는 거예요. 이게 되면 "좋아요 많은 순으로 정렬" 같은 일을, 정렬 기준을 함수로 쏙 건네주는 방식으로 아주 우아하게 풀 수 있어요. 오늘 우리가 도착할 지점입니다.

텍스트
   지난 시간 (C-1)                오늘 배울 것 (C-2)
   ──────────────                ──────────────────
   def 로 함수를 만들어     ──>   함수를 "값" 으로 다루기
   호출만 했다                     │
                                  ├─ 람다 — 이름 없는 한 줄 함수
                                  ├─ map / filter — 변환하고 거르기
                                  ├─ sorted(key=) — 좋아요순 정렬!
                                  ├─ 클로저 — 바깥 값을 기억하는 함수
                                  └─ functools — reduce · partial

낯선 단어가 여럿 보여도 괜찮아요. 하나씩, 코드를 직접 실행해 보면서 "아, 이게 이렇게 되는구나" 를 눈으로 확인하며 갈게요. 출발해 볼까요?

💡 오늘 수업의 핵심 — "함수도 값이다. 그래서 넘기고, 받고, 즉석에서 만든다"

🎯 학습 목표

  • 함수를 값처럼 변수에 담고 다른 함수에 넘기는 고차 함수의 감각을 잡아요
  • 람다로 한 줄 함수를 즉석에서 만들고, map·filter·sorted(key=) 에 넘겨 데이터를 변환하고 거르고 정렬해요
  • 클로저functools 로 "함수를 만들어내는 함수" 까지 맛봐요

Step 1: "함수도 값이에요 — 고차 함수"

먼저 가장 중요한 감각부터 잡고 갈게요. 우리가 지금까지 다룬 값은 숫자(8500), 문자열("민지"), 리스트([1, 2, 3]) 같은 것들이었어요. 변수에 담고, 함수에 넘기고, 함수가 돌려주기도 했죠.

그런데 파이썬에서는 함수도 그런 값 중 하나예요. 한번 직접 확인해 봅시다.

Python
# src/c_functions/c2_higher_order.py
def shout(text):
    return text.upper() + "!"


# 함수를 변수에 담아봅시다 (괄호 () 없이 이름만!)
my_func = shout
print(my_func("hello"))        # HELLO!

여기서 핵심은 my_func = shout 한 줄이에요. shout() 처럼 괄호를 붙이면 "함수를 실행" 하는 거지만, 괄호 없이 shout 이름만 쓰면 "함수 그 자체" 를 가리켜요. 그래서 my_funcshout 은 같은 함수를 가리키는 두 이름이 됐어요.

텍스트
   shout  ─────┐
               ├──>  같은 함수를 가리키는 두 이름
   my_func ────┘

   my_func("hello")  ==  shout("hello")  ==  "HELLO!"

우리가 매일 쓰던 print 도 사실 값이라, 변수에 담을 수 있어요.

Python
say = print
say("함수도 값이에요")          # 함수도 값이에요

함수를 인자로 받는 함수

함수가 값이라면, 함수에 다른 함수를 넘길 수도 있겠죠? 이렇게 함수를 인자로 받거나 함수를 돌려주는 함수를 고차 함수(higher-order function) 라고 불러요. "함수를 다루는 함수" 라는 뜻이에요.

Python
# 함수를 "인자로 받는" 함수 = 고차 함수(higher-order function)
def apply_twice(func, value):
    return func(func(value))


print(apply_twice(shout, "hi"))    # HI!!


def add_one(n):
    return n + 1


print(apply_twice(add_one, 10))    # 12

apply_twice 는 받은 함수를 두 번 적용해요. shout 을 넘기면 "hi""HI!""HI!!" 가 되고, add_one 을 넘기면 101112 가 돼요. 똑같은 apply_twice 인데, 어떤 함수를 넘기느냐에 따라 하는 일이 달라지죠.

비유하자면, apply_twice 는 "도구를 두 번 쓰는 일꾼" 이에요. 망치를 쥐어주면 두 번 망치질하고, 붓을 쥐어주면 두 번 칠해요. 일꾼은 그대로인데, 건네주는 도구(함수)가 결과를 바꿔요. 오늘 배울 map·filter·sorted 가 전부 이런 고차 함수예요.

💡 한 줄 정리

함수도 값이라서 변수에 담고 다른 함수에 넘길 수 있어요. 함수를 받거나 돌려주는 함수를 고차 함수라고 불러요.

🙋 학생 질문 — "튜터님, 왜 괄호를 안 붙여요? my_func = shout() 이랑 뭐가 달라요?"

아주 좋은 질문이에요! 이 차이가 오늘의 핵심이라 꼭 짚고 갈게요.

  • shout (괄호 없음) → 함수 그 자체 를 가리켜요. 아직 실행 안 했어요.
  • shout("hi") (괄호 있음) → 함수를 실행한 결과 예요. 여기서는 "HI!" 라는 문자열이죠.

그래서 my_func = shout 는 "함수를 담는" 거고, my_func = shout("hi") 는 "함수를 실행한 결과("HI!")를 담는" 거예요. 고차 함수에 함수를 넘길 땐 괄호 없이 이름만 넘겨야 해요. 괄호를 붙이면 함수가 아니라 그 결과값이 넘어가 버리거든요.


Step 2: "이름 없는 한 줄 함수 — 람다"

add_one 처럼 아주 짧은 함수를 만들 때, def 로 두 줄을 쓰는 게 살짝 번거로울 때가 있어요. 특히 함수를 다른 함수에 잠깐 넘기려고 만들 때는 더 그렇죠. 이럴 때 쓰는 게 람다(lambda) — 이름 없는 한 줄 함수예요.

def 로 만든 함수와 똑같은 걸 람다로 나란히 만들어 비교해 볼게요.

Python
# src/c_functions/c2_lambda.py
# def 로 만든 함수
def add_one(n):
    return n + 1


# 똑같은 걸 lambda 로 (이름 없는 함수를 변수에 담음)
add_one_lambda = lambda n: n + 1

print(add_one(10))          # 11
print(add_one_lambda(10))   # 11

생긴 걸 뜯어볼게요. lambda n: n + 1 에서

  • lambda → "지금부터 한 줄 함수를 만들게요" 신호
  • n → 매개변수 (콜론 앞)
  • n + 1 → 돌려줄 값 (콜론 뒤, return 을 안 써도 이게 반환값)

매개변수가 둘이어도, 아예 없어도 돼요.

Python
# 매개변수 두 개
times = lambda a, b: a * b
print(times(3, 4))          # 12

# 좋아요 수로 라벨 붙이기 (삼항 — 지난 시간 B-3)
label = lambda likes: "인기" if likes >= 1000 else "일반"
print(label(8500))          # 인기
print(label(120))           # 일반

label 안에 지난 시간 B-3 에서 배운 삼항 표현식(A if 조건 else B)이 들어갔죠? 람다 한 줄 안에서도 잘 동작해요.

⚠️ 그런데 한 가지 주의할 게 있어요. 위에서 add_one_lambda = lambda ... 처럼 람다에 이름을 붙여 변수에 담는 건 사실 권장되지 않아요. 이름을 붙일 거라면 그냥 def 로 만드는 게 더 읽기 좋거든요. 람다가 진짜 빛나는 건 다음 Step 부터예요 — map·filter·sorted즉석에서 만들어 바로 넘길 때죠. 여기서는 "def 와 똑같다" 를 보여주려고 일부러 변수에 담아 비교했어요.

💡 한 줄 정리

lambda 매개변수: 돌려줄값 으로 이름 없는 한 줄 함수를 만들어요. def 와 같은 일을 하지만, 다른 함수에 즉석으로 넘길 때 가장 잘 어울려요.

🙋 학생 질문 — "튜터님, 그럼 함수는 무조건 람다로 짧게 쓰는 게 좋은 거예요?"

아니에요, 정반대예요! 람다는 딱 한 줄로 끝나는 아주 단순한 함수일 때만 어울려요.

람다는 return 도 못 쓰고, 여러 줄로 늘릴 수도 없고, 이름이 없어서 나중에 코드를 읽는 사람이 "이게 무슨 함수지?" 하고 멈칫할 수 있어요. 조금이라도 로직이 길어지거나, 여러 곳에서 재사용한다면 def 로 이름을 붙여 만드는 게 훨씬 좋아요.

기억하기 쉽게 정리하면, "한 번 쓰고 버릴 짧은 함수는 람다, 이름 붙여 두고두고 쓸 함수는 def" 예요.


Step 3: "하나씩 변환하기 — map"

이제 람다가 진짜 빛날 차례예요. 리스트의 모든 원소를 똑같은 방식으로 바꾸고 싶을 때가 많죠. 예를 들어 좋아요 수 리스트의 모든 값에 10을 곱한다든지, 단어 리스트를 전부 해시태그로 바꾼다든지요.

이럴 때 map(함수, 리스트) 를 쓰면 각 원소에 함수를 하나씩 적용해 줘요.

Python
# src/c_functions/c2_map.py
likes = [120, 8500, 320, 9200]

# 각 좋아요에 10을 곱하기 (이벤트 보너스!)
bonus = list(map(lambda x: x * 10, likes))
print(bonus)        # [1200, 85000, 3200, 92000]

# 각 단어를 해시태그로
words = ["일상", "여행", "맛집"]
tags = list(map(lambda w: "#" + w, words))
print(tags)         # ['#일상', '#여행', '#맛집']

map(lambda x: x * 10, likes) 는 "likes 의 원소 x 하나하나에 x * 10 을 적용해라" 는 뜻이에요. 람다를 즉석에서 만들어 map 에 바로 넘겼죠? 이게 Step 2 에서 말한 "람다가 빛나는 순간" 이에요.

한 가지 알아둘 점이 있어요. map 의 결과는 바로 리스트가 아니라 게을러서 아직 계산을 미뤄둔 객체예요. 그래서 list() 로 감싸야 실제 결과가 나와요.

Python
# map 은 게으른 객체라 list() 로 감싸야 보여요
lazy = map(lambda x: x * 10, likes)
print(lazy)         # <map object at 0x...> (주소는 매번 달라요)
print(list(lazy))   # [1200, 85000, 3200, 92000]
print(list(lazy))   # []  ← 한 번 꺼내면 비어요 (B-3 제너레이터처럼)

<map object at 0x...> 처럼 정체 모를 게 찍히죠? 이건 "아직 안 꺼낸 상태" 라는 뜻이에요. 그리고 지난 시간 B-3 의 제너레이터 표현식처럼 한 번 꺼내면 비어버려요. 그래서 보통은 만들자마자 list() 로 바로 감싸요.

컴프리헨션과 비교

"어? 이거 지난 시간 컴프리헨션이랑 똑같은 일 아니야?" 라고 생각했다면 정확해요!

Python
# 컴프리헨션으로도 같은 일을 해요 (지난 시간 B-3)
tags_comp = ["#" + w for w in words]
print(tags_comp)    # ['#일상', '#여행', '#맛집']

map 과 컴프리헨션은 같은 일을 해요. 어느 쪽이 더 좋은지는 마지막 생각해볼 주제에서 함께 고민해 봐요.

💡 한 줄 정리

map(함수, 리스트) 는 각 원소에 함수를 하나씩 적용해요. 결과는 게으른 객체라 list() 로 감싸 꺼내요.

🙋 학생 질문 — "튜터님, list() 로 안 감싸면 결과가 안 나오는 게 왜 좋은 거예요? 불편한데요?"

처음엔 불편해 보이죠? 그런데 "미뤄두는 것" 이 장점이 되는 순간이 있어요.

좋아요 수가 100만 개라고 해볼게요. map 이 곧바로 100만 개를 다 계산해서 메모리에 쌓아두면 부담이 크겠죠. 그런데 게으른 객체는 진짜 필요할 때 하나씩 꺼내 계산해요. 그래서 큰 데이터를 다룰 때 메모리를 아낄 수 있어요.

지난 시간 B-3 의 제너레이터 표현식(() 로 감싼 것)도 같은 이유로 게을렀던 거 기억나시죠? 지금은 "작은 데이터는 그냥 list() 로 감싸 쓰면 된다" 정도만 알아두면 충분해요.


Step 4: "조건으로 거르기 — filter"

map 이 "모든 원소를 변환" 하는 거였다면, filter 는 "조건에 맞는 원소만 남기는" 도구예요. filter(함수, 리스트) 에서 함수가 True 를 돌려주는 원소만 통과시켜요.

Python
# src/c_functions/c2_filter.py
likes = [120, 8500, 320, 9200, 80]

# 좋아요 1000 이상만 남기기 (인기 게시물)
popular = list(filter(lambda x: x >= 1000, likes))
print(popular)          # [8500, 9200]

lambda x: x >= 1000 은 좋아요가 1000 이상이면 True, 아니면 False 를 돌려주는 함수예요. filter 는 이 함수가 True 라고 한 원소만 남겨서 [8500, 9200] 만 통과시켰어요.

문자열에도 똑같이 써요. 빈 캡션을 걸러내 볼게요.

Python
# 빈 문자열이 아닌 캡션만 남기기
captions = ["첫 게시물", "", "여행 중", "", "맛집 탐방"]
non_empty = list(filter(lambda c: c != "", captions))
print(non_empty)        # ['첫 게시물', '여행 중', '맛집 탐방']

비어 있던 "" 두 개가 쏙 빠졌죠? 이렇게 filter 는 "체" 같은 역할이에요. 조건이라는 그물을 통과한 것만 아래로 떨어져요.

이것도 컴프리헨션으로 같은 일을 할 수 있어요. 지난 시간에 배운 조건 필터 기억나시죠?

Python
# 컴프리헨션 필터로도 같은 일 (지난 시간 B-3)
popular_comp = [x for x in likes if x >= 1000]
print(popular_comp)     # [8500, 9200]

💡 한 줄 정리

filter(함수, 리스트) 는 함수가 True 를 돌려주는 원소만 남겨요. map 이 변환이라면 filter 는 골라내기예요.

🙋 학생 질문 — "튜터님, map 은 변환이고 filter 는 거르기인데, 헷갈리지 않게 기억하는 방법 없을까요?"

좋은 질문이에요! 둘은 이름은 비슷한데 하는 일이 완전히 달라요. 이렇게 기억해 보세요.

  • map = "지도(map)" 가 아니라 "바꾸기" → 들어온 개수 그대로, 값만 변환해요. [1, 2, 3][10, 20, 30] (개수 3개 그대로)
  • filter = "거르기" → 조건에 맞는 것만 남아서, 개수가 줄어들 수 있어요. [1, 2, 3][2, 3] (조건 통과한 것만)

한마디로 map 은 개수가 그대로, filter 는 개수가 줄어든다 예요. 다음 Step 에서 배울 sorted 와 합치면, "거르고(filter) → 정렬하고(sorted) → 변환하기(map)" 같은 멋진 흐름도 만들 수 있어요.


Step 5: "정렬 기준을 함수로 — sorted(key=)"

드디어 C-1 마지막에 예고했던 그 장면이에요. "좋아요 순으로 게시물을 정렬할 때, 무엇을 기준으로 정렬할지를 함수로 넘겨준다" 고 했죠? 바로 sortedkey 예요.

지난 시간 B-1 에서 리스트.sort() 로 정렬해 봤지만, 그땐 숫자나 문자열 리스트처럼 단순한 경우였어요. 그런데 게시물은 보통 이렇게 생겼죠 — 제목과 좋아요 수가 함께 든 딕셔너리의 리스트요.

Python
# src/c_functions/c2_sorted.py
posts = [
    {"title": "노을", "likes": 120},
    {"title": "여행 사진", "likes": 8500},
    {"title": "맛집", "likes": 320},
    {"title": "오늘의 일상", "likes": 9200},
]

# 좋아요 적은 순 (오름차순 기본)
by_likes = sorted(posts, key=lambda p: p["likes"])
print(by_likes[0]["title"])     # 노을
print(by_likes[-1]["title"])    # 오늘의 일상

여기서 key=lambda p: p["likes"] 가 마법의 한 줄이에요. "각 게시물 p 를 정렬할 때, p["likes"] 값을 기준으로 삼아라" 는 뜻이죠. 정렬 기준을 함수로 넘겨준 거예요!

텍스트
   sorted(posts, key=lambda p: p["likes"], reverse=True)
                     └──────────┬─────────┘  └────┬────┘
                      "무엇을 기준으로 정렬?"      "큰 것부터"
                      각 게시물의 likes 값

이제 좋아요 많은 순으로 뒤집어 볼게요. reverse=True 만 붙이면 돼요. 우리가 그토록 만들고 싶던 "좋아요순 정렬" 이에요.

Python
# 좋아요 많은 순 (reverse=True) — 지난 시간 예고했던 "좋아요순 정렬"!
top = sorted(posts, key=lambda p: p["likes"], reverse=True)
for p in top:
    print(p["title"], p["likes"])
# 오늘의 일상 9200
# 여행 사진 8500
# 맛집 320
# 노을 120

좋아요 9200 짜리 "오늘의 일상" 이 맨 위로 올라왔죠? key 만 바꾸면 다른 기준으로도 정렬할 수 있어요. 제목 길이순으로 해볼게요.

Python
# 제목 길이 순으로도 정렬 가능 (같은 길이는 원래 순서 유지 = 안정 정렬)
by_len = sorted(posts, key=lambda p: len(p["title"]))
print([p["title"] for p in by_len])
# ['노을', '맛집', '여행 사진', '오늘의 일상']

key=lambda p: len(p["title"]) 로 바꾸니 제목이 짧은 순서로 정렬됐어요. "노을" 과 "맛집" 은 둘 다 두 글자라 길이가 같은데, 이럴 땐 원래 리스트에 있던 순서를 그대로 지켜요. 이걸 안정 정렬(stable sort)이라고 불러요.

마지막으로, 정렬까지 안 가고 "가장 좋아요 많은 게시물 하나" 만 필요하다면 max 에도 key 를 넘길 수 있어요.

Python
# max / min 도 key= 를 받아요
most = max(posts, key=lambda p: p["likes"])
print(most["title"])            # 오늘의 일상

💡 한 줄 정리

sorted(리스트, key=함수) 는 "무엇을 기준으로 정렬할지" 를 함수로 정해요. reverse=True 면 큰 것부터, max·minkey 를 받아요.

🙋 학생 질문 — "튜터님, 지난 시간 B-1 에서는 리스트.sort() 였는데 오늘은 sorted(리스트) 네요? 뭐가 달라요?"

날카로운 관찰이에요! 둘 다 정렬인데 미묘하게 달라요.

  • 리스트.sort()원래 리스트 자체를 바꿔요. 돌려주는 값은 없어요(None). 원본이 정렬돼 버려요.
  • sorted(리스트) → 원본은 그대로 두고, 정렬된 새 리스트를 돌려줘요.

그래서 원본을 보존하고 싶을 땐 sorted 가 안전해요. 그리고 오늘처럼 key= 로 기준을 정하는 건 둘 다 똑같이 할 수 있어요(리스트.sort(key=...) 도 돼요). 게시물 원본 리스트를 망가뜨리지 않으려고 오늘은 sorted 를 주로 썼어요.


Step 6: "함수가 기억을 갖는다 — 클로저"

지금까지는 함수를 넘기는 쪽을 봤어요. 이번엔 함수가 다른 함수를 돌려주는 경우예요. 좀 신기한 일이 일어나거든요.

함수 안에서 함수를 만들어 돌려주면, 안쪽 함수는 바깥 함수의 변수를 기억해요. 이렇게 바깥 변수를 기억하는 함수를 클로저(closure) 라고 불러요.

Python
# src/c_functions/c2_closure.py
def make_multiplier(n):
    def multiply(x):
        return x * n      # 바깥의 n 을 기억!
    return multiply


double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))     # 20
print(triple(10))     # 30
print(double(7))      # 14

천천히 따라가 볼게요. make_multiplier(2) 를 부르면, 안에서 multiply 함수를 만들어 돌려줘요. 이때 n2 였죠. 돌려받은 함수를 double 이라고 이름 붙였어요. 나중에 double(10) 을 부르면, multiply 는 자기를 만들 때의 n=2기억하고 있어서 10 * 2 = 20 을 돌려줘요.

triplen=3 을 기억하니 10 * 3 = 30 이고요. 같은 make_multiplier 인데, 넘긴 숫자를 기억하는 서로 다른 함수 두 개가 만들어진 거예요.

텍스트
   make_multiplier(2)  ──>  double  (x * 2 를 기억)
   make_multiplier(3)  ──>  triple  (x * 3 를 기억)

   double(10) ──> 20      triple(10) ──> 30

비유하자면 make_multiplier도장을 찍어내는 기계예요. "2배" 라고 새긴 도장, "3배" 라고 새긴 도장을 각각 찍어내요. 도장마다 자기가 새겨진 숫자를 기억하죠. 인스타 예시로, 좋아요에 가중치를 주는 함수도 이렇게 찍어낼 수 있어요.

Python
# 좋아요에 가중치를 주는 함수를 찍어내는 함수
def make_booster(rate):
    def boost(likes):
        return int(likes * rate)
    return boost


vip_boost = make_booster(1.5)
print(vip_boost(100))     # 150
print(vip_boost(8500))    # 12750

make_booster(1.5) 로 "1.5배 부스터" 함수를 찍어냈어요. vip_boost 는 자기가 기억하는 rate=1.5 로 좋아요를 뻥튀기해 줘요.

💡 한 줄 정리

함수 안에서 함수를 만들어 돌려주면, 안쪽 함수가 바깥 변수를 기억해요. 이걸 클로저라고 불러요. 같은 함수로 기억이 다른 함수들을 찍어낼 수 있어요.

🙋 학생 질문 — "튜터님, doublen=2 를 기억한다는 게 잘 안 와닿아요. make_multiplier 가 끝났으면 n 도 사라지는 거 아니에요?"

정말 좋은 의문이에요! 보통 함수가 끝나면 그 안의 지역 변수는 사라지는 게 맞아요(지난 시간 C-1 의 스코프 기억나시죠?).

그런데 클로저는 특별해요. 안쪽 함수(multiply)가 바깥 변수(n)를 쓰고 있으면, 파이썬이 "이 함수가 아직 n 을 기억해야 하네" 하고 그 값을 함수에 딸려 보내줘요. 그래서 make_multiplier 가 끝나도 double 안에는 n=2 가 살아남아 있어요.

지금은 "안쪽 함수가 바깥 값을 들고 나간다" 정도로만 이해해도 충분해요. 이 원리는 나중에 함수를 꾸며주는 데코레이터를 배울 때 다시 만나니, 그때 더 깊이 들여다볼게요.


Step 7: "functools 맛보기 — reduce·partial"

오늘 배운 도구들과 결이 비슷한, 표준 라이브러리 하나를 가볍게 맛볼게요. functools 는 함수를 다루는 편리한 도구가 든 상자예요. 거기서 reducepartial 두 개만 살짝 꺼내 봅니다.

Python
# src/c_functions/c2_functools.py
# functools 는 함수를 다루는 도구가 든 표준 라이브러리예요.
# (import 는 다음 시간 C-3 에서 자세히 — 지금은 "도구를 꺼내 쓴다" 정도)
from functools import reduce, partial

likes = [120, 8500, 320, 9200]

# reduce(함수, 리스트) = 옆으로 누적해서 하나로 줄이기
total = reduce(lambda acc, x: acc + x, likes)
print(total)        # 18140
print(sum(likes))   # 18140  ← 같은 결과, 사실 sum() 이 더 읽기 쉬워요

reduce 는 리스트를 옆으로 누적해서 하나의 값으로 줄여요. acc(누적값)에 원소를 하나씩 더해가는 거죠. 1208500 더하고, 거기에 320 더하고… 결국 전체 합 18140 이 나와요.

그런데 보세요 — 바로 아래 sum(likes) 가 같은 결과를 훨씬 간단하게 내죠? 그래서 단순 합계라면 reduce 보다 sum 이 더 좋아요. reduce 는 "이런 도구도 있다" 정도로 알아두고, 실제로는 sum·컴프리헨션처럼 더 읽기 쉬운 길을 먼저 떠올려요.

partial 은 함수의 일부 인자를 미리 고정해 새 함수를 만들어요.

Python
# partial = 함수의 일부 인자를 미리 고정한 새 함수
def power(base, exp):
    return base ** exp


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

power(base, exp) 는 거듭제곱 함수예요. partial(power, exp=2) 는 "exp 는 항상 2로 고정한 새 함수" 를 만들어요. 그래서 square(5)power(5, 2) = 25 가 되죠. Step 6 의 클로저로 만들었던 "기억하는 함수" 와 비슷한 느낌이 들지 않나요?

⚠️ 방금 from functools import ... 로 도구를 가져왔는데, 이 import 가 무엇이고 어떻게 동작하는지는 다음 시간(C-3) 에서 제대로 배워요. 지금은 "표준 라이브러리에서 도구를 꺼내 쓴다" 정도로만 봐 두세요.

💡 한 줄 정리

functoolsreduce 는 리스트를 하나로 누적하고(단순 합은 sum 이 더 명확), partial 은 인자를 미리 고정한 새 함수를 만들어요.

🙋 학생 질문 — "튜터님, reducesum 보다 안 좋으면 왜 배워요?"

하하, 솔직한 질문 좋아요! 두 가지 이유가 있어요.

첫째, reduce단순 합 말고도 쓸 수 있어요. 곱하기로 누적하거나, 가장 큰 값을 누적으로 찾는 것처럼 sum 으로는 안 되는 누적도 표현할 수 있죠. "누적해서 하나로 줄인다" 는 개념 자체가 중요해요.

둘째, 다른 사람의 코드나 다른 언어(특히 함수형 언어)에서 reduce(또는 fold)를 자주 만나요. 그때 "아, 이게 누적해서 줄이는 거구나" 하고 알아볼 수 있어야 하니까요.

다만 파이썬에서는 합계는 sum, 최댓값은 max 처럼 더 읽기 쉬운 도구가 있으면 그걸 먼저 쓰는 게 좋아요. 그래서 reduce 는 "개념은 알되, 남용하지 않는다" 가 정답이에요.


Step 8: "캡스톤 — 좋아요순 피드 + 캡션 변환"

이제 오늘 배운 걸 한자리에 모아 볼게요. 인스타 피드를 만든다고 생각해 봅시다. 게시물 중에서 인기 게시물만 골라서(filter) → 좋아요 많은 순으로 정렬하고(sorted) → 보기 좋은 한 줄 카드로 바꾸기(map). 오늘의 세 도구가 흐름으로 이어져요.

Python
# src/c_functions/c2_caption.py
posts = [
    {"title": "노을", "likes": 120},
    {"title": "여행 사진", "likes": 8500},
    {"title": "맛집", "likes": 320},
    {"title": "오늘의 일상", "likes": 9200},
]

# 1) 좋아요 1000 이상만 (filter)
popular = list(filter(lambda p: p["likes"] >= 1000, posts))

# 2) 좋아요 많은 순 정렬 (sorted + key)
ranked = sorted(popular, key=lambda p: p["likes"], reverse=True)

# 3) 한 줄 카드로 변환 (map)
cards = list(map(lambda p: f'{p["title"]} (좋아요 {p["likes"]})', ranked))

for card in cards:
    print(card)
# 오늘의 일상 (좋아요 9200)
# 여행 사진 (좋아요 8500)

세 단계가 데이터를 차례로 흘려보내며 다듬죠.

텍스트
   [게시물 4개] ──filter(1000)──> [인기글 2개] ──sorted(좋아요)──> [정렬됨] ──map──> [카드 2개]

좋아요 1000 미만인 "노을"(120)과 "맛집"(320)은 filter 에서 빠지고, 남은 둘이 좋아요순으로 정렬된 뒤, map 으로 "오늘의 일상 (좋아요 9200)" 같은 문자열이 됐어요. 이 흐름은 코드베이스 test_c2_functional.py 에서도 똑같은 결과가 나오도록 검증해 뒀어요.

각 단계가 작은 함수(람다) 하나씩으로 표현되니, 무슨 일을 하는지 한눈에 읽히죠? 이게 "함수를 값처럼 넘긴다" 는 오늘 감각의 결실이에요.

💡 한 줄 정리

filter(고르기) → sorted(정렬) → map(변환)을 이어 붙이면, 데이터를 단계별로 다듬는 깔끔한 흐름이 돼요. 각 단계의 기준은 람다로 넘겨요.

🙋 학생 질문 — "튜터님, 이걸 컴프리헨션이랑 for 문으로도 만들 수 있는데, 어떤 게 더 좋아요?"

맞아요, 똑같은 걸 컴프리헨션과 반복문으로도 만들 수 있어요. 정답은 "상황에 따라 다르다" 예요.

filter/map 을 쓰면 "거른다", "변환한다" 는 의도가 단어로 드러나는 장점이 있어요. 반면 파이썬에서는 컴프리헨션이 더 익숙하고 읽기 쉽다고 느끼는 사람도 많아요.

오늘은 "함수를 값으로 넘긴다" 는 감각을 익히는 게 목표라 filter/map/sorted 로 써봤어요. 실무에서는 둘 다 보이니, 양쪽을 다 읽을 줄 아는 게 중요해요. 어느 쪽이 더 파이썬다운지는 바로 아래 생각해볼 주제에서 더 고민해 봅시다!


마무리

오늘은 "함수도 값이다" 라는 감각을 잡고, 그걸 도구로 휘둘러 봤어요. 함수를 변수에 담고 다른 함수에 넘기는 고차 함수에서 시작해, 람다로 한 줄 함수를 즉석에서 만들고, map·filter·sorted(key=) 로 데이터를 변환하고 거르고 정렬했죠. 클로저로 "기억하는 함수" 를, functools 로 reduce·partial 까지 맛봤어요. 정말 잘 따라오셨습니다.

오늘 배운 핵심 세 가지

💡 하나: 파이썬에서 함수는 이에요. 변수에 담고(my_func = shout), 다른 함수에 넘길 수 있어요. 함수를 받거나 돌려주는 함수를 고차 함수라고 불러요.

💡 : 람다는 이름 없는 한 줄 함수예요. map(변환)·filter(거르기)·sorted(key=)(정렬 기준) 에 즉석으로 넘길 때 가장 빛나요. 특히 sorted(key=lambda p: p["likes"], reverse=True) 로 좋아요순 정렬을 완성했죠.

💡 : 클로저는 바깥 변수를 기억하는 함수예요. functoolsreduce(누적)·partial(인자 고정)도 함수를 다루는 도구인데, 단순 합계는 sum 처럼 더 읽기 쉬운 길을 먼저 떠올려요.

다음 시간 예고

오늘 functools 를 잠깐 가져올 때 from functools import ... 라고 썼던 거, 기억하시죠? 다음 시간엔 바로 그 import 를 제대로 배워요. 지금까지 우리는 코드를 파일 하나에 다 몰아 썼는데, 코드가 커지면 여러 파일(모듈)로 나눠 관리하는 게 좋아요.

다음 시간(C-3)엔 importfrom ... import 로 코드를 모듈로 나누고, 파이썬이 기본 제공하는 표준 라이브러리를 둘러보고, 여러 파일을 패키지로 묶는 법을 배웁니다. "잘 나누고 가져다 쓰는" 정리정돈의 기술이에요. 기대해 주세요!


과제

오늘 배운 함수형 도구를 직접 손으로 써봅시다. 눈으로 읽는 것과 직접 짜보는 건 완전히 달라요. 막히면 Step 으로 돌아가 코드를 다시 보세요.

[기초] 캡션 다듬기 — map 과 람다

캡션 리스트를 map 과 람다로 다듬어 보세요.

  • captions = [" 첫 게시물 ", "여행 중", " 맛집 탐방"] 를 준비합니다
  • map 과 람다로 각 캡션의 앞뒤 공백을 제거한 새 리스트를 만듭니다 (strip 활용)
  • 각 캡션 뒤에 " #인스타" 를 붙인 리스트도 만들어 봅니다
  • 다듬은 각 캡션의 글자 수(len)를 담은 리스트도 만들어 출력합니다
  • (생각해보기) 같은 일을 컴프리헨션으로 만들면 어떻게 될까요? 직접 바꿔보고 어느 쪽이 더 읽기 쉬운지 느껴 보세요

[응용] 인기 게시물 순위 만들기 — filter 와 sorted(key=)

게시물 리스트에서 인기 게시물만 골라 순위를 매겨 보세요.

  • 제목과 좋아요 수가 담긴 게시물 딕셔너리 리스트를 5개 이상 준비합니다
  • filter 로 좋아요 1000 이상인 게시물만 남깁니다
  • sorted(key=..., reverse=True) 로 좋아요 많은 순으로 정렬합니다
  • 정렬된 순서대로 "1위: 제목 (좋아요 N)" 형식으로 출력합니다 (enumerate 활용 — 지난 시간 B-3)
  • (도전) maxkey= 를 넘겨 "가장 인기 있는 게시물 하나" 만 따로 뽑아 출력해 보세요

[심화] 나만의 포매터 찍어내기 — 클로저

클로저로 "접두사를 기억하는 포매터" 를 만들어 보세요.

  • make_formatter(prefix) 함수를 만듭니다. 이 함수는 안에서 다른 함수를 만들어 돌려줍니다
  • 돌려주는 함수는 받은 텍스트 앞에 prefix 를 붙여 돌려줍니다
  • make_formatter("#") 로 해시태그 포매터를, make_formatter("@") 로 멘션 포매터를 만들어 각각 써봅니다
  • (도전) 인사말을 기억하는 make_greeter(greeting) 도 만들어, make_greeter("안녕하세요")"안녕하세요, @민지님!" 같은 문장을 돌려주게 해보세요

생각해볼 주제

정답이 하나로 정해진 질문이 아니에요. 혼자 곰곰이 생각해 보거나, 함께 공부하는 동료와 이야기 나눠 보면 좋습니다.

1. 람다와 def, 언제 무엇을 쓸까?

오늘 우리는 람다로 한 줄 함수를 만들어 map·filter·sorted 에 넘겼어요. 그런데 람다에 이름을 붙여 변수에 담는 건 권장되지 않는다고도 했죠.

람다가 def 보다 나은 상황은 언제일까요? 반대로 람다로 쓰면 오히려 코드가 읽기 어려워지는 경우는 어떤 때일까요? "한 번 쓰고 버릴 짧은 함수" 와 "이름 붙여 재사용할 함수" 의 경계를 어디에 둘지 생각해 보세요.

2. map·filter vs 컴프리헨션, 무엇이 더 파이썬다울까?

Step 3·4 에서 map·filter 가 컴프리헨션과 똑같은 일을 한다는 걸 봤어요. 둘은 결과가 같은데, 생긴 모습이 다르죠.

어떤 사람은 map(lambda x: x*10, likes) 가 "변환한다" 는 의도를 분명히 보여 좋다고 하고, 어떤 사람은 [x*10 for x in likes] 가 더 읽기 쉽다고 해요. 여러분은 어느 쪽이 더 끌리나요? 그리고 그 선택의 기준은 무엇일까요?

3. reduce 는 왜 파이썬에서 덜 권장될까?

Step 7 에서 reduce 로 합계를 구했지만, 바로 옆에서 sum 이 같은 일을 더 간단히 했어요. 파이썬을 만든 사람들도 reduce 를 표준에서 한 발 물러난 자리(functools)로 옮겨뒀죠.

그런데도 reduce 라는 개념 자체는 여러 언어에 두루 있어요. 왜 파이썬은 sum·max·컴프리헨션 같은 길을 더 권할까요? "누적해서 하나로 줄인다" 는 개념이 여전히 중요한 이유는 무엇일까요? 읽기 쉬움과 표현력 사이의 균형을 생각해 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 먼저 스스로 충분히 손으로 짜본 뒤에 펼쳐 보세요. 정답을 외우는 게 아니라, "내가 짠 것과 어디가 다른지" 를 비교하는 게 진짜 공부예요.


🎯 [과제 1 예시답안] 캡션 다듬기 — map 과 람다

채점 포인트

항목 확인 핵심
map + 람다 list(map(lambda c: c.strip(), captions)) 각 원소에 함수 하나씩 적용
문자열 메서드 c.strip() 앞뒤 공백 제거 (지난 카테고리)
문자열 합치기 c.strip() + " #인스타" 다듬은 뒤 붙이기
글자 수 len(c.strip()) 다듬은 결과의 길이
list() 로 꺼내기 list(map(...)) 게으른 객체를 리스트로

풀이 예시

Python
# src/c_functions/c2_caption_transform_solution.py
# [기초] map 과 람다로 캡션 다듬기

captions = ["  첫 게시물  ", "여행 중", "  맛집 탐방"]

# 1) 앞뒤 공백 제거 (strip) — map + 람다
trimmed = list(map(lambda c: c.strip(), captions))
print(trimmed)

# 2) 각 캡션 뒤에 해시태그 붙이기
tagged = list(map(lambda c: c.strip() + " #인스타", captions))
print(tagged)

# 3) 다듬은 캡션의 글자 수 세기
lengths = list(map(lambda c: len(c.strip()), captions))
print(lengths)

실행 결과예요.

텍스트
['첫 게시물', '여행 중', '맛집 탐방']
['첫 게시물 #인스타', '여행 중 #인스타', '맛집 탐방 #인스타']
[5, 4, 5]

💡 튜터의 한마디: 세 줄 다 map(람다, captions) 라는 같은 골격이에요. 바꾸는 내용(공백 제거, 해시태그 붙이기, 길이 세기)만 람다로 갈아 끼웠죠. 마지막 생각거리, 컴프리헨션으로 바꿔 보셨나요? [c.strip() for c in captions] 가 되는데, 둘 중 어느 게 더 읽기 쉬운지는 사람마다 달라요. 양쪽 다 써보고 손에 익혀 두면 어떤 코드를 만나도 읽을 수 있어요.

🎯 [과제 2 예시답안] 인기 게시물 순위 만들기 — filter 와 sorted(key=)

채점 포인트

항목 확인 핵심
filter filter(lambda p: p["likes"] >= 1000, posts) 조건에 맞는 게시물만 남기기
sorted + key sorted(..., key=lambda p: p["likes"], reverse=True) 좋아요순 정렬
enumerate for rank, p in enumerate(ranked, start=1) 순위 번호 붙이기 (지난 카테고리)
f-string f'{rank}위: {p["title"]} ...' 보기 좋은 한 줄
max + key max(posts, key=lambda p: p["likes"]) 최고 하나만 뽑기

풀이 예시

Python
# src/c_functions/c2_top_posts_solution.py
# [응용] filter 와 sorted(key=) 로 인기 게시물 순위 만들기

posts = [
    {"title": "노을", "likes": 120},
    {"title": "여행 사진", "likes": 8500},
    {"title": "맛집", "likes": 320},
    {"title": "오늘의 일상", "likes": 9200},
    {"title": "카페 투어", "likes": 1500},
]

# 1) 좋아요 1000 이상만 남기기
popular = list(filter(lambda p: p["likes"] >= 1000, posts))

# 2) 좋아요 많은 순으로 정렬
ranked = sorted(popular, key=lambda p: p["likes"], reverse=True)

# 3) 순위 매겨 출력 (enumerate — 지난 시간 B-3)
for rank, p in enumerate(ranked, start=1):
    print(f'{rank}위: {p["title"]} ({p["likes"]} 좋아요)')

# 가장 인기 있는 게시물 하나만 (max + key)
best = max(posts, key=lambda p: p["likes"])
print("최고 인기:", best["title"])

실행 결과예요.

텍스트
1위: 오늘의 일상 (9200 좋아요)
2위: 여행 사진 (8500 좋아요)
3위: 카페 투어 (1500 좋아요)
최고 인기: 오늘의 일상

💡 튜터의 한마디: 좋아요 1000 미만인 "노을"(120)과 "맛집"(320)은 filter 에서 빠지고, 남은 셋이 좋아요순으로 정렬됐어요. 핵심은 sorted 에 넘긴 key=lambda p: p["likes"] — "무엇을 기준으로 정렬할지" 를 함수로 넘긴 거죠. 도전 과제의 max(posts, key=...) 는 정렬까지 안 가고 최고 하나만 필요할 때 더 빠르고 간단해요. "전체 순위가 필요하면 sorted, 하나만 필요하면 max" 로 기억해 두세요.

🎯 [과제 3 예시답안] 나만의 포매터 찍어내기 — 클로저

채점 포인트

항목 확인 핵심
함수 안의 함수 def fmt(text): ...make_formatter 안에 안쪽 함수 정의
함수 돌려주기 return fmt 만든 함수를 그대로 반환
바깥 변수 기억 return prefix + text prefix 를 기억하는 클로저
여러 개 찍어내기 make_formatter("#") / ("@") 기억이 다른 함수 두 개
인사말 클로저 make_greeter("안녕하세요") greeting 을 기억

풀이 예시

Python
# src/c_functions/c2_make_formatter_solution.py
# [심화] 클로저로 "나만의 포매터" 찍어내기

def make_formatter(prefix):
    def fmt(text):
        return prefix + text
    return fmt


hashtag = make_formatter("#")
mention = make_formatter("@")

print(hashtag("일상"))
print(mention("민지"))


# 인사말 포매터 (바깥 인자를 기억하는 클로저)
def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, @{name}님!"
    return greet


hello = make_greeter("안녕하세요")
welcome = make_greeter("환영해요")

print(hello("재훈"))
print(welcome("수진"))

실행 결과예요.

텍스트
#일상
@민지
안녕하세요, @재훈님!
환영해요, @수진님!

💡 튜터의 한마디: make_formatter("#") 로 만든 hashtag 는 자기 안에 prefix="#" 를 기억하고 있어요. 그래서 hashtag("일상") 만 불러도 "#일상" 이 나오죠. 같은 make_formatter"#" 도장과 "@" 도장을 따로 찍어낸 거예요. make_greeter 도 똑같이 인사말을 기억해요. "함수가 만들어질 때의 값을 기억한다" 는 클로저의 감각을 잡았다면, 나중에 데코레이터를 배울 때 훨씬 수월할 거예요.


🤔 [생각해볼 주제 1] 람다와 def, 언제 무엇을 쓸까?

문제 상황 요약

람다로 한 줄 함수를 만들어 map·filter·sorted 에 넘겨봤지만, 람다에 이름을 붙여 변수에 담는 건 권장되지 않는다고 배웠어요. 그럼 람다와 def 의 경계는 어디일까요?

튜터의 가이드 및 해설

정답을 가르는 기준은 "이 함수에 이름이 필요한가, 그리고 한 줄로 충분한가" 예요.

람다가 어울리는 자리는 함수를 다른 함수에 즉석으로 넘길 때예요. sorted(posts, key=lambda p: p["likes"]) 처럼요. 정렬 기준은 딱 한 번 쓰고 버릴 짧은 표현이라, 굳이 위에 def get_likes(p): return p["likes"] 를 따로 만들어 이름 붙이는 게 오히려 번거롭죠.

def 가 어울리는 자리는 그 반대예요.

  • 로직이 한 줄로 안 끝나고 여러 줄이 필요할 때
  • 같은 함수를 여러 곳에서 재사용할 때
  • 함수 이름만으로 "이게 무슨 일을 하는지" 설명되면 좋을 때

특히 add_one = lambda n: n + 1 처럼 람다에 이름을 붙이는 건 피하는 게 좋아요. 이름을 붙일 거면 def add_one(n): 가 더 읽기 좋고, 오류가 났을 때 메시지도 더 친절하거든요. 한마디로 "넘길 거면 람다, 이름 붙일 거면 def" 예요.

💡 핵심을 한마디로

"람다는 map·filter·sorted 에 즉석으로 넘기는 한 줄 함수일 때 빛나요. 이름을 붙이거나 여러 줄이 필요하면 def 가 정답이에요. 람다에 이름을 붙이고 있다면, 그건 def 로 써야 한다는 신호예요."

🤔 [생각해볼 주제 2] map·filter vs 컴프리헨션, 무엇이 더 파이썬다울까?

문제 상황 요약

map(lambda x: x*10, likes)[x*10 for x in likes] 는 결과가 똑같아요. 둘은 생긴 모습이 다른데, 파이썬에서는 어느 쪽이 더 권장될까요?

튜터의 가이드 및 해설

파이썬 커뮤니티에서는 대체로 컴프리헨션을 더 선호하는 편이에요. 이유가 있어요.

  • [x*10 for x in likes]list() 로 감싸지 않아도 바로 리스트가 나와요. list(map(...)) 는 한 번 더 감싸야 하죠.
  • 조건까지 붙이면 컴프리헨션이 더 자연스러워요. [x for x in likes if x >= 1000] 한 줄이, list(filter(lambda x: x >= 1000, likes)) 보다 짧고 읽기 쉽다고 느끼는 사람이 많아요.

그럼 map·filter 는 쓸모가 없을까요? 그렇지 않아요.

  • 이미 이름이 있는 함수를 적용할 땐 map 이 깔끔해요. list(map(str, numbers)) 처럼요. 이걸 컴프리헨션으로 쓰면 [str(x) for x in numbers]x 가 한 번 더 등장하죠.
  • 다른 사람의 코드, 특히 함수형 스타일을 좋아하는 코드에서 map·filter 를 자주 만나요. 그래서 읽을 줄은 반드시 알아야 해요.

결론은 "둘 다 알되, 새로 짤 땐 컴프리헨션을 먼저 떠올린다" 가 무난해요. 다만 정답이 하나는 아니니, 팀의 코드 스타일을 따르는 것도 중요해요.

💡 핵심을 한마디로

"파이썬에서는 보통 컴프리헨션이 더 환영받아요 — 특히 조건 필터가 붙을 때요. 하지만 이미 이름 있는 함수를 적용할 땐 map 이 더 깔끔하고, 남의 코드엔 둘 다 나오니 읽을 줄은 꼭 알아야 해요."

🤔 [생각해볼 주제 3] reduce 는 왜 파이썬에서 덜 권장될까?

문제 상황 요약

reduce(lambda acc, x: acc + x, likes) 로 합계를 구했지만, 바로 옆 sum(likes) 가 같은 일을 더 간단히 했어요. 파이썬은 왜 reduce 를 한 발 물러난 자리로 옮겨뒀을까요?

튜터의 가이드 및 해설

핵심은 "읽는 사람이 무엇을 하는지 한눈에 아는가" 예요.

sum(likes) 는 이름만 봐도 "합계구나" 하고 바로 알아요. 그런데 reduce(lambda acc, x: acc + x, likes) 는 람다를 한 박자 읽고 "아, accx 를 계속 더하니 합계네" 하고 해석해야 하죠. 같은 결과인데 읽는 데 품이 더 들어요.

파이썬은 "읽기 쉬운 코드" 를 아주 중요하게 여기는 언어예요. 그래서 자주 쓰는 누적 작업에는 더 분명한 전용 도구를 줬어요.

  • 합계는 sum
  • 최댓값·최솟값은 max·min
  • 거르고 변환은 컴프리헨션

그럼 reduce 는 쓸모가 없을까요? 아니에요. sum·max 로 표현이 안 되는 누적도 있어요. 예를 들어 리스트의 모든 수를 곱하거나, 더 복잡한 규칙으로 한 값으로 줄일 때는 reduce 가 답이 되기도 해요. 그리고 "누적해서 하나로 줄인다(fold)" 는 개념은 다른 언어에도 두루 있어서, 알아두면 시야가 넓어져요.

정리하면, 개념은 알되 남용하지 않는다 예요. 더 읽기 쉬운 길이 있으면 그걸 먼저 쓰고, reduce 는 정말 그게 필요한 자리에서만 꺼내요.

💡 핵심을 한마디로

"reduce 가 한 발 물러난 건 표현력이 부족해서가 아니라, sum·max·컴프리헨션이 같은 일을 더 분명하게 보여주기 때문이에요. 누적이라는 개념은 중요하니 알아두되, 합계엔 sum 처럼 읽기 쉬운 도구를 먼저 떠올리는 게 파이썬다운 선택이에요."

전체 목록 파이썬 기초