모던 파이썬 기초 #5 함수 — 인자 패턴
#4 컬렉션과 컴프리헨션에서 데이터 다루는 도구를 봤다면, 이번 글은 그 데이터를 받아서 일하는 함수의 시그니처 표현법입니다. 파이썬 함수는 인자 표기법이 굉장히 풍부합니다. 그게 강점이지만 처음에는 헷갈리는 부분이기도 합니다. 한곳에 모아 정리합니다.
가장 단순한 함수 #
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이 성질이 구 강좌의 데코레이터와 클로저의 토대입니다. 모던 시리즈에서는 **다음 시리즈(파이썬 중급)**에서 데코레이터를 다시 다룹니다.
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[[인자타입들], 반환타입] 형태입니다. 인자 타입을 리스트로 적는 게 처음에 어색합니다.
docstring — 문서 문자열 #
함수 첫 줄의 문자열은 docstring이 됩니다.
def add(a: int, b: int) -> int:
"""두 정수를 더해 반환한다."""
return a + b
print(add.__doc__)
# 두 정수를 더해 반환한다.
help(add)타입 힌트가 “무엇을 받고 무엇을 주는가"를 채워주니, docstring은 **“왜 / 어떻게 쓰는가”**를 적는 부분입니다. 짧고 단호한 한 줄이 가장 좋습니다.
정리 #
이번 글에서 다룬 모든 인자 패턴:
- 기본값 — 가변 객체(list/dict)는 절대 기본값으로 두지 말고
None+ 함수 안에서 생성 - positional vs keyword 호출 — keyword 호출이 의도를 잘 드러냄
*args— 가변 위치 인자, tuple로 받음**kwargs— 가변 키워드 인자, dict로 받음- 호출 측
*seq,**dict— 펼쳐서 넘기기 *,뒤는 keyword-only — 불리언 플래그 여럿일 때 호출 가독성 강제/,앞은 positional-only — 매개변수 이름의 자유를 보존lambda는 콜백 용도, 변수 대입은 안 함nonlocal로 바깥 스코프 변수 수정- 함수는 일급 객체 —
Callable[[...], ...]로 타입 힌트 - docstring은
"""..."""로 첫 줄
다음 글(#6 에러와 예외 처리)에서는 **try/except/else/finally**와 사용자 정의 예외, 그리고 Python 3.11에서 들어온 except* (exception group) 까지 다룹니다.