파이썬 함수: def, return, 인자, *args와 **kwargs, lambda, 타입 힌트
파이썬 함수는 def 키워드로 정의하고, 이름과 괄호로 호출하며, return으로 결과를 돌려줍니다. return이 없으면 None을 반환합니다. 인자는 순서대로 또는 이름으로 전달할 수 있고, *args는 남은 위치 인자를 튜플로, **kwargs는 남은 키워드 인자를 딕셔너리로 모읍니다. lambda는 한 줄짜리 익명 함수를 만듭니다.
파이썬 함수는 이름이 붙은 코드 조각으로, 서로 다른 입력으로 여러 번 호출할 수 있습니다. def 키워드로 정의하고, 매개변수로 값을 받고, return으로 결과를 돌려줍니다. 긴 스크립트를 이해하기 쉽고 검증하기 쉬운 단계들로 나누는 가장 기본적인 방법이 함수입니다.
def로 함수 정의하기
함수 정의는 def, 이름, 괄호 안의 매개변수, 콜론으로 시작합니다. 본문은 4칸 들여씁니다. 파이썬에서 들여쓰기는 장식이 아니라 문법의 일부입니다.
def greet(name):
"""사용자에게 줄 인사말을 반환합니다."""
return f"안녕하세요, {name}님!"
print(greet("안나")) # 안녕하세요, 안나님!
헤더 바로 아래에 있는 세 개의 따옴표 문자열은 독스트링(docstring), 즉 함수의 설명문입니다. help(greet)나 편집기의 도움말에 표시됩니다. 함수 이름은 PEP 8 규칙에 따라 snake_case로 씁니다: add_expense, get_rate.
def greet(name): 같은 한 줄을 함수의 시그니처라고 합니다. 이름과 매개변수 목록(필요하면 타입 힌트 포함)입니다. 시그니처만 봐도 본문을 읽지 않고 함수를 어떻게 호출하는지 알 수 있습니다.
return의 동작
return은 함수를 끝내고, 호출한 쪽에 값을 돌려줍니다. return이 실행되고 나면 그 아래 코드는 실행되지 않습니다. return이 없거나 값 없이 쓰이면 함수는 None을 반환합니다.
def area(w, h):
w * h # 계산은 되지만 버려집니다
print(area(3, 4)) # None
def area(w, h):
return w * h
print(area(3, 4)) # 12
return을 빠뜨리는 것은 초보자가 가장 많이 하는 실수 중 하나입니다. 함수는 실행되지만 None을 돌려주고, 오류는 한두 줄 뒤에서 나타납니다. 값을 여러 개 돌려줄 수도 있습니다. 실제로는 튜플 하나가 반환되며, 편리하게 풀어서 받을 수 있습니다.
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 1, 9])
print(low, high) # 1 9
이른 return은 중첩을 줄이는 데 좋습니다. 예외 상황을 먼저 걸러내고, 기본 로직을 마지막에 씁니다.
def discount(price, is_member):
if price <= 0:
return 0
if not is_member:
return price
return price * 0.9
파이썬 함수 인자: 위치 인자와 키워드 인자
매개변수는 정의에 적힌 이름이고, 인자는 호출할 때 실제로 넘기는 값입니다. 인자는 순서대로(위치 인자) 또는 이름으로(키워드 인자) 전달할 수 있습니다. 이름으로 넘기면 호출이 스스로 설명되고, 순서에 신경 쓸 필요가 없습니다.
def add_expense(title, amount, category="기타"):
return {"title": title, "amount": amount, "category": category}
add_expense("커피", 250) # 위치 인자
add_expense(amount=250, title="커피") # 키워드 인자
add_expense("택시", 600, category="교통") # 섞어서 사용
기본값이 있는 매개변수는 선택 사항입니다. 규칙이 있습니다. 기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 하고, 호출할 때도 위치 인자가 키워드 인자보다 앞에 와야 합니다. 그렇지 않으면 SyntaxError가 납니다.
함정: 변경 가능한 기본값
기본값은 함수 정의 시점에 한 번만 계산됩니다. 매 호출마다 새로 만들어지는 것이 아닙니다. 기본값이 리스트나 딕셔너리라면 모든 호출이 같은 객체를 공유합니다.
def add_item(item, acc=[]):
acc.append(item)
return acc
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — 리스트가 이전 호출을 기억합니다
올바른 방법은 기본값을 None으로 두고, 함수 안에서 새 객체를 만드는 것입니다.
def add_item(item, acc=None):
if acc is None:
acc = []
acc.append(item)
return acc
*args와 **kwargs: 인자 개수가 정해지지 않은 경우
매개변수 앞의 별표 하나는 남은 위치 인자를 모두 튜플(tuple)로 모읍니다. 별표 두 개는 남은 키워드 인자를 딕셔너리(dict)로 모읍니다. args와 kwargs라는 이름은 관례일 뿐이며, 중요한 것은 별표입니다.
def total(*args):
return sum(args)
print(total(100, 250, 40)) # 390
def make_user(name, **kwargs):
user = {"name": name}
user.update(kwargs)
return user
print(make_user("안나", city="서울", age=30))
# {'name': '안나', 'city': '서울', 'age': 30}
같은 별표를 호출할 때 쓰면 반대 방향으로 동작합니다. 시퀀스나 딕셔너리를 펼쳐서 인자로 넘깁니다.
values = [3, 4]
print(pow(*values)) # 81
opts = {"sep": " | ", "end": "\n"}
print("a", "b", **opts) # a | b
위치 전용과 키워드 전용 매개변수
매개변수 목록의 슬래시 /와 별표 *는 차단선 역할을 합니다. / 앞의 매개변수는 순서대로만 전달할 수 있고(Python 3.8부터, PEP 570), * 뒤의 매개변수는 이름으로만 전달할 수 있습니다.
def convert(amount, /, *, rate, currency="USD"):
return round(amount / rate, 2), currency
convert(100, rate=90.5) # (1.1, 'USD')
convert(amount=100, rate=90.5) # TypeError: amount는 위치로만 전달할 수 있습니다
convert(100, 90.5) # TypeError: rate는 이름으로만 전달해야 합니다
키워드 전용 매개변수는 convert(100, 90.5, "EUR") 같은 헷갈리는 호출을 막아 줍니다. 어떤 값이 무엇인지 분명하지 않을 때 특히 유용합니다.
변수의 범위: LEGB 규칙
파이썬은 이름을 다음 순서로 찾습니다. Local(함수 내부), Enclosing(바깥 함수), Global(모듈 최상위), Built-in(len 같은 내장 이름). 함수 안에서 전역 변수를 읽을 수는 있지만, 값을 새로 대입하려면 global 선언이 필요합니다.
limit = 1000
def check(amount):
return amount > limit # 전역 변수 읽기는 가능
def raise_limit():
global limit
limit = 2000 # global이 없으면 지역 변수가 새로 생깁니다
실무에서는 global을 피하는 편이 좋습니다. 바깥 상태를 바꾸는 함수는 테스트하기 어렵습니다. 값을 인자로 받고 결과를 반환하는 방식이 더 안전합니다.
중첩 함수와 클로저
함수 안에 함수를 정의할 수 있습니다. 안쪽 함수는 바깥 함수가 끝난 뒤에도 바깥 변수를 기억합니다. 이것을 클로저라고 합니다. 바깥 변수의 값을 바꾸려면 nonlocal을 씁니다.
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c()) # 1 2 3
클로저는 데코레이터와 함수 팩토리의 바탕입니다.
함수는 객체다
파이썬에서 함수는 일반 객체입니다. 변수에 담을 수 있고, 다른 함수에 넘길 수 있고, 함수에서 반환할 수 있으며, 딕셔너리에 저장할 수도 있습니다. 함수에는 __name__, __doc__, __defaults__, __annotations__ 같은 속성이 있습니다.
def double(x):
return x * 2
f = double
print(f(5), f.__name__) # 10 double
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]
lambda: 익명 함수
lambda 함수는 이름이 없고 본문이 식 하나로 된 짧은 함수입니다. 식의 결과가 자동으로 반환되므로 return은 쓰지 않습니다.
square = lambda x: x ** 2
print(square(4)) # 16
expenses = [("커피", 250), ("택시", 600), ("점심", 450)]
expenses.sort(key=lambda e: e[1], reverse=True)
print(expenses) # [('택시', 600), ('점심', 450), ('커피', 250)]
print(list(filter(lambda n: n % 2 == 0, range(10)))) # [0, 2, 4, 6, 8]
lambda는 sorted, max, map, filter에 넘기는 짧은 인자로 어울립니다. 위의 square처럼 lambda를 변수에 대입하는 것은 PEP 8이 권장하지 않습니다. 이름이 필요하다면 def를 쓰세요. 그래야 트레이스백에 제대로 된 __name__이 나오고, 독스트링을 적을 수 있습니다.
| def | lambda | |
|---|---|---|
| 이름 | 있음 | 없음 (<lambda>) |
| 본문 | 명령문을 여러 개 가능 | 식 하나 |
| return | 명시적으로 씀 | 자동 |
| 독스트링, 타입 힌트 | 가능 | 불가능 |
| 적합한 경우 | 모든 경우 | 짧은 key나 콜백 |
타입 힌트: 힌트일 뿐 검사가 아님
타입 힌트는 매개변수와 반환값의 기대 타입을 표시합니다. 파이썬은 실행할 때 힌트를 검사하지 않습니다. x: int로 적힌 함수에 문자열을 넘겨도 실행은 됩니다. 검사는 mypy, pyright, ty 같은 외부 도구와 편집기가 실행 전에 해 줍니다.
def add_expense(title: str, amount: float, category: str = "기타") -> dict[str, object]:
return {"title": title, "amount": amount, "category": category}
def area(w: float, h: float) -> float:
w * h # mypy: Missing return statement
설치 없이 검사하려면 uvx mypy .를 실행하세요. 위 예제는 타입 힌트의 장점을 보여 줍니다. -> float로 선언했는데 return이 없으면, 분석 도구가 실행 전에 바로 알려 줍니다.
Python 3.12(PEP 695)부터는 제네릭 함수를 TypeVar 없이 쓸 수 있습니다. 타입 매개변수는 대괄호에 적고, 타입 별칭은 type 키워드로 선언합니다.
def first[T](xs: list[T]) -> T:
return xs[0]
type Money = int | float
Python 3.14(PEP 649)에서는 어노테이션이 지연 평가됩니다. 처음 접근할 때 계산되므로, 아래에 선언된 클래스를 따옴표 없이 참조할 수 있고, from __future__ import annotations도 더 이상 필요하지 않습니다.
인자를 안 넘긴 것과 None을 넘긴 것의 차이: Python 3.15의 sentinel
기본값을 None으로 두면 '인자를 전달하지 않음'과 '일부러 None을 전달함'을 구분할 수 없습니다. 예전에는 직접 만든 자리 표시자로 이 문제를 풀었습니다.
_MISSING = object()
def get_rate(currency, default=_MISSING):
rates = {"USD": 90.5}
if currency in rates:
return rates[currency]
if default is _MISSING:
raise KeyError(currency)
return default
Python 3.15에는 내장 함수 sentinel이 추가되었습니다(PEP 661). MISSING = sentinel("MISSING")은 고유한 표식을 만들며, repr이 읽기 쉽고, 복사해도 같은 객체로 남고, pickle로 직렬화되며, float | MISSING 같은 타입 힌트에도 쓸 수 있습니다. 이전 버전에서는 object()를 이용한 앞의 방법을 계속 쓰면 됩니다.
함수에서 자주 하는 실수
- return을 빠뜨림 — 함수가
None을 반환합니다. - 괄호를 빠뜨림 —
print(greet)는 함수 객체를 출력하고, 호출 결과는 출력하지 않습니다. - 변경 가능한 기본값 — 모든 호출이 같은 리스트를 공유합니다.
- 인자 개수 틀림 —
TypeError: greet() missing 1 required positional argument: 'name'. - 내장 이름과 충돌 —
list나sum이라는 이름으로 함수를 만들면 내장 함수를 가려 버립니다.
정리
파이썬 함수는 def로 정의하고, return으로 결과를 돌려주며, 인자를 순서대로 또는 이름으로 받습니다. *args와 **kwargs는 유연성을 주고, /와 *는 호출 방식을 엄격하게 만들며, lambda는 한 번만 쓰는 함수를 짧게 표현하고, 타입 힌트는 mypy와 함께 실행 전에 오류를 잡습니다. 과정 강의에서는 이 주제를 실습으로 다룹니다. 지출 관리기를 타입 힌트가 붙은 함수들로 나누고 mypy로 검사합니다.
자주 묻는 질문
매개변수와 인자는 무엇이 다른가요?
매개변수는 함수 정의에 적힌 이름이고, 인자는 호출할 때 넘기는 실제 값입니다. def greet(name)에서 name은 매개변수이고, greet("안나")에서 "안나"는 인자입니다.
함수를 정의하기 전에 호출할 수 있나요?
모듈 최상위에서는 안 됩니다. def 문이 먼저 실행되어야 하므로, 그렇지 않으면 NameError가 납니다. 다른 함수 안에서는 호출 시점에 정의되어 있으면 괜찮습니다.
함수는 값을 여러 개 반환할 수 있나요?
형식상은 하나를 반환하지만, return a, b처럼 쓰면 두 원소의 튜플이 반환되고, x, y = f()로 풀어서 받을 수 있습니다.
타입 힌트는 꼭 써야 하나요?
필수는 아닙니다. 파이썬은 힌트 없이도 동작합니다. 하지만 다른 사람이 읽는 코드에서는 힌트가 유지보수를 쉽게 하고, mypy나 pyright가 실행 전에 오류를 찾게 해 줍니다.
lambda에서 if를 쓸 수 있나요?
조건식은 쓸 수 있습니다: lambda x: "짝" if x % 2 == 0 else "홀". 하지만 lambda 안에서는 명령문, 반복문, 대입문을 쓸 수 없습니다.