09. 함수
- 함수가 "이름 붙인 코드 묶음"이라는 개념을 잡고 직접 정의한다.
- 인자(매개변수)로 값을 받고
return으로 결과를 돌려준다. - 기본값·키워드 인자·
*args·**kwargs로 유연하게 인자를 다룬다. - 스코프(변수의 유효 범위)와 람다를 이해한다.
함수: 이름 붙인 코드 묶음
요리를 하다 보면 "양파 볶기"라는 과정을 여러 요리에서 반복합니다. 매번 "기름 두르고, 양파 넣고, 5분 볶고…"를 다시 설명하는 대신 "양파 볶기"라는 이름을 붙여두면, 필요할 때 그 이름만 부르면 됩니다.
함수(function)가 바로 이것입니다. 자주 쓰는 코드 묶음에 이름을 붙여두고, 필요할 때마다 호출(call)합니다.
사실 우리는 이미 함수를 써왔습니다. print(), len(), int(), input() — 모두 누군가 미리 만들어 둔 함수입니다. 이번 장에서는 직접 함수를 만듭니다.
def greet(): print("안녕하세요!") print("함수에 오신 걸 환영합니다") greet() # 함수 호출 → 안에 든 코드가 실행됨 greet() # 또 호출 (재사용!) # 출력: # 안녕하세요! # 함수에 오신 걸 환영합니다 # 안녕하세요! # 함수에 오신 걸 환영합니다
함수 정의의 구조를 뜯어봅시다.
def greet ( ) : ─┬─ ──┬── ─┬─ ┬ │ │ │ └ 콜론 (이후 들여쓴 블록이 함수 몸체) │ │ └ 괄호 (인자가 들어갈 자리, 지금은 비어 있음) │ └ 함수 이름 (snake_case) └ "함수를 정의한다"는 키워드 def
def)하는 것만으로는 아무 일도 일어나지 않는다. 호출(이름())해야 비로소 안의 코드가 실행된다. 레시피를 적는 것과 실제로 요리하는 것의 차이다.함수를 쓰는 이유는 분명합니다.
- 재사용: 한 번 만들면 몇 번이든 호출
- 가독성:
calculate_tax()라는 이름이 코드의 의도를 설명 - 수정 용이: 로직이 바뀌어도 함수 한 곳만 고치면 됨
인자: 함수에 값 전달하기
위 greet()는 항상 똑같이 인사합니다. 이름을 받아 맞춤 인사를 하려면 인자(argument)를 받습니다. 괄호 안에 받을 변수(매개변수)를 적습니다.
def greet(name): print(f"안녕하세요, {name}님!") greet("민지") # 안녕하세요, 민지님! greet("현우") # 안녕하세요, 현우님!
호출할 때 넘기는 값("민지")이 함수 안의 name에 담깁니다. 인자는 여러 개도 됩니다.
def introduce(name, age): print(f"{name}님은 {age}살입니다") introduce("민지", 25) # 민지님은 25살입니다
name, age)를 매개변수(parameter), 함수를 호출할 때 실제로 넘기는 값("민지", 25)을 인자(argument)라고 구분하기도 합니다. 입문 단계에선 둘을 엄밀히 구분하지 않아도 괜찮습니다.return: 결과를 돌려받기
print()는 화면에 출력만 할 뿐, 그 값을 다른 곳에 쓸 수는 없습니다. 함수가 계산한 결과를 돌려받아 재사용하려면 return을 씁니다.
def add(a, b): return a + b # 결과를 돌려줌 result = add(3, 5) # 돌려받은 값을 변수에 담음 print(result) # 8 print(add(10, 20) * 2) # 60 ← 돌려받은 값으로 계산도 가능
print와 return의 차이는 입문자가 가장 헷갈리는 부분입니다. 똑똑히 구분합시다.
# 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을 만나면 함수는 즉시 종료됩니다. 이를 이용해 조건에 따라 일찍 빠져나올 수 있습니다.
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장의 언패킹으로 받습니다.
def min_max(nums): return min(nums), max(nums) # 두 값을 한 번에 low, high = min_max([3, 1, 4, 1, 5]) # 언패킹으로 받기 print(low, high) # 1 5
기본값 인자
인자에 기본값을 정해두면, 호출할 때 그 인자를 생략할 수 있습니다.
def greet(name, greeting="안녕"): # greeting의 기본값은 "안녕" return f"{greeting}, {name}님!" print(greet("민지")) # 안녕, 민지님! (기본값 사용) print(greet("현우", "반가워")) # 반가워, 현우님! (기본값 대체)
def f(greeting="안녕", name)은 SyntaxError입니다. "기본값 없는 것 먼저, 있는 것 나중에".[]나 딕셔너리 {} 같은 가변 객체를 쓰면 예상 밖의 버그가 생깁니다(def f(items=[])). 이유와 해결은 중급편에서 다루며, 지금은 "기본값엔 가변 객체를 피한다"만 기억하세요.키워드 인자
호출할 때 이름=값 형태로 넘기면, 순서와 무관하게 어떤 인자인지 분명해집니다.
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를 씁니다.
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)입니다.
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)입니다.
def my_func(): inside = "함수 안에서 태어남" print(inside) # OK my_func() # 함수 안에서 태어남 print(inside) # ❌ NameError! 함수 밖에선 inside를 모름
NameError: name 'inside' is not defined
전역 변수는 함수 안에서 읽을 수는 있습니다.
greeting = "안녕" # 전역 변수 def greet(name): print(f"{greeting}, {name}님!") # 전역 greeting을 읽음 (OK) greet("민지") # 안녕, 민지님!
하지만 함수 안에서 전역 변수와 같은 이름으로 할당하면, 그것은 전역을 바꾸는 게 아니라 새로운 지역 변수를 만드는 것입니다.
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로 이름 없이 만들 수 있습니다.
# 일반 함수 def square(n): return n ** 2 # 같은 일을 하는 람다 square = lambda n: n ** 2 print(square(5)) # 25
lambda 인자: 식 형태이고, return 없이 식의 결과가 자동으로 반환됩니다. 람다는 보통 다른 함수의 인자로 잠깐 쓸 때 진가를 발휘합니다. 대표적으로 정렬 기준 지정입니다.
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 좋은 예 ✅
반복되는 코드는 함수로 묶는 게 파이썬답습니다.
# ❌ 나쁜 예: 같은 계산을 복사-붙여넣기 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: 식)는 짧은 일회용 함수로, 주로sorted의key같은 곳에 쓴다.
🧪 실습 문제
문제 1. 두 수를 받아 큰 값을 돌려주는(출력 아님!) 함수 bigger(a, b)를 작성하고, bigger(7, 3)의 결과를 출력하세요. (내장 max를 써도, if로 직접 비교해도 됩니다.)
문제 2. 다음 코드의 출력은? 왜 그런가요?
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. 다음 코드의 출력은?
count = 10 def update(): count = 99 update() print(count)
<details>
<summary>✅ 정답·해설 보기</summary>
1.
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.
def greet(name, greeting="Hello"): return f"{greeting}, {name}!" print(greet("Mina")) # Hello, Mina! print(greet("Mina", "Hi")) # Hi, Mina!
4.
def my_max(*nums): return max(nums) print(my_max(3, 7, 2, 9, 4)) # 9
5. 10. 함수 안의 count = 99는 전역 count를 바꾸는 게 아니라 새 지역 변수를 만듭니다. 함수가 끝나면 지역 count는 사라지고, 전역 count는 10 그대로입니다.
</details>
◀️ 이전 장: 08. 반복문 | ▶️ 다음 장: 10. 예외 처리 입문