
이미지 출처: Pexels · Sóc Năng Động · Pexels License
카페 주문 앱의 장바구니 함수가 이상하다는 말이 들어왔습니다. 첫 손님이 사과를 담았는데, 셋째 손님 바구니에 사과와 우유가 이미 들어 있더라는 것입니다. 함수는 여덟 줄이고 에러는 한 번도 나지 않았습니다.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("사과"))
print(add_item("우유"))
print(add_item("계란"))
['사과']
['사과', '우유']
['사과', '우유', '계란']
기대한 출력은 ['사과'], ['우유'], ['계란']이었습니다. 손님마다 새 바구니를 주려고 basket=[]를 기본값으로 둔 것인데, 결과는 바구니 하나를 세 손님이 돌려 쓴 모양입니다. 이 편은 이 여덟 줄이 왜 그렇게 도는지 원인을 찾고, 같은 함수 문법에서 초보자가 두 번째로 걸리는 자리인 count += 1까지 실행 결과로 확인합니다. 시즌 1의 9편(함수 def)이 "함수를 만드는 법"이었다면, 이 편은 "함수에 값이 들어오고 나가는 규칙"입니다.
기본값과 별표 인자가 뭔가요 — 정의부터
파이썬 공식 자습서는 함수의 매개변수에 기본 인자 값을 둘 수 있다고 정의합니다. def add_item(item, basket=[])에서 basket=[]가 그것이고, 호출할 때 그 자리를 비우면 기본값이 들어갑니다. 자습서는 바로 뒤에 "중요한 주의사항"이라고 적고 이렇게 씁니다. 기본값은 오직 한 번만 값이 구해집니다. 리스트나 딕셔너리처럼 바꿀 수 있는 객체가 기본값이면 여기서 차이가 생깁니다.
별표가 붙는 매개변수는 둘입니다. *name은 형식 매개변수 목록 밖의 위치 인자들을 담은 튜플을 받고, **name은 대응하는 매개변수가 없는 키워드 인자들을 담은 딕셔너리를 받습니다. 자습서는 *name이 **name 앞에 와야 한다고 적었습니다.
성질은 넷입니다.
| 성질 | 뜻 |
|---|---|
기본값은 def가 실행될 때 한 번 만들어진다 | 호출할 때마다 새로 만들어지지 않는다 |
| 바꿀 수 있는 기본값은 호출 사이에 이어진다 | 리스트·딕셔너리 기본값에 넣은 값이 다음 호출에 남는다 |
*args는 튜플, **kwargs는 딕셔너리다 | 몇 개를 넣든 함수 안에서는 그릇 하나로 받는다 |
| 함수 안에서 대입하면 그 이름은 지역 변수가 된다 | 밖의 변수를 고치려면 global을 써야 한다 |
이 편에서 쓰는 말은 이렇게 정합니다.
| 용어 | 이 글에서의 뜻 |
|---|---|
| 기본값 | def 줄에서 = 뒤에 적어 둔, 호출 때 비워도 되는 값 |
| 바꿀 수 있는 객체 | 리스트·딕셔너리·집합처럼 안의 내용을 고칠 수 있는 값 |
*args | 자리 인자를 몇 개든 받아 튜플로 묶는 매개변수 |
**kwargs | 이름=값 인자를 몇 개든 받아 딕셔너리로 묶는 매개변수 |
| 지역 변수 | 함수 안에서 대입해서 생긴, 함수 밖에서는 보이지 않는 이름 |
비유는 정의 뒤에 붙입니다. def 줄의 기본값은 가게 문을 열 때 계산대 옆에 놓아 둔 바구니 하나입니다. 손님이 자기 바구니를 안 들고 오면 그 바구니를 씁니다. 문제는 그 바구니를 쓰고 나서 비우는 사람이 없다는 것입니다. 다음 손님도 같은 바구니를 받습니다.
재현 — 세 번 부르면 세 번 자랍니다
의뢰받은 여덟 줄을 그대로 돌립니다.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("사과"))
print(add_item("우유"))
print(add_item("계란"))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
['사과']
['사과', '우유']
['사과', '우유', '계란']
에러가 없습니다. 첫 호출은 기대한 대로이고, 둘째부터 앞 손님 물건이 남아 있습니다. 함수 어디에도 "앞 호출 결과를 기억하라"는 줄이 없는데 기억합니다. 의심 가는 줄은 basket=[] 하나입니다.

세 번째 출력이 사건 현장입니다. 기대한 것은 계란 하나였습니다.
원인 — 기본값 리스트는 def 때 한 번만 만들어집니다
[]는 호출할 때마다 새로 만들어지는 것이 아니라, def add_item(...) 줄이 실행되는 순간 딱 한 번 만들어집니다. 그 뒤 모든 호출이 그 리스트 하나를 basket으로 받습니다. 리스트는 안의 내용을 바꿀 수 있는 객체라 append한 것이 남고, 다음 호출이 그 상태를 이어받습니다.
같은 리스트인지 확인하는 방법이 있습니다. 파이썬은 객체마다 번호를 붙이고 id()로 보여 줍니다.
def add_item(item, basket=[]):
print(id(basket))
basket.append(item)
return basket
a = add_item("사과")
b = add_item("우유")
print(a is b)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
2122221678144
2122221678144
True
숫자는 실행할 때마다 다르지만 두 줄이 같습니다. a is b가 True이니 첫 호출이 돌려준 리스트와 둘째 호출이 돌려준 리스트가 같은 물건입니다. 공식 FAQ는 이 문제를 "흔히 신참 프로그래머들을 깨무는 버그"라고 부르고, 원인을 한 문장으로 적었습니다. 함수가 정의될 때 기본값은 정확히 한 번 만들어집니다.
기본값이 숫자나 문자열이면 이 문제가 없습니다. 안의 내용을 바꿀 수 없기 때문입니다.
def greet(name, times=1):
return ("안녕 " + name + " ") * times
print(greet("철수"))
print(greet("영희", 3))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
안녕 철수
안녕 영희 안녕 영희 안녕 영희
times=1은 몇 번을 불러도 1입니다. 숫자는 고칠 수 없는 값이라 호출 사이에 이어질 것이 없습니다. FAQ도 숫자, 문자열, 튜플, None은 안전하고 딕셔너리, 리스트, 클래스 인스턴스가 문제라고 갈라 적었습니다.

위는 basket=[], 아래는 basket=None으로 두고 함수 안에서 새로 만든 경우입니다.
해결 — None을 두고 함수 안에서 새로 만듭니다
FAQ가 권하는 방법은 하나입니다. 기본값을 None으로 두고, 함수 안에서 None이면 그때 새 리스트를 만듭니다. None은 바꿀 수 없는 값이라 공유돼도 문제가 없고, 새 리스트는 호출할 때마다 함수 안에서 만들어집니다.
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("사과"))
print(add_item("우유"))
mine = ["빵"]
print(add_item("커피", mine))
print(mine)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
['사과']
['우유']
['빵', '커피']
['빵', '커피']
앞의 두 줄이 기대한 대로 따로 나옵니다. 뒤의 두 줄은 손님이 자기 바구니 mine을 들고 온 경우입니다. 그때는 새로 만들지 않고 받은 바구니에 담고, 그래서 mine에도 커피가 들어 있습니다. 이건 버그가 아니라 의도한 동작입니다. 리스트를 넘기면 함수가 그 리스트를 고칠 수 있다는 것은 시즌 1 리스트 편에서 본 성질 그대로입니다.
고친 코드에서 바뀐 것은 두 줄입니다. basket=[]가 basket=None이 되고, if basket is None: basket = []가 들어갔습니다. 비교는 ==가 아니라 is로 합니다. None은 세상에 하나뿐인 값이라 "같은 물건인가"를 묻는 is가 맞습니다.
값이 몇 개든 받기 — *args는 튜플로 묶입니다
사건 하나가 끝났으니 같은 def 줄에서 자주 쓰는 문법을 둘 더 봅니다. 손님이 물건을 몇 개 사든 합계를 내는 함수를 만들려면 매개변수 개수를 미리 정할 수 없습니다. 이때 이름 앞에 별표를 하나 붙입니다.
def total(*prices):
print(type(prices), prices)
return sum(prices)
print(total(1000, 2500))
print(total())📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
<class 'tuple'> (1000, 2500)
3500
<class 'tuple'> ()
0
total(1000, 2500)이라고 부르면 함수 안에서 prices는 (1000, 2500), 즉 튜플입니다. 아무것도 안 넣으면 빈 튜플입니다. 자습서는 이 인자들이 "튜플로 묶인다"고 적었고, 별표 매개변수 뒤에 오는 매개변수는 키워드로만 넘길 수 있다고 덧붙였습니다. 이름이 args일 필요는 없습니다. 관례일 뿐이고 *prices처럼 뜻이 보이는 이름이 낫습니다.
이름=값 꼴을 몇 개든 받으려면 별표를 둘 붙입니다.
def order(**options):
print(type(options), options)
for k, v in options.items():
print(k, "=", v)
order(size="large", ice=False)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
<class 'dict'> {'size': 'large', 'ice': False}
size = large
ice = False
이번에는 딕셔너리입니다. 자습서 정의대로 "대응하는 매개변수가 없는 키워드 인자들을 담은 딕셔너리"를 받습니다. 시즌 2 2편에서 배운 items()로 그대로 돕니다.
셋을 한 def에 같이 쓸 때 순서가 있습니다. 보통 매개변수, *args, **kwargs 순입니다.
def receipt(name, *items, **opts):
print(name, items, opts)
receipt("철수", "커피", "빵", discount=10)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
철수 ('커피', '빵') {'discount': 10}
"철수"는 name이 받고, 남은 자리 인자 "커피", "빵"은 튜플로, discount=10은 딕셔너리로 갑니다. 순서를 바꿔 키워드 인자 뒤에 자리 인자를 쓰면 SyntaxError: positional argument follows keyword argument가 납니다. 이건 실행 전에 걸리는 에러라 금방 찾습니다.

별표 하나는 튜플로, 둘은 딕셔너리로 포장합니다. 풀 때도 별표입니다.
포장의 반대도 됩니다. 값이 이미 리스트나 딕셔너리에 들어 있으면 호출할 때 별표를 붙여 풀어 넣습니다.
def total(*prices):
return sum(prices)
def order(**options):
for k, v in options.items():
print(k, "=", v)
prices = [1000, 2500, 700]
print(total(*prices))
opts = {"size": "small", "ice": True}
order(**opts)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
4200
size = small
ice = True
total(*prices)는 total(1000, 2500, 700)과 같습니다. 자습서는 range()에 [3, 6]을 *args로 풀어 넘기는 예를 들었습니다. 정의에서 별표는 "모아라", 호출에서 별표는 "풀어라"입니다.
두 번째 사건 — count += 1이 UnboundLocalError가 됩니다
방문 횟수를 세는 함수도 신고가 들어왔습니다. 이번에는 에러가 납니다.
count = 0
def visit():
count += 1
return count
try:
visit()
except UnboundLocalError as e:
print("UnboundLocalError:", e)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
count는 분명히 위에서 0으로 만들었는데 "값이 없다"고 합니다. 이유는 파이썬이 def 안에서 대입이 있는 이름을 지역 변수로 본다는 규칙에 있습니다. count += 1은 count = count + 1이고 대입입니다. 그래서 함수 안의 count는 밖의 count와 다른 새 이름이 되고, 아직 값이 없는 새 이름에서 + 1을 하려니 에러가 납니다. 공식 FAQ의 "변수에 값이 있을 때 UnboundLocalError가 발생하는 이유" 항목이 정확히 이 상황입니다.
읽기만 하면 문제가 없습니다.
rate = 0.1
def tax(price):
return price * rate
print(tax(1000))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
100.0
rate는 함수 안에서 대입되지 않으니 지역 변수가 아니고, 파이썬은 밖에서 찾아 씁니다. 자습서 9.2절이 적은 찾는 순서는 안에서 밖으로입니다. 함수 안, 그 함수를 감싼 함수, 모듈 전체, 마지막으로 내장 이름입니다.
밖의 변수를 정말로 바꾸려면 global로 선언합니다.
count = 0
def visit():
global count
count += 1
return count
print(visit(), visit())📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
1 2
global count 한 줄이 "이 함수 안의 count는 밖의 것"이라고 알립니다. 자습서는 global로 선언하면 모든 참조와 대입이 모듈의 전역 이름으로 간다고 설명합니다. 다만 이 방법은 함수가 밖의 상태를 몰래 바꾸게 만들어 나중에 찾기 어려운 버그가 되기 쉽습니다. 저는 값을 return으로 돌려주고 밖에서 count = visit(count)처럼 받는 쪽을 먼저 씁니다. global은 정말 하나뿐인 설정값을 다룰 때만 씁니다.
함수 안에 함수가 있을 때는 nonlocal입니다. 바깥 함수의 변수를 안쪽 함수가 바꿉니다.
def counter():
n = 0
def hit():
nonlocal n
n += 1
return n
return hit
h = counter()
print(h(), h(), h())📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
1 2 3
hit은 counter가 만든 n을 기억하고 부를 때마다 늘립니다. 자습서는 nonlocal로 선언하지 않으면 감싼 함수의 변수는 읽기 전용이라고 적었습니다. 이 꼴은 시즌 3 클래스 편에서 다시 나옵니다.
장바구니 함수로 본 규칙 다섯
| 상황 | 규칙 | 근거 |
|---|---|---|
| 기본값이 리스트·딕셔너리 | None으로 두고 안에서 새로 만든다 | 기본값은 def 때 한 번만 만들어진다 |
기본값이 숫자·문자열·튜플·None | 그대로 써도 된다 | 바꿀 수 없는 값은 호출 사이에 이어지지 않는다 |
| 자리 인자를 몇 개든 | *args, 함수 안에서는 튜플 | 남은 자리 인자를 튜플로 묶는다 |
이름=값을 몇 개든 | **kwargs, 함수 안에서는 딕셔너리 | 대응 매개변수 없는 키워드 인자를 딕셔너리로 묶는다 |
| 밖의 변수를 함수 안에서 바꾸기 | global(모듈) · nonlocal(감싼 함수), 되도록 return으로 | 함수 안 대입은 지역 변수를 만든다 |
기억할 것은 셋입니다. 리스트 기본값은 None으로 바꿉니다. 별표는 정의에서 모으고 호출에서 풉니다. 함수 안에서 +=를 쓰는 이름은 지역 변수라 밖의 것을 바꾸려면 선언이 필요합니다.
출력을 맞혀 보면 어디서 갈리는지 보입니다
미니 퀴즈
참고 자료
- 파이썬 공식 자습서 4.9.1 "기본 인자 값" — "기본값은 오직 한 번만 값이 구해집니다"와
L=[]누적 예제,L=None해법 https://docs.python.org/ko/3/tutorial/controlflow.html - 파이썬 공식 자습서 4.9.2 "키워드 인자"·4.9.4 "임의의 인자 목록"·4.9.5 "인자 목록 언 패킹" —
**name은 딕셔너리,*name은 튜플, 호출 때*로 풀기 https://docs.python.org/ko/3/tutorial/controlflow.html - 파이썬 프로그래밍 FAQ "객체 간에 기본값이 공유되는 이유는 무엇입니까?"·"변수에 값이 있을 때 UnboundLocalError가 발생하는 이유는 무엇입니까?" https://docs.python.org/ko/3/faq/programming.html
- 파이썬 공식 자습서 9.2 "파이썬 스코프와 이름 공간" — 이름을 찾는 순서,
global과nonlocalhttps://docs.python.org/ko/3/tutorial/classes.html
포켓코딩(PYTHON2) 5편 · 4편(파이썬 리스트 컴프리헨션)에서 이어집니다. 예제 코드는 강사 개인 GitHub 저장소의 강의 자료와 파이썬 공식 자습서의 예제를 바탕으로 다시 썼고, 출력은 전부 Python 3.14에서 실행해 받아 적었습니다. 다음 편은 모듈과 import입니다. 파일을 나누는 법, from으로 골라 가져오는 법, 그리고 파일 이름을 표준 모듈과 똑같이 지으면 왜 엉뚱한 것이 불려 오는지 봅니다.