09. 함수

🎯 이 장의 목표
  • 함수가 "이름 붙인 코드 묶음"이라는 개념을 잡고 직접 정의한다.
  • 인자(매개변수)로 값을 받고 return으로 결과를 돌려준다.
  • 기본값·키워드 인자·*args·**kwargs로 유연하게 인자를 다룬다.
  • 스코프(변수의 유효 범위)와 람다를 이해한다.

함수: 이름 붙인 코드 묶음

요리를 하다 보면 "양파 볶기"라는 과정을 여러 요리에서 반복합니다. 매번 "기름 두르고, 양파 넣고, 5분 볶고…"를 다시 설명하는 대신 "양파 볶기"라는 이름을 붙여두면, 필요할 때 그 이름만 부르면 됩니다.

함수(function)가 바로 이것입니다. 자주 쓰는 코드 묶음에 이름을 붙여두고, 필요할 때마다 호출(call)합니다.

사실 우리는 이미 함수를 써왔습니다. print(), len(), int(), input() — 모두 누군가 미리 만들어 둔 함수입니다. 이번 장에서는 직접 함수를 만듭니다.

PYTHON
def greet():
    print("안녕하세요!")
    print("함수에 오신 걸 환영합니다")

greet()      # 함수 호출 → 안에 든 코드가 실행됨
greet()      # 또 호출 (재사용!)
# 출력:
# 안녕하세요!
# 함수에 오신 걸 환영합니다
# 안녕하세요!
# 함수에 오신 걸 환영합니다

함수 정의의 구조를 뜯어봅시다.

TEXT
def  greet  (        )  :
─┬─  ──┬──   ─┬─       ┬
 │     │      │        └ 콜론 (이후 들여쓴 블록이 함수 몸체)
 │     │      └ 괄호 (인자가 들어갈 자리, 지금은 비어 있음)
 │     └ 함수 이름 (snake_case)
 └ "함수를 정의한다"는 키워드 def
📌 핵심
핵심: 함수를 정의(def)하는 것만으로는 아무 일도 일어나지 않는다. 호출(이름())해야 비로소 안의 코드가 실행된다. 레시피를 적는 것과 실제로 요리하는 것의 차이다.

함수를 쓰는 이유는 분명합니다.

  • 재사용: 한 번 만들면 몇 번이든 호출
  • 가독성: calculate_tax()라는 이름이 코드의 의도를 설명
  • 수정 용이: 로직이 바뀌어도 함수 한 곳만 고치면 됨

인자: 함수에 값 전달하기

greet()는 항상 똑같이 인사합니다. 이름을 받아 맞춤 인사를 하려면 인자(argument)를 받습니다. 괄호 안에 받을 변수(매개변수)를 적습니다.

PYTHON
def greet(name):
    print(f"안녕하세요, {name}님!")

greet("민지")     # 안녕하세요, 민지님!
greet("현우")     # 안녕하세요, 현우님!

호출할 때 넘기는 값("민지")이 함수 안의 name에 담깁니다. 인자는 여러 개도 됩니다.

PYTHON
def introduce(name, age):
    print(f"{name}님은 {age}살입니다")

introduce("민지", 25)     # 민지님은 25살입니다
💡 팁
용어 정리: 함수를 정의할 때 괄호 안의 변수(name, age)를 매개변수(parameter), 함수를 호출할 때 실제로 넘기는 값("민지", 25)을 인자(argument)라고 구분하기도 합니다. 입문 단계에선 둘을 엄밀히 구분하지 않아도 괜찮습니다.

return: 결과를 돌려받기

print()는 화면에 출력만 할 뿐, 그 값을 다른 곳에 쓸 수는 없습니다. 함수가 계산한 결과를 돌려받아 재사용하려면 return을 씁니다.

PYTHON
def add(a, b):
    return a + b        # 결과를 돌려줌

result = add(3, 5)      # 돌려받은 값을 변수에 담음
print(result)           # 8
print(add(10, 20) * 2)  # 60   ← 돌려받은 값으로 계산도 가능

printreturn의 차이는 입문자가 가장 헷갈리는 부분입니다. 똑똑히 구분합시다.

PYTHON
# print만 하는 함수: 화면에 보여줄 뿐, 값을 가져다 쓸 수 없음
def add_print(a, b):
    print(a + b)

# return 하는 함수: 값을 돌려줘서 재사용 가능
def add_return(a, b):
    return a + b

x = add_print(3, 5)    # 화면엔 8이 찍히지만...
print(x)               # None  ← x에는 아무것도 안 담김!

y = add_return(3, 5)   # 화면엔 아무것도 안 나오지만...
print(y)               # 8     ← y에 결과가 담김!
⚠️ 흔한 실수
흔한 함정: return이 없는 함수는 자동으로 None을 돌려줍니다. "함수를 호출했는데 결과가 None이네?" 싶으면 return을 빠뜨렸는지 확인하세요.
📌 핵심
핵심: print보여주기, return돌려주기. 결과를 나중에 쓰려면 반드시 return. 둘은 완전히 다르다.

return을 만나면 함수는 즉시 종료됩니다. 이를 이용해 조건에 따라 일찍 빠져나올 수 있습니다.

PYTHON
def check_age(age):
    if age < 0:
        return "잘못된 나이"      # 여기서 함수 끝
    if age < 18:
        return "미성년자"
    return "성인"                 # 위 조건이 다 거짓일 때

print(check_age(-5))    # 잘못된 나이
print(check_age(15))    # 미성년자
print(check_age(30))    # 성인
flowchart TD
    Call(["add(3, 5) 호출"]):::user --> Param["a=3, b=5 전달"]:::data
    Param --> Body["a + b 계산"]:::proc
    Body --> Ret["return 8"]:::result
    Ret --> Back(["result = 8 로 돌려받음"]):::user

    classDef user fill:#fff3b0,stroke:#e0a800,color:#5c4500
    classDef data fill:#a8dadc,stroke:#457b9d,color:#1d3557
    classDef proc fill:#7fd8d8,stroke:#2a9d8f,color:#14532d
    classDef result fill:#b8e6c1,stroke:#34a853,color:#14532d

여러 값 돌려주기

return에 쉼표로 여러 값을 적으면 튜플로 묶여 돌아옵니다. 6장의 언패킹으로 받습니다.

PYTHON
def min_max(nums):
    return min(nums), max(nums)     # 두 값을 한 번에

low, high = min_max([3, 1, 4, 1, 5])   # 언패킹으로 받기
print(low, high)        # 1 5

기본값 인자

인자에 기본값을 정해두면, 호출할 때 그 인자를 생략할 수 있습니다.

PYTHON
def greet(name, greeting="안녕"):     # greeting의 기본값은 "안녕"
    return f"{greeting}, {name}님!"

print(greet("민지"))             # 안녕, 민지님!       (기본값 사용)
print(greet("현우", "반가워"))    # 반가워, 현우님!     (기본값 대체)
⚠️ 흔한 실수
흔한 함정: 기본값이 있는 인자는 반드시 없는 인자 뒤에 와야 합니다. def f(greeting="안녕", name)SyntaxError입니다. "기본값 없는 것 먼저, 있는 것 나중에".
⚠️ 흔한 실수
주의 (중급 예고): 기본값으로 리스트 []나 딕셔너리 {} 같은 가변 객체를 쓰면 예상 밖의 버그가 생깁니다(def f(items=[])). 이유와 해결은 중급편에서 다루며, 지금은 "기본값엔 가변 객체를 피한다"만 기억하세요.

키워드 인자

호출할 때 이름=값 형태로 넘기면, 순서와 무관하게 어떤 인자인지 분명해집니다.

PYTHON
def profile(name, age, city):
    return f"{name} / {age}세 / {city}"

# 위치로 전달 (순서 중요)
print(profile("민지", 25, "서울"))

# 키워드로 전달 (순서 무관, 명확함)
print(profile(age=25, city="서울", name="민지"))
# 둘 다: 민지 / 25세 / 서울
💡 팁
인자가 많거나 True/False 같은 값을 넘길 때 키워드 인자를 쓰면 가독성이 좋아집니다. setup(False, True, False)보다 setup(debug=False, verbose=True, cache=False)가 훨씬 읽기 쉽죠.

*args와 **kwargs: 개수를 모를 때

인자가 몇 개 올지 모를 때가 있습니다. 예를 들어 "넘어온 모든 숫자의 합"을 구하는 함수는 인자가 2개일 수도, 10개일 수도 있죠.

*args — 위치 인자를 다 모으기

매개변수 앞에 별 하나(*)를 붙이면, 넘어온 위치 인자들이 튜플로 묶입니다. 이름은 관습적으로 args를 씁니다.

PYTHON
def total(*nums):
    print(nums)             # 넘어온 값들이 튜플로
    return sum(nums)

print(total(1, 2, 3))       # (1, 2, 3) → 6
print(total(10, 20))        # (10, 20) → 30
print(total())              # () → 0

**kwargs — 키워드 인자를 다 모으기

별 두 개(**)를 붙이면, 이름=값 형태의 인자들이 딕셔너리로 묶입니다. 이름은 관습적으로 kwargs(keyword arguments)입니다.

PYTHON
def make_profile(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

make_profile(name="민지", age=25, city="서울")
# name: 민지
# age: 25
# city: 서울
표기모으는 것묶이는 형태
*args위치 인자들튜플
**kwargs키워드 인자들딕셔너리
💡 팁
***는 "묶기"의 반대인 "풀기(언패킹)"에도 쓰입니다(print(*my_list)). 이 심화 내용은 중급편에서 다룹니다. 지금은 "개수가 가변일 때 *args/**kwargs"만 기억하면 충분합니다.

스코프: 변수의 유효 범위

함수 안에서 만든 변수는 그 함수 안에서만 살아 있습니다. 이를 지역 변수(local)라 합니다. 함수 밖에서 만든 변수는 전역 변수(global)입니다.

PYTHON
def my_func():
    inside = "함수 안에서 태어남"
    print(inside)       # OK

my_func()               # 함수 안에서 태어남
print(inside)           # ❌ NameError! 함수 밖에선 inside를 모름
TEXT
NameError: name 'inside' is not defined

전역 변수는 함수 안에서 읽을 수는 있습니다.

PYTHON
greeting = "안녕"        # 전역 변수

def greet(name):
    print(f"{greeting}, {name}님!")   # 전역 greeting을 읽음 (OK)

greet("민지")            # 안녕, 민지님!

하지만 함수 안에서 전역 변수와 같은 이름으로 할당하면, 그것은 전역을 바꾸는 게 아니라 새로운 지역 변수를 만드는 것입니다.

PYTHON
x = "전역"

def change():
    x = "지역"           # 전역 x를 바꾸는 게 아니라, 새 지역 x를 만듦
    print(x)             # 지역

change()                 # 지역
print(x)                 # 전역   ← 바깥 x는 그대로!
flowchart TD
    subgraph Global["전역 영역"]
        G["x = '전역'<br/>greeting = '안녕'"]:::data
    end
    subgraph Local["change() 함수 내부 (지역 영역)"]
        L["x = '지역'<br/>(별개의 변수!)"]:::proc
    end
    Global -.함수는 전역을 읽을 수 있음.-> Local
    Local -.하지만 같은 이름 할당은<br/>전역을 바꾸지 못함.-> Global

    classDef data fill:#a8dadc,stroke:#457b9d,color:#1d3557
    classDef proc fill:#7fd8d8,stroke:#2a9d8f,color:#14532d
📌 핵심
핵심: 함수 안 변수는 밖에서 안 보인다(지역). 전역 변수를 함수에서 읽을 순 있지만, 함수 안의 할당은 그 함수만의 지역 변수를 만들 뿐이다. 이 격리 덕분에 함수끼리 변수 이름이 겹쳐도 안전하다.

람다: 한 줄짜리 익명 함수

아주 짧고 일회성인 함수는 lambda로 이름 없이 만들 수 있습니다.

PYTHON
# 일반 함수
def square(n):
    return n ** 2

# 같은 일을 하는 람다
square = lambda n: n ** 2

print(square(5))     # 25

lambda 인자: 식 형태이고, return 없이 식의 결과가 자동으로 반환됩니다. 람다는 보통 다른 함수의 인자로 잠깐 쓸 때 진가를 발휘합니다. 대표적으로 정렬 기준 지정입니다.

PYTHON
words = ["banana", "apple", "fig"]

# 길이 순으로 정렬 (기준을 람다로 전달)
print(sorted(words, key=lambda w: len(w)))
# ['fig', 'apple', 'banana']

# 숫자를 내림차순으로
nums = [3, 1, 4, 1, 5]
print(sorted(nums, key=lambda n: -n))
# [5, 4, 3, 1, 1]
💡 팁
람다는 "짧은 일회용"에만 쓰세요. 로직이 한 줄을 넘어가면 그냥 def로 이름 있는 함수를 만드는 게 읽기 좋습니다.

나쁜 예 ❌ vs 좋은 예 ✅

반복되는 코드는 함수로 묶는 게 파이썬답습니다.

PYTHON
# ❌ 나쁜 예: 같은 계산을 복사-붙여넣기
price1 = 10000
final1 = price1 - price1 * 0.1
print(f"할인가: {final1}원")

price2 = 20000
final2 = price2 - price2 * 0.1
print(f"할인가: {final2}원")

# ✅ 좋은 예: 함수로 한 번만 정의하고 재사용
def apply_discount(price, rate=0.1):
    return price - price * rate

print(f"할인가: {apply_discount(10000)}원")    # 할인가: 9000.0원
print(f"할인가: {apply_discount(20000)}원")    # 할인가: 18000.0원

함수로 만들면, 할인율 정책이 바뀌어도 함수 한 곳만 고치면 됩니다. 이것이 "반복하지 말라(DRY: Don't Repeat Yourself)"는 프로그래밍의 핵심 원칙입니다.

이 장에서 배운 것

  • 함수def 이름(인자):로 정의하고 이름()으로 호출하는, 이름 붙인 재사용 코드 묶음이다.
  • 인자로 값을 전달하고 return으로 결과를 돌려받는다. return이 없으면 None. print(보여주기)와 return(돌려주기)은 다르다.
  • 기본값 인자(생략 가능), 키워드 인자(이름=값), *args(위치 인자→튜플), **kwargs(키워드 인자→딕셔너리)로 유연하게 받는다.
  • 스코프: 함수 안 변수는 지역(밖에서 안 보임). 전역은 읽기 가능하나, 함수 안 할당은 지역 변수를 새로 만든다.
  • 람다(lambda x: 식)는 짧은 일회용 함수로, 주로 sortedkey 같은 곳에 쓴다.

🧪 실습 문제

문제 1. 두 수를 받아 큰 값을 돌려주는(출력 아님!) 함수 bigger(a, b)를 작성하고, bigger(7, 3)의 결과를 출력하세요. (내장 max를 써도, if로 직접 비교해도 됩니다.)

문제 2. 다음 코드의 출력은? 왜 그런가요?

PYTHON
def double(n):
    n * 2

result = double(5)
print(result)

문제 3. 이름과 인사말을 받아 인사 문자열을 돌려주는 함수를 작성하되, 인사말의 기본값을 "Hello"로 하세요. greet("Mina")greet("Mina", "Hi")를 각각 출력하세요.

문제 4. *args를 받아 그중 최댓값을 돌려주는 함수 my_max(*nums)를 작성하세요. my_max(3, 7, 2, 9, 4)의 결과를 출력하세요.

문제 5. 다음 코드의 출력은?

PYTHON
count = 10
def update():
    count = 99
update()
print(count)

<details>

<summary>✅ 정답·해설 보기</summary>

1.

PYTHON
def bigger(a, b):
    return a if a > b else b      # 또는 return max(a, b)
print(bigger(7, 3))               # 7

2. None. double 함수가 n * 2를 계산하지만 return이 없어서 결과가 버려지고 None이 반환됩니다. return n * 2로 고쳐야 10이 나옵니다. 가장 흔한 실수입니다!

3.

PYTHON
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"
print(greet("Mina"))         # Hello, Mina!
print(greet("Mina", "Hi"))   # Hi, Mina!

4.

PYTHON
def my_max(*nums):
    return max(nums)
print(my_max(3, 7, 2, 9, 4))   # 9

5. 10. 함수 안의 count = 99는 전역 count를 바꾸는 게 아니라 새 지역 변수를 만듭니다. 함수가 끝나면 지역 count는 사라지고, 전역 count10 그대로입니다.

</details>

◀️ 이전 장: 08. 반복문 | ▶️ 다음 장: 10. 예외 처리 입문