함수 — 인자 패턴
기본값, *args/**kwargs, positional-only(/), keyword-only(*)까지 함수 시그니처를 표현력 있게 적는 모든 도구를 정리합니다.
4장 컬렉션과 컴프리헨션에서 데이터 다루는 도구를 봤다면, 본 챕터는 그 데이터를 받아서 처리하는 함수의 시그니처 표현법입니다. 파이썬 함수는 인자 표기법이 풍부합니다. 장점이지만 처음에는 헷갈릴 수 있는 부분이기도 합니다. 한곳에 모아 정리합니다.
본 챕터의 인자 표기는 2부의 12장 데코레이터 패턴과 3부의 20장 typing 고급 — Variance, ParamSpec, Self, overload에서 다시 만납니다. ParamSpec 같은 고급 타입 도구가 결국 본 챕터의 *args / **kwargs 모양을 타입 시스템에 그대로 옮겨놓은 것이라, 본 챕터 끝낼 때 표기 패턴을 분명히 잡아두는 게 뒤에 도움이 됩니다.
가장 단순한 함수 #
def greet(name: str) -> None:
print(f"hi, {name}!")
greet("커티스") # hi, 커티스!def 키워드, 콜론 후 들여쓰기. 2장에서 본 타입 힌트 (: str, -> None)를 매 함수에 적는 것이 모던 파이썬의 원칙이고 본 책도 그 원칙을 따릅니다.
return과 다중 반환
#
def split_name(full: str) -> tuple[str, str]:
first, last = full.split(" ", 1)
return first, last
first, last = split_name("커티스 김")
print(first, last) # 커티스 김return a, b는 사실 **return (a, b)**입니다. 받는 쪽에서 unpack으로 풉니다. JavaScript의 객체 분해와 비슷한 효용을 tuple로 합니다.
return이 없으면 함수는 None을 반환 합니다. 명시적으로 적든, 안 적든 결과는 같습니다.
기본값 #
def greet(name: str, greeting: str = "hi") -> None:
print(f"{greeting}, {name}!")
greet("커티스") # hi, 커티스!
greet("커티스", "안녕하세요") # 안녕하세요, 커티스!기본값이 있는 매개변수는 기본값이 없는 것 뒤에 와야 합니다.
def bad(x: int = 0, y: int) -> int: ...
# SyntaxError: non-default argument follows default argument함정 — 가변 기본값 #
이건 파이썬에서 가장 유명한 함정입니다.
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← ?!
print(append_to(3)) # [1, 2, 3]기본값 []가 함수가 정의될 때 한 번만 만들어지고, 모든 호출에 같은 객체를 공유합니다. 호출할 때마다 새 빈 리스트가 생기는 게 아닙니다.
해결: None을 기본값으로 두고, 함수 안에서 새로 만듭니다.
def append_to(item, target: list[int] | None = None):
if target is None:
target = []
target.append(item)
return target이 패턴은 자주 보게 됩니다. 익혀 두면 편합니다.
Positional과 Keyword 호출 #
호출할 때 인자를 두 가지 방법으로 넘길 수 있습니다.
def create_user(name: str, age: int, role: str = "member") -> dict:
return {"name": name, "age": age, "role": role}
# positional — 위치로
create_user("커티스", 30)
create_user("커티스", 30, "admin")
# keyword — 이름으로
create_user(name="커티스", age=30)
create_user(age=30, name="커티스") # 순서 바꿔도 OK
# 섞어 쓰기 (positional 먼저)
create_user("커티스", age=30, role="admin")호출 코드만 봐도 의도가 보이게 하려면 keyword 호출이 강력합니다. create_user("커티스", 30, True, False, "admin") 같은 시그니처는 읽기 어려운데, create_user(name="커티스", age=30, is_active=True, is_admin=False, role="admin")은 한눈에 들어옵니다.
*args — 가변 위치 인자
#
매개변수 앞에 *를 붙이면 “남은 모든 위치 인자를 tuple로 받음"이라는 의미입니다.
def add(*nums: int) -> int:
total = 0
for n in nums:
total += n
return total
print(add(1, 2, 3)) # 6
print(add(1, 2, 3, 4, 5)) # 15
print(add()) # 0이름은 관례상 args 지만, *nums처럼 의미 있는 이름을 적어도 됩니다.
풀어서 넘기기 #
*는 호출하는 쪽에서도 쓸 수 있습니다. 시퀀스를 풀어서 위치 인자로 넘깁니다.
def add(a: int, b: int, c: int) -> int:
return a + b + c
nums = [1, 2, 3]
print(add(*nums)) # 1, 2, 3 으로 풀려서 들어감 → 6**kwargs — 가변 키워드 인자
#
**를 붙이면 “남은 모든 키워드 인자를 dict로 받음”.
def configure(**options: str) -> None:
for key, value in options.items():
print(f"{key} = {value}")
configure(host="localhost", port="5432", db="myapp")
# host = localhost
# port = 5432
# db = myapp풀어서 넘기기 #
호출 측의 **는 dict를 풀어서 keyword 인자로 넘깁니다.
def create_user(name: str, age: int, role: str) -> dict:
return {"name": name, "age": age, "role": role}
data = {"name": "커티스", "age": 30, "role": "admin"}
print(create_user(**data))API 응답을 함수 인자로 그대로 흘려보내는 경우에 자주 씁니다.
다 합친 형태 #
def fn(a: int, b: int, *args: int, **kwargs: str) -> None:
print(a, b, args, kwargs)
fn(1, 2, 3, 4, 5, name="커티스", role="admin")
# 1 2 (3, 4, 5) {'name': '커티스', 'role': 'admin'}순서: **일반 인자 → *args → 일반 인자(keyword-only) → **kwargs**입니다.
Keyword-only — * 단독 사용
#
*만 따로 두면 그 뒤의 매개변수는 반드시 keyword로만 받게 됩니다.
def create(name: str, *, role: str = "member", active: bool = True) -> dict:
return {"name": name, "role": role, "active": active}
create("커티스") # OK
create("커티스", role="admin") # OK
create("커티스", "admin") # ✗ TypeError
# create() takes 1 positional argument but 2 were given호출 측이 create("커티스", "admin", True) 같은 모양을 못 쓰게 막아줍니다. 불리언 플래그가 여러 개일 때 호출이 의미 있게 보이도록 강제할 수 있습니다.
Positional-only — / 단독 사용 (3.8+)
#
반대로 / 앞은 위치로만 받게 강제합니다.
def power(base: float, exp: float = 2, /) -> float:
return base ** exp
power(3) # 9
power(2, 3) # 8
power(base=3) # ✗ TypeError — base 는 keyword 못 씀언제 쓰나? 매개변수 이름을 미래에 바꿀 자유를 남겨두고 싶을 때. 호출 측이 이름으로 묶이지 않으면 라이브러리 메인테이너가 이름만 리네이밍해도 호출 측 코드가 안 깨집니다. 빌트인 함수(abs(x), len(seq))가 대부분 이런 형태입니다.
셋 다 함께 #
def f(pos1: int, pos2: int, /, normal: int, *, kw1: int, kw2: int) -> None:
print(pos1, pos2, normal, kw1, kw2)
# pos1, pos2 → 위치만
# normal → 위치 또는 keyword
# kw1, kw2 → keyword 만
f(1, 2, 3, kw1=4, kw2=5) # OK
f(1, 2, normal=3, kw1=4, kw2=5) # OK
f(pos1=1, pos2=2, ...) # ✗ pos1, pos2 는 keyword 불가
f(1, 2, 3, 4, 5) # ✗ kw1, kw2 는 위치 불가읽는 법:
/앞 = positional-only/와*사이 = 일반*뒤 = keyword-only
람다 — 한 줄 함수 #
lambda는 익명 함수입니다. 한 표현식만 가질 수 있습니다.
square = lambda x: x ** 2
print(square(5)) # 25
# sorted 의 key 같은 콜백 역할에 자주 씀
people = [("커티스", 30), ("스미스", 25), ("존", 40)]
people.sort(key=lambda p: p[1])
print(people)
# [('스미스', 25), ('커티스', 30), ('존', 40)]람다를 변수에 대입해서 쓰는 패턴(square = lambda x: ...)은 권장되지 않습니다. def square(x): ...로 적으세요. 람다는 이름이 필요 없는 경우에만 쓰는 것이 관례입니다.
nonlocal과 global — 스코프 잠깐
#
함수 안에서 바깥 변수를 수정 하려면 명시가 필요합니다.
def outer():
count = 0
def inner():
nonlocal count # outer 의 count 를 수정한다고 선언
count += 1
inner()
inner()
print(count) # 2
outer()nonlocal 없이 count += 1 하면 새 로컬 변수가 inner 안에 만들어져 outer의 것은 안 바뀝니다. (정확히는 UnboundLocalError가 납니다 — 읽기 전에 쓰기를 하기 때문.)
전역 변수를 함수 안에서 수정할 때는 global을 씁니다. 하지만 가능하면 글로벌 변수 자체를 안 쓰는 쪽이 낫습니다.
함수도 일급 객체 #
함수는 변수에 담을 수 있고, 인자로 넘길 수 있고, 반환할 수도 있습니다.
def add(a: int, b: int) -> int:
return a + b
op = add # 함수를 변수에 담음
print(op(2, 3)) # 5
def apply(fn, x: int, y: int) -> int:
return fn(x, y)
print(apply(add, 2, 3)) # 5이 성질이 12장 데코레이터 패턴의 토대입니다. 함수를 받아 함수를 반환하는 한 줄 함수가 데코레이터의 본질입니다.
Callable 타입 — 함수를 받는 함수의 타입
#
함수를 인자로 받을 때 그 시그니처를 적고 싶으면 Callable을 씁니다.
from collections.abc import Callable
def apply(fn: Callable[[int, int], int], x: int, y: int) -> int:
return fn(x, y)
apply(lambda a, b: a + b, 1, 2) # 3Callable[[인자타입들], 반환타입] 형태입니다. 인자 타입을 리스트로 적는 게 처음에 어색합니다.
Callable의 인자 시그니처 자체를 타입 변수로 다루는 ParamSpec은 20장 typing 고급에서 다룹니다.
docstring — 문서 문자열 #
함수 첫 줄의 문자열은 docstring이 됩니다.
def add(a: int, b: int) -> int:
"""두 정수를 더해 반환한다."""
return a + b
print(add.__doc__)
# 두 정수를 더해 반환한다.
help(add)타입 힌트가 “무엇을 받고 무엇을 주는가"를 채워주니, docstring은 **“왜 / 어떻게 쓰는가”**를 적는 부분입니다. 짧고 단호한 한 줄이 가장 좋습니다.
연습문제 #
- 가변 기본값 함정을 직접 재현해 보세요.
def collect(item, bucket=[]):시그니처의 함수에 같은 인자로 3번 호출 후 결과를 확인하고, 같은 함수를bucket: list | None = None+ 안에서 새 리스트 생성으로 고친 뒤 같은 호출을 다시 해 보세요. def request(url: str, *, method: str = "GET", timeout: float = 5.0, retries: int = 0) -> str:시그니처를 작성하세요.method,timeout,retries는 keyword-only로 강제됩니다.request("https://example.com", "POST")호출이 실패하는 것과request("https://example.com", method="POST")가 동작하는 것을 직접 확인합니다.from collections.abc import Callable을 import 하고,def apply_n_times(fn: Callable[[int], int], x: int, n: int) -> int:를 작성하세요.fn을x에n번 적용한 결과를 반환합니다.apply_n_times(lambda x: x + 1, 0, 5)→ 5,apply_n_times(lambda x: x * 2, 1, 10)→ 1024가 나오는지 확인합니다.
한 줄 요약: 함수의 인자 표기는 일반 / 기본값 /
*args/**kwargs/ keyword-only(*) / positional-only(/)의 조합이다. 가변 기본값은None+ 함수 안 생성, 불리언 여럿이면 keyword-only 강제. 호출 측의*seq/**dict는 펼치기. 함수는 값이라Callable[[...], ...]로 타입 힌트.
다음 챕터 #
다음 6장 에러와 예외 처리에서는 **try/except/else/finally**와 사용자 정의 예외, 그리고 Python 3.11에서 들어온 except* (exception group)까지 다룹니다. 본 챕터의 함수가 던지는 예외를 호출 측이 어떻게 받는지가 6장의 시작입니다.