import platform
import matplotlib.pyplot as plt
if platform.system() == "Windows":
plt.rc("font", family="Malgun Gothic")
elif platform.system() == "Darwin":
plt.rc("font", family="AppleGothic")
else:
plt.rc("font", family="NanumBarunGothic")
plt.rcParams["axes.unicode_minus"] = False5 함수와 딕셔너리: 경제 모델의 부품 만들기 (5주차)
5.1 이번 장에서 배우는 것
4장까지 우리는 데이터를 담고(변수, 리스트), 조건에 따라 갈라지고(if), 같은 일을 여러 번 반복하는(for, while) 법을 익혔습니다. 이번 장은 방향이 조금 다릅니다. 코드를 더 길게 쓰는 법이 아니라, 한 번 쓴 코드를 부품처럼 떼어 두었다가 다시 쓰는 법을 다룹니다. 경제학자가 수요 함수 \(Q_d = a - bP\)를 한 번 정의해 두고 여러 시장에 적용하듯이, 우리도 계산 절차를 한 번 정의해 두고 여러 상황에 적용할 것입니다.
- 반복되는 계산에 이름을 붙여
def로 함수를 만들고, 매개변수와return으로 값을 주고받습니다. - 기본값 매개변수를 경제 모형의 파라미터로 읽습니다. 기본값 하나를 바꾸는 일이 곧 다른 시장을 가정하는 일입니다.
- 함수 안의 변수와 밖의 변수가 서로 다른 공간에 산다는 규칙, 즉 스코프(scope)를 이해합니다.
- 딕셔너리로 이름표가 붙은 데이터를 만들고,
get과items로 안전하게 꺼내고 순회합니다. - 함수가 값을 여러 개 돌려줄 때 튜플이 왜 등장하는지 압니다.
- 수요의 가격 탄력성(중간점 공식)과 장바구니 물가지수를 함수로 구현해, “가격을 올리면 매출이 늘까 줄까”라는 질문에 숫자로 답합니다.
5.2 여는 질문: 아메리카노 값을 올리면 매출이 늘까
4장에서 판매 기록을 정리해 본 교내 카페, 이콘 카페로 돌아가 봅시다. 사장님이 고민에 빠졌습니다. 원두값과 우유값이 올라서 아메리카노 가격을 2,000원에서 2,500원으로 올리고 싶은데, 가격을 올리면 손님이 줄어들 게 뻔하기 때문입니다. 잔당 받는 돈은 늘고 파는 잔 수는 줄어든다면, 하루 매출은 늘어날까요, 줄어들까요?
감으로는 답할 수 없는 질문입니다. 답은 손님들이 가격 인상에 얼마나 민감하게 반응하느냐에 달려 있고, 경제학은 그 민감도를 수요의 가격 탄력성이라는 숫자로 요약합니다. 문제는 이 계산을 한 번만 하고 끝나지 않는다는 점입니다. 인상 폭을 300원으로 할 때와 500원으로 할 때, 손님이 많이 줄 때와 조금 줄 때, 시나리오마다 같은 공식을 다시 계산해야 합니다. 4장까지의 방식이라면 같은 코드를 복사해서 붙여 넣고 숫자만 바꾸는 수밖에 없습니다.
이번 장의 주인공인 함수는 정확히 이 문제를 풉니다. 계산 절차를 한 번 정의해 두면, 그다음부터는 숫자만 바꿔 넣으며 몇 번이든 다시 쓸 수 있습니다. 장 후반에서는 탄력성 계산기를 직접 만들어 사장님의 질문에 답하고, 딕셔너리로 이콘 카페의 가격표를 정리해 나만의 물가지수까지 계산해 봅니다.
5.3 그래프 준비
이번 장에서도 후반부에 그래프가 등장합니다. 4장에서 했던 것처럼, 한글 라벨이 깨지지 않도록 폰트 설정 셀을 먼저 실행해 둡니다.
!pip install koreanize-matplotlib
import koreanize_matplotlib구글 Colab에서 실습 중이라면 위의 폰트 이름이 설치되어 있지 않아 한글이 네모 상자로 나올 수 있습니다. 그럴 때는 바로 위 셀의 koreanize-matplotlib 설치 셀을 한 번 실행해 주면 됩니다. 내 컴퓨터(Windows, macOS)에서 실습 중이라면 이 셀은 건너뜁니다.
5.4 같은 코드를 세 번째 복사하고 있다면: 함수
함수가 왜 필요한지부터 몸으로 느껴 봅시다. 이콘 카페 사장님을 도와 가격대별 하루 매출을 계산한다고 합시다. 가격이 2,000원일 때, 2,300원일 때, 2,500원일 때, 각각 예상 판매량을 곱해서 매출을 구합니다. 지금까지 배운 방식대로라면 이렇게 쓰게 됩니다.
# 아메리카노 가격별 하루 매출 -- 함수 없이 계산하면
price = 2000
cups = 300
print(f"가격 {price}원: 하루 매출 {price * cups:,}원")
price = 2300
cups = 250
print(f"가격 {price}원: 하루 매출 {price * cups:,}원")
price = 2500
cups = 180
print(f"가격 {price}원: 하루 매출 {price * cups:,}원")가격 2000원: 하루 매출 600,000원
가격 2300원: 하루 매출 575,000원
가격 2500원: 하루 매출 450,000원
계산 자체는 맞습니다. 문제는 구조입니다. price * cups라는 같은 계산이 세 번 반복되고 있고, 만약 매출 계산법이 바뀌면(예를 들어 부가가치세를 빼야 한다면) 세 군데를 전부 고쳐야 합니다. 한 군데라도 빠뜨리면 찾기 어려운 버그가 됩니다. 시나리오가 세 개가 아니라 서른 개라면 이 방식은 유지할 수 없습니다.
파이썬은 이럴 때 계산 절차에 이름을 붙여 두는 문법을 제공합니다. 함수 정의의 뼈대는 이렇습니다.
def 함수이름(매개변수1, 매개변수2):
"""이 함수가 하는 일을 설명하는 문장 (docstring)"""
# 들여쓰기된 코드 블록: 함수가 할 일
return 돌려줄_값def는 정의한다(define)의 줄임말입니다. 함수 이름은 변수 이름과 같은 규칙으로 짓되, 하는 일이 드러나는 이름이 좋습니다. 백문이 불여일견이니, 방금의 매출 계산을 함수로 바꿔 보겠습니다.
def daily_revenue(price, cups):
"""가격과 판매 잔 수를 받아 하루 매출을 돌려줍니다."""
revenue = price * cups
return revenue
sales = daily_revenue(2000, 300) # 함수 호출: 괄호 안에 재료를 넣습니다
print(f"{sales:,}원")
print(f"{daily_revenue(2500, 180):,}원")600,000원
450,000원
한 줄씩 뜯어보기
첫 줄 def daily_revenue(price, cups):는 “지금부터 daily_revenue라는 이름의 함수를 정의한다”는 선언입니다. 괄호 안의 price와 cups는 매개변수(parameter)라고 부르는데, 함수가 일을 하려면 받아야 하는 재료의 목록입니다. 이 시점에는 아직 아무 값도 들어 있지 않습니다. 재료를 담을 빈 그릇의 이름만 정해 둔 것입니다.
둘째 줄의 큰따옴표 세 개로 감싼 문장은 docstring이라고 하며, 이 함수가 무슨 일을 하는지 설명하는 주석의 일종입니다. 없어도 함수는 돌아가지만, 잠시 뒤에 보듯 파이썬이 이 문장을 특별하게 대접하기 때문에 습관을 들일 가치가 있습니다. 셋째 줄부터는 함수의 몸통입니다. if나 for와 마찬가지로 들여쓰기가 “여기까지가 함수 소속”임을 표시합니다.
return revenue는 계산 결과를 함수 밖으로 돌려주는(반환하는) 문장입니다. 함수는 return을 만나는 순간 일을 마치고, 그 값을 들고 호출한 자리로 돌아갑니다. 마지막으로 daily_revenue(2000, 300)이 실제 호출입니다. 괄호 안에 넣은 실제 값 2000과 300을 인자(argument)라고 부르며, 각각 매개변수 price와 cups에 순서대로 배달됩니다. 함수가 돌려준 값은 sales = ...처럼 변수에 담을 수도 있고, print(...) 안에서 바로 쓸 수도 있습니다.
정의는 한 번, 호출은 몇 번이든. 이것이 함수의 핵심 거래입니다. 이제 매출 계산법이 바뀌어도 함수 안 한 곳만 고치면 됩니다.
화면에 보여주기와 값을 돌려주기는 다릅니다
처음 함수를 배울 때 가장 흔한 혼란이 print와 return의 구분입니다. 둘 다 “결과가 나온다”는 점은 같아 보이지만, 하는 일이 다릅니다. print는 사람이 읽으라고 화면에 글자를 보여줄 뿐이고, return은 다음 계산이 이어받을 수 있도록 값을 건네줍니다. 두 함수를 나란히 놓고 비교해 봅시다.
def show_revenue(price, cups):
"""매출을 화면에 출력만 합니다. (돌려주는 값 없음)"""
print(f"오늘 매출은 {price * cups:,}원입니다.")
def calc_revenue(price, cups):
"""매출을 계산해 값으로 돌려줍니다."""
return price * cups
show_revenue(2500, 180) # 화면에 보여주기만 합니다
sales = calc_revenue(2500, 180) # 값을 돌려받아 변수에 담습니다
print(f"{sales + 50000:,}원") # 돌려받은 값은 이어서 계산할 수 있습니다오늘 매출은 450,000원입니다.
500,000원
show_revenue는 친절해 보이지만 값을 돌려주지 않기 때문에, 그 결과를 받아 세금을 빼거나 한 달 치를 합산하는 후속 계산에는 쓸 수 없습니다. 반면 calc_revenue가 돌려준 값은 방금처럼 + 50000 같은 계산에 바로 이어 붙일 수 있습니다. 모형의 부품이 되려면 함수는 말을 하는 쪽(print)이 아니라 값을 건네주는 쪽(return)이어야 합니다.
실수 노트: 함수가 자꾸 None을 돌려줄 때
return이 없는 함수의 결과를 변수에 담으면 어떻게 될까요? 파이썬은 “돌려줄 것이 없다”는 뜻의 특별한 값None을 대신 건네줍니다. 문제는 그None으로 계산을 이어가려 할 때 터집니다.result = show_revenue(2500, 180) # 화면에는 출력되지만 result에는 None이 담깁니다 print(result + 50000)TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'에러 메시지를 읽어 봅시다. “
NoneType과int사이에는+연산이 지원되지 않는다”는 뜻입니다. 숫자가 있어야 할 자리에None이 있다는 신호이고, 십중팔구 어딘가의 함수가return없이return이 제대로 있는지부터 확인하세요.
기본값 매개변수는 모형의 파라미터입니다
경제학원론에서 수요 함수를 \(Q_d = a - bP\)로 쓸 때, \(P\)는 그때그때 바뀌는 변수이고 \(a\)와 \(b\)는 그 시장의 성격을 결정하는 파라미터입니다. \(a\)는 가격이 0일 때의 수요량(수요의 규모), \(b\)는 가격이 1원 오를 때 수요량이 얼마나 줄어드는지(가격 민감도)를 나타냅니다. 파이썬 함수에는 이 구분을 그대로 옮길 수 있는 문법이 있습니다. 매개변수에 기본값(default)을 정해 두는 것입니다.
def quantity_demanded(price, a=100, b=2):
"""선형 수요 함수 Qd = a - bP 로 주어진 가격에서의 수요량을 계산합니다."""
quantity = a - b * price
return max(0, quantity) # 수요량이 음수가 되지 않도록 0에서 막습니다
print(quantity_demanded(10)) # a, b를 안 넘기면 기본값 100, 2가 쓰입니다
print(quantity_demanded(30))
print(quantity_demanded(60)) # 가격이 너무 높으면 수요는 080
40
0
한 줄씩 뜯어보기
정의부 def quantity_demanded(price, a=100, b=2):에서 price에는 기본값이 없고 a와 b에는 있습니다. 이 배치가 곧 모형의 문법입니다. 가격은 호출할 때마다 반드시 넣어야 하는 변수이고, a와 b는 특별히 말하지 않으면 “기본 시장”의 값 100과 2를 쓰겠다는 파라미터입니다. 기본값이 있는 매개변수는 기본값이 없는 매개변수보다 뒤에 와야 한다는 순서 규칙도 함께 기억해 두세요.
몸통의 max(0, quantity)는 두 값 중 큰 쪽을 고르는 내장 함수입니다. 가격이 60원이면 수식으로는 \(100 - 2 \times 60 = -20\)이지만, 수요량이 음수라는 것은 경제학적으로 말이 되지 않으니 0으로 잘라 줍니다. 수학 공식을 코드로 옮길 때는 이렇게 “공식이 유효한 범위”까지 함께 옮겨야 한다는 것을 보여주는 한 줄입니다.
호출부를 보면 quantity_demanded(10)처럼 가격만 넣었습니다. 파이썬은 빠진 a와 b를 기본값으로 채워서 \(100 - 2 \times 10 = 80\)을 계산합니다.
기본값의 진짜 재미는 필요할 때만 골라서 바꿀 수 있다는 데 있습니다. 호출할 때 b=4처럼 매개변수의 이름을 지목해서 값을 넘기는 방식을 키워드 인자(keyword argument)라고 합니다. 순서에 의존하지 않고 이름으로 전달하기 때문에, 파라미터가 여럿일 때 코드가 훨씬 읽기 쉬워집니다.
print(quantity_demanded(10, 150, 3)) # 위치로 전달: a=150, b=3인 다른 시장
print(quantity_demanded(10, b=4)) # 이름으로 전달: 기울기만 바꾸기
print(quantity_demanded(price=10, a=120)) # 전부 이름으로 전달해도 됩니다120
60
100
세 번의 호출은 각각 다른 시장을 가정한 것과 같습니다. 같은 가격 10원에서도 수요의 규모(\(a\))와 민감도(\(b\))가 다르면 수요량이 달라집니다. 함수 하나를 정의해 놓고 파라미터를 바꿔 가며 여러 시장을 실험하는 것, 이것이 경제 모형을 코드로 다루는 기본 동작입니다.
수요 함수를 만들었으니 같은 요령으로 시장의 나머지 반쪽과 비용 구조도 부품으로 만들어 둡시다. 공급 함수 \(Q_s = c + dP\)는 가격이 오를수록 공급량이 늘어나는 우상향 관계이고, 총비용 \(TC = FC + v \times Q\)는 생산량과 무관한 고정비용에 잔당 가변비용을 더한 것입니다. 이 부품들은 6장에서 시장 균형을 찾을 때 그대로 다시 등장합니다.
def quantity_supplied(price, c=10, d=3):
"""선형 공급 함수 Qs = c + dP 로 주어진 가격에서의 공급량을 계산합니다."""
return c + d * price
def total_cost(quantity, fixed=50000, unit_cost=800):
"""하루 고정비용과 잔당 가변비용으로 총비용을 계산합니다."""
return fixed + unit_cost * quantity
print(f"가격이 10일 때 공급량: {quantity_supplied(10)}")
print(f"아메리카노 180잔을 만드는 총비용: {total_cost(180):,}원")가격이 10일 때 공급량: 40
아메리카노 180잔을 만드는 총비용: 194,000원
docstring은 미래의 나를 위한 사용 설명서
세 함수 모두 첫 줄에 docstring을 달아 두었습니다. 이것이 단순한 주석과 다른 점은, 파이썬이 이 문장을 함수에 딸린 공식 설명서로 보관한다는 것입니다. 내장 함수 help에 함수 이름을 넣으면 언제든 꺼내 볼 수 있습니다.
help(quantity_demanded)Help on function quantity_demanded in module __main__:
quantity_demanded(price, a=100, b=2)
선형 수요 함수 Qd = a - bP 로 주어진 가격에서의 수요량을 계산합니다.
함수 이름, 매개변수 목록과 기본값, 그리고 우리가 쓴 docstring이 그대로 출력됩니다. 한 학기 뒤에 이 노트북을 다시 열었을 때, 혹은 팀 프로젝트에서 동료가 내 함수를 쓸 때, 몸통 코드를 읽지 않고도 help 한 줄로 사용법을 알 수 있습니다. 여러분이 지금까지 써 온 print나 len도 똑같은 방식으로 설명서를 갖고 있습니다. help(len)을 실행해 보면 여러분의 함수와 파이썬 내장 함수가 같은 형식의 설명서를 가진 동급의 부품이라는 것이 보입니다.
[직접 해보기] 해외 직구를 자주 한다면 환율 계산도 부품으로 만들어 둘 만합니다. 아래 셀의 TODO를 채워 보세요.
# [직접 해보기] 달러를 원화로 바꾸는 함수
# TODO 1) usd_to_krw(dollars, rate=1350.0) 함수를 정의하세요.
# 달러 금액에 환율을 곱한 값을 return 합니다.
# TODO 2) 환율을 지정하지 않고 usd_to_krw(100)을 호출해 결과를 출력하세요.
# TODO 3) 환율이 1,420원으로 올랐을 때의 결과를 키워드 인자(rate=1420.0)로 호출해 비교하세요.5.5 함수 안의 변수는 밖에서 보이지 않습니다: 스코프
함수를 쓰기 시작하면 머지않아 이상한 경험을 하게 됩니다. 분명히 함수 안에서 만든 변수인데, 함수 밖에서 부르면 파이썬이 모른다고 하는 것입니다. 직접 확인해 봅시다. 아래 셀은 일부러 오류를 내는 셀이므로 실행하지 않고 눈으로 따라가도 됩니다.
def demand_at(price):
quantity = 100 - 2 * price # quantity는 함수 안에서 태어난 지역 변수
return quantity
print(demand_at(10))
print(quantity) # 함수 밖에서 지역 변수를 부르면?실행하면 첫 print는 80을 출력하지만, 둘째 줄에서 이런 오류가 납니다.
80
NameError: name 'quantity' is not defined
quantity는 함수가 호출되는 동안에만 존재하는 지역 변수(local variable)입니다. 함수가 return으로 일을 마치는 순간 함수 안의 작업 공간은 통째로 정리되고, quantity도 함께 사라집니다. 그래서 밖에서 부르면 “그런 이름은 정의된 적 없다”는 NameError가 나는 것입니다. 함수 안에서 계산한 값을 밖에서 쓰고 싶으면, 방법은 하나뿐입니다. return으로 돌려받아 밖의 변수에 담는 것입니다.
반대 방향은 어떨까요? 함수 안에서 밖의 변수를 읽는 것은 가능합니다.
vat_rate = 0.1 # 함수 밖에서 만든 전역 변수
def price_with_vat(price):
return price * (1 + vat_rate) # 함수 안에서 전역 변수를 읽는 것은 됩니다
print(f"{price_with_vat(2500):,.0f}원")2,750원
함수 밖, 노트북 전체에서 보이는 변수를 전역 변수(global variable)라고 합니다. 함수는 자기 안에서 이름을 못 찾으면 바깥(전역)에서 찾아보기 때문에 vat_rate를 읽을 수 있었습니다. 정리하면 규칙은 비대칭입니다. 안에서 밖은 읽을 수 있지만, 밖에서 안은 보이지 않습니다.
왜 이렇게 만들었을까요? 부품의 독립성 때문입니다. 함수 안에서 어떤 이름을 쓰든 바깥세상과 충돌하지 않아야, 내 함수와 남의 함수를 안심하고 조립할 수 있습니다. 함수 안의 quantity와 다른 함수 안의 quantity가 서로를 침범한다면, 함수를 부품이라 부를 수 없을 것입니다. 매개변수도 지역 변수라는 점을 함께 기억해 두세요. demand_at(price)의 price 역시 함수가 끝나면 사라집니다.
실수 노트: 전역 변수를 고치려다 만나는 UnboundLocalError
읽기가 되니 고치기도 될 것 같지만, 함수 안에서 전역 변수에 값을 대입하려는 순간 일이 꼬입니다. 다음 코드는 누적 매출을 전역 변수에 더하려는 흔한 시도입니다.
monthly_sales = 0 # 이번 달 누적 매출 (전역 변수)
def add_sale(amount):
monthly_sales = monthly_sales + amount # 전역 변수를 고치려는 시도
return monthly_sales
add_sale(2500)실행하면 이런 오류가 납니다. (파이썬 버전에 따라 문구가 조금 다릅니다.)
UnboundLocalError: cannot access local variable 'monthly_sales'
where it is not associated with a value
구버전에서는 local variable 'monthly_sales' referenced before assignment라고 나옵니다. 메시지를 풀면 “지역 변수 monthly_sales를 값이 정해지기도 전에 쓰려 했다”는 뜻입니다. 함수 안에서 monthly_sales = ...라는 대입문을 본 순간, 파이썬은 이 이름을 지역 변수로 취급하기로 결정합니다. 그런데 그 지역 변수의 값을 계산하는 우변에서 아직 만들어지지도 않은 자기 자신을 읽으려 하니 오류가 나는 것입니다.
해결책은 전역 변수를 몰래 고치는 것이 아니라, 함수의 정문으로 드나드는 것입니다. 필요한 값은 인자로 받고, 결과는 return으로 돌려줍니다.
def add_sale(total, amount):
"""지금까지의 누적액과 새 판매액을 받아 새 누적액을 돌려줍니다."""
return total + amount
monthly_sales = 0
monthly_sales = add_sale(monthly_sales, 2500)
monthly_sales = add_sale(monthly_sales, 3200)
print(f"누적 매출: {monthly_sales:,}원")누적 매출: 5,700원
값이 어디서 들어와서 어디로 나가는지가 코드에 전부 드러나기 때문에, 이런 함수는 추적하기 쉽고 어디서든 재사용할 수 있습니다. 파이썬에는 함수 안에서 전역 변수를 강제로 고치게 해 주는 global이라는 키워드도 있지만, 부품의 독립성을 스스로 무너뜨리는 문법이라 이 책에서는 쓰지 않습니다.
5.6 이름표가 붙은 데이터: 딕셔너리
이제 이번 장의 두 번째 부품으로 넘어갑니다. 이콘 카페의 메뉴판을 코드에 담는다고 해 봅시다. 4장에서 배운 리스트로 하면 prices = [2500, 3200, 4200]처럼 되는데, 이 방식에는 약점이 있습니다. prices[1]이 카페라떼였는지 샌드위치였는지를 코드가 아니라 내 기억이 책임져야 한다는 것입니다. 메뉴가 스무 개쯤 되면 기억은 반드시 배신합니다.
리스트가 값을 순서(0번, 1번, …)로 찾는 구조라면, 딕셔너리(dictionary)는 값마다 이름표를 붙여 이름으로 찾는 구조입니다. 사전에서 단어로 뜻을 찾듯이, 메뉴 이름으로 가격을 찾습니다.
menu = {
"아메리카노": 2500,
"카페라떼": 3200,
"샌드위치": 4200,
}
print(menu)
americano = menu["아메리카노"]
print(f"아메리카노 한 잔: {americano:,}원"){'아메리카노': 2500, '카페라떼': 3200, '샌드위치': 4200}
아메리카노 한 잔: 2,500원
한 줄씩 뜯어보기
딕셔너리는 중괄호 {}로 만듭니다. 안에는 "아메리카노": 2500처럼 이름표와 값을 콜론(:)으로 묶은 쌍이 쉼표로 이어집니다. 이름표를 키(key), 그에 딸린 값을 값(value)이라고 부르고, 한 쌍을 키-값 쌍이라고 합니다. 항목이 많을 때는 위처럼 줄을 바꿔 쓰면 메뉴판처럼 읽힙니다.
값을 꺼낼 때는 menu["아메리카노"]처럼 대괄호 안에 키를 넣습니다. 리스트에서 prices[1]이라고 쓰던 자리에 번호 대신 이름이 들어간 것뿐입니다. 꺼낸 값은 여느 값처럼 변수에 담아 계산이나 출력에 쓸 수 있습니다. 키는 대부분 문자열을 쓰지만 숫자도 가능하고, 값 자리에는 숫자, 문자열, 리스트, 심지어 다른 딕셔너리까지 무엇이든 올 수 있습니다.
딕셔너리는 읽기만 하는 구조가 아닙니다. 값을 고치고, 새 항목을 붙이고, 어떤 키가 있는지 물어보는 것도 대괄호와 in 하나로 됩니다.
menu["아메리카노"] = 2300 # 개강 행사: 기존 키에 새 값을 대입하면 수정
menu["쿠키"] = 1500 # 없던 키에 대입하면 새 항목 추가
print(menu)
print("쿠키" in menu) # 4장에서 배운 in: 딕셔너리에서는 키의 존재를 묻습니다
print("아이스티" in menu){'아메리카노': 2300, '카페라떼': 3200, '샌드위치': 4200, '쿠키': 1500}
True
False
대입문 하나가 문맥에 따라 수정도 되고 추가도 된다는 점이 처음에는 낯설 수 있습니다. 규칙은 단순합니다. 키가 이미 있으면 값을 바꾸고, 없으면 새로 만듭니다. in 연산자는 4장에서 리스트의 원소를 확인할 때 썼던 그대로인데, 딕셔너리에 쓰면 값이 아니라 키가 있는지를 확인한다는 점만 다릅니다.
그런데 없는 키의 값을 읽으려고 하면 어떻게 될까요? 손님이 메뉴판에 없는 아이스티를 주문한 상황입니다.
실수 노트: KeyError는 범인의 이름을 알려줍니다
아래 셀처럼 없는 키를 대괄호로 읽으면
KeyError가 납니다. 이 에러의 좋은 점은 메시지에 문제의 키가 그대로 적혀 나온다는 것입니다. 오타인지, 아직 추가하지 않은 항목인지 메시지만 보고 바로 판단할 수 있습니다. 특히"아메리카노 "처럼 눈에 안 보이는 공백이 낀 오타가 단골 범인이니,KeyError를 만나면 따옴표 안을 한 글자씩 의심하세요.
print(menu["아이스티"]) # 메뉴판에 없는 항목을 대괄호로 읽으면?실행하면 마지막 줄에 이렇게 나옵니다.
KeyError: '아이스티'
키가 있을지 없을지 확신할 수 없는 상황이라면, 대괄호 대신 get 메서드를 쓰는 편이 안전합니다. get은 키가 없어도 프로그램을 멈추지 않고 미리 정한 기본값을 돌려줍니다.
print(menu.get("아이스티")) # 키가 없으면 None을 돌려줍니다
print(menu.get("아이스티", 0)) # 둘째 인자로 "없을 때의 값"을 정할 수 있습니다
print(menu.get("쿠키", 0)) # 키가 있으면 원래 값을 그대로 돌려줍니다None
0
1500
menu.get(...)처럼 점을 찍어 부르는 함수를 메서드라고 불렀습니다. 4장에서 리스트에 append를 쓸 때와 같은 문법입니다. 프로그램을 멈추게 두는 대괄호와 조용히 기본값으로 넘어가는 get 중 무엇이 옳은지는 상황에 달렸습니다. 절대 없어서는 안 되는 키라면 시끄럽게 멈추는 쪽이 버그를 일찍 찾아 주고, 없을 수도 있는 키라면 get이 매끄럽습니다.
딕셔너리 한 바퀴: 순회
메뉴판 전체를 훑으며 일을 해야 할 때는 4장의 for가 그대로 통합니다. 딕셔너리를 for에 넣으면 키가 하나씩 나오고, 키와 값을 동시에 받고 싶으면 items() 메서드를 씁니다.
print("-- 키만 돌기 --")
for item in menu:
print(item)
print("-- 키와 값을 같이 돌기 --")
for item, price in menu.items():
print(f"{item}: {price:,}원")
total = 0
for price in menu.values(): # values(): 값들만 차례로
total += price
print(f"메뉴 평균 가격: {total / len(menu):,.0f}원")-- 키만 돌기 --
아메리카노
카페라떼
샌드위치
쿠키
-- 키와 값을 같이 돌기 --
아메리카노: 2,300원
카페라떼: 3,200원
샌드위치: 4,200원
쿠키: 1,500원
메뉴 평균 가격: 2,800원
for item, price in menu.items():는 한 바퀴 돌 때마다 키와 값을 한 쌍씩 꺼내서 두 변수에 나눠 담습니다. 변수 두 개가 쉼표로 나란히 서 있는 이 모양은 잠시 뒤 튜플을 배우면 정체가 밝혀지니 기억해 두세요. values()는 값들만, keys()는 키들만 차례로 내놓는 메서드입니다. 마지막 세 줄은 4장에서 익힌 누적 합계 패턴에 values()를 결합한 것으로, “딕셔너리에 든 숫자들의 통계”는 앞으로도 자주 쓰게 될 조합입니다.
복선: 이 딕셔너리는 곧 표가 됩니다
9장에서 배울 판다스(pandas)의 기본 단위인
Series는 값마다 이름표(인덱스)가 붙은 열입니다. 사실상 오늘 배운 딕셔너리와 같은 구조여서,{"아메리카노": 2300, ...}를 판다스에 넘기면 그대로 표의 한 열이 됩니다. 지금 만드는 가격표와 장바구니 딕셔너리는 몇 주 뒤 여러분이 다룰 데이터 표의 축소판입니다.
5.7 함수와 딕셔너리를 이어 붙이기
두 부품을 각각 익혔으니 이제 이어 붙일 차례입니다. 딕셔너리는 값이므로 함수의 인자로 통째로 넘길 수 있습니다. 서로 관련된 데이터 여러 개를 매개변수 대여섯 개로 일일이 받는 대신, 딕셔너리 하나로 묶어 받는 것입니다.
이콘 카페는 본관점과 도서관점 두 곳을 운영합니다. 지점마다 월간 장부를 딕셔너리로 정리해 두었다면, 장부를 받아 분석하는 함수를 이렇게 만들 수 있습니다.
def analyze_cafe(report):
"""카페 지점의 월간 장부 딕셔너리를 받아 핵심 지표를 출력합니다."""
name = report.get("지점명", "이름 없는 지점")
revenue = report.get("매출", 0)
print(f"[{name}] 월간 리포트")
print(f" 매출: {revenue:,}원")
if "영업이익" in report:
profit = report["영업이익"]
print(f" 영업이익: {profit:,}원")
if revenue > 0:
print(f" 영업이익률: {profit / revenue * 100:.1f}%")
else:
print(" 영업이익 기록이 빠져 있습니다. 장부를 확인해 주세요.")
main_branch = {"지점명": "이콘 카페 본관점", "매출": 12500000, "영업이익": 1875000}
library_branch = {"지점명": "이콘 카페 도서관점", "매출": 8400000} # 영업이익 기록 누락
analyze_cafe(main_branch)
analyze_cafe(library_branch)[이콘 카페 본관점] 월간 리포트
매출: 12,500,000원
영업이익: 1,875,000원
영업이익률: 15.0%
[이콘 카페 도서관점] 월간 리포트
매출: 8,400,000원
영업이익 기록이 빠져 있습니다. 장부를 확인해 주세요.
함수 하나, 장부 두 개. 호출 두 번으로 두 지점의 리포트가 나왔고, 지점이 열 개로 늘어도 호출만 늘리면 됩니다. 눈여겨볼 부분은 방어의 층입니다. get으로 지점명과 매출의 누락에 대비했고, 영업이익처럼 “없으면 없다고 말해야 하는” 항목은 in으로 존재부터 확인했습니다. 실제로 기록이 누락된 도서관점 장부에서도 함수는 멈추지 않고 무엇이 문제인지 알려 줍니다. 현실의 데이터는 늘 어딘가 비어 있기 때문에, 데이터를 받는 함수는 이렇게 의심하며 짜는 습관이 필요합니다.
받을 수 있으면 돌려줄 수도 있어야 합니다. 이번에는 함수가 계산 결과를 딕셔너리로 묶어서 반환하는 방향입니다. 거시경제학의 첫 항등식인 지출 접근법 GDP, \(Y = C + I + G + NX\)를 부품으로 만들어 보겠습니다. 소비(\(C\)), 투자(\(I\)), 정부지출(\(G\)), 순수출(\(NX\))을 받아 합계와 함께 돌려줍니다.
def calculate_gdp_components(consumption, investment, government, net_exports):
"""지출 접근법 Y = C + I + G + NX 로 GDP를 계산해
구성요소와 합계를 딕셔너리로 돌려줍니다."""
total = consumption + investment + government + net_exports
return {
"소비(C)": consumption,
"투자(I)": investment,
"정부지출(G)": government,
"순수출(NX)": net_exports,
"GDP(Y)": total,
}
# 한국의 연간 값과 비슷한 규모의 교육용 근사치 (단위: 조 원)
korea = calculate_gdp_components(1170, 760, 430, 40)
for component, amount in korea.items():
print(f"{component}: {amount:,}조 원")
share = korea["소비(C)"] / korea["GDP(Y)"] * 100
print(f"GDP에서 소비가 차지하는 비중: {share:.1f}%")소비(C): 1,170조 원
투자(I): 760조 원
정부지출(G): 430조 원
순수출(NX): 40조 원
GDP(Y): 2,400조 원
GDP에서 소비가 차지하는 비중: 48.8%
숫자 다섯 개를 각각 다른 변수로 돌려받았다면 호출하는 쪽이 어수선했을 텐데, 딕셔너리 하나로 묶으니 결과 전체가 이름표까지 달고 한 덩어리로 옵니다. 돌려받은 딕셔너리는 방금 배운 대로 items()로 순회할 수도 있고, korea["GDP(Y)"]처럼 필요한 값만 집어 후속 계산에 쓸 수도 있습니다. 소비 비중 약 49%라는 결과는 실제 한국 경제의 감각과도 크게 다르지 않습니다. 뉴스에서 “내수가 살아야 성장이 산다”는 말이 나오는 배경에 이 비중이 있습니다.
[직접 해보기] 딕셔너리 순회를 내 생활 데이터로 연습해 봅시다.
# [직접 해보기] 내 시간표 딕셔너리
# TODO 1) 과목명을 키, 주당 수업 시간을 값으로 하는 딕셔너리 timetable을 만드세요. (3과목 이상)
# TODO 2) items()로 순회하며 "경제학원론: 주 3시간" 형식으로 출력하세요.
# TODO 3) values()와 4장의 누적 합계 패턴(또는 sum 함수)으로 주당 총 수업 시간을 출력하세요.5.8 함수가 여러 값을 돌려줄 때: 튜플
함수를 만들다 보면 곧 이런 상황을 만납니다. 하루 장사를 마감하면서 매출과 이익을 둘 다 알고 싶은데, return은 하나 아니었나? 파이썬의 답은 간단합니다. 쉼표로 나란히 적으면 여러 값을 한 번에 돌려줄 수 있습니다.
def daily_report(price, cups, cost):
"""하루 매출과 이익을 한꺼번에 계산해 돌려줍니다."""
revenue = price * cups
profit = revenue - cost
return revenue, profit # 쉼표로 나란히: 두 값을 한 번에 반환
revenue, profit = daily_report(2500, 180, 300000)
print(f"매출: {revenue:,}원 / 이익: {profit:,}원")매출: 450,000원 / 이익: 150,000원
한 줄씩 뜯어보기
return revenue, profit은 두 번 반환하는 것이 아닙니다. 파이썬은 쉼표로 이어진 값들을 튜플(tuple)이라는 하나의 묶음으로 포장해서 한 번에 돌려줍니다. 튜플은 리스트처럼 순서가 있는 값들의 나열이지만, 한 번 만들면 내용을 바꿀 수 없다는 점이 다릅니다. 보통 괄호 (450000, 150000)으로 표기하는데, 위처럼 문맥이 분명하면 괄호를 생략할 수 있습니다.
받는 쪽의 revenue, profit = daily_report(...)는 언패킹(unpacking)이라고 부르는 문법으로, 묶음으로 도착한 포장을 뜯어 값들을 변수 여러 개에 순서대로 나눠 담습니다. 왼쪽 변수의 개수와 튜플 속 값의 개수가 맞아야 한다는 것이 유일한 규칙입니다. 그리고 조금 전 딕셔너리 순회에서 본 for item, price in menu.items():의 정체가 여기서 밝혀집니다. items()가 내놓는 키-값 쌍 하나하나가 바로 튜플이고, 우리는 매 바퀴 그 튜플을 언패킹하고 있었던 것입니다.
포장을 뜯지 않고 통째로 받으면 튜플의 맨얼굴을 볼 수 있습니다.
result = daily_report(2500, 180, 300000)
print(result) # 괄호로 묶인 튜플
print(type(result))
print(result[0]) # 리스트처럼 인덱스로 접근할 수 있습니다
for pair in menu.items(): # items()의 정체 확인: 키-값 튜플의 나열
print(pair)(450000, 150000)
<class 'tuple'>
450000
('아메리카노', 2300)
('카페라떼', 3200)
('샌드위치', 4200)
('쿠키', 1500)
튜플은 인덱싱과 for 순회 등 리스트에서 배운 읽기 동작을 그대로 지원합니다. 다만 append로 늘리거나 인덱스로 값을 바꾸는 쓰기 동작은 허용하지 않습니다. 아래 셀처럼 값을 바꾸려 하면 오류가 납니다.
result[0] = 999999 # 튜플의 값을 바꾸려고 하면?TypeError: 'tuple' object does not support item assignment
“튜플은 항목 대입을 지원하지 않는다”는 뜻입니다. 언뜻 불편해 보이는 이 성질은 사실 보증서입니다. 함수가 돌려준 결과 묶음이나 (위도, 경도) 같은 좌표처럼 도중에 바뀌면 안 되는 값들은, 바꿀 수 없는 그릇에 담는 편이 안전합니다. 정리하면 이렇습니다. 수정하며 쌓아 가는 데이터는 리스트에, 묶어서 건네는 데이터는 튜플에, 이름으로 찾는 데이터는 딕셔너리에. 자료 구조 선택은 데이터의 쓰임새를 선언하는 일이기도 합니다.
5.9 경제학 응용 1: 수요의 가격 탄력성
부품이 다 모였으니 장 첫머리의 질문으로 돌아갑니다. 이콘 카페가 아메리카노를 2,000원에서 2,500원으로 올리면 매출은 늘어날까요, 줄어들까요? 경제학의 답은 “손님들의 가격 탄력성에 달렸다”입니다. 수요의 가격 탄력성(price elasticity of demand)은 가격이 1% 변할 때 수요량이 몇 % 변하는지를 나타내는 비율입니다.
\[E_d = \frac{\text{수요량 변화율(\%)}}{\text{가격 변화율(\%)}}\]
변화율을 계산할 때 한 가지 기술적 문제가 있습니다. 2,000원에서 2,500원으로 오르면 25% 인상이지만, 2,500원에서 2,000원으로 내리면 20% 인하입니다. 같은 구간인데 방향에 따라 변화율이 달라지는 것입니다. 그래서 경제학원론에서는 변화 전(0)과 후(1)의 중간값을 분모로 쓰는 중간점 공식(midpoint formula)을 사용합니다.
\[E_d = \frac{(Q_1 - Q_0) \,/\, \dfrac{Q_0 + Q_1}{2}}{(P_1 - P_0) \,/\, \dfrac{P_0 + P_1}{2}}\]
수식이 다소 번잡하지만, 함수로 한 번 포장해 두면 그다음부터는 공식을 다시 볼 일이 없습니다. 이것이 함수의 또 다른 쓸모입니다. 복잡함을 상자에 넣고 뚜껑에 이름만 써 두는 것.
def midpoint_elasticity(p0, p1, q0, q1):
"""가격과 수량의 변화 전(0)/후(1) 값으로
수요의 가격 탄력성을 중간점 공식으로 계산합니다."""
pct_q = (q1 - q0) / ((q0 + q1) / 2) # 수요량 변화율 (중간점 기준)
pct_p = (p1 - p0) / ((p0 + p1) / 2) # 가격 변화율 (중간점 기준)
return pct_q / pct_p
# 이콘 카페의 두 가지 시나리오: 2,000원 -> 2,500원 인상
e_sensitive = midpoint_elasticity(2000, 2500, 300, 180) # 손님이 민감: 하루 300잔 -> 180잔
e_loyal = midpoint_elasticity(2000, 2500, 300, 280) # 손님이 둔감: 하루 300잔 -> 280잔
print(f"민감한 손님들의 탄력성: {e_sensitive:.2f}")
print(f"둔감한 손님들의 탄력성: {e_loyal:.2f}")민감한 손님들의 탄력성: -2.25
둔감한 손님들의 탄력성: -0.31
탄력성이 음수로 나오는 것은 정상입니다. 가격이 오르면(+) 수요량이 줄어드는(-) 수요의 법칙이 부호에 그대로 새겨진 것으로, 해석할 때는 관례상 부호를 떼고 절댓값의 크기만 봅니다. 절댓값이 1보다 크면 수요량이 가격보다 크게 반응하는 탄력적(elastic) 수요, 1보다 작으면 둔하게 반응하는 비탄력적(inelastic) 수요, 정확히 1이면 단위탄력적이라고 부릅니다.
이 판정 규칙은 3장에서 배운 if-elif-else 구조 그 자체입니다. 판정도 함수로 만들어 둡시다.
def interpret_elasticity(e):
"""탄력성 값을 받아 경제학 용어로 판정합니다."""
size = abs(e) # abs: 절댓값을 구하는 내장 함수
if size > 1:
return "탄력적"
elif size == 1:
return "단위탄력적"
else:
return "비탄력적"
print(f"민감한 손님들({e_sensitive:.2f}): {interpret_elasticity(e_sensitive)}")
print(f"둔감한 손님들({e_loyal:.2f}): {interpret_elasticity(e_loyal)}")민감한 손님들(-2.25): 탄력적
둔감한 손님들(-0.31): 비탄력적
같은 500원 인상인데 한 시나리오는 탄력적, 다른 시나리오는 비탄력적으로 판정되었습니다. 이제 결정적인 질문입니다. 두 경우에 하루 매출, 즉 총수입(가격 x 판매량)은 각각 어떻게 될까요? 감이 아니라 계산으로 확인해 봅시다.
revenue_before = 2000 * 300 # 인상 전 하루 매출
revenue_sensitive = 2500 * 180 # 인상 후: 탄력적 시나리오
revenue_loyal = 2500 * 280 # 인상 후: 비탄력적 시나리오
print(f"인상 전 하루 매출: {revenue_before:,}원")
print(f"인상 후 (탄력적, E=-2.25): {revenue_sensitive:,}원")
print(f"인상 후 (비탄력적, E=-0.31): {revenue_loyal:,}원")인상 전 하루 매출: 600,000원
인상 후 (탄력적, E=-2.25): 450,000원
인상 후 (비탄력적, E=-0.31): 700,000원
답이 갈렸습니다. 손님들이 탄력적이면 매출은 60만 원에서 45만 원으로 오히려 줄고, 비탄력적이면 70만 원으로 늘어납니다. 우연이 아니라 정확히 탄력성이 예고한 결과입니다. 가격을 올리면 잔당 수입은 늘지만 잔 수가 줄어드는데, 어느 힘이 이기는지를 탄력성이 결정합니다. 수요가 탄력적이면(절댓값이 1보다 크면) 잔 수 감소가 이겨서 총수입이 줄고, 비탄력적이면 가격 인상이 이겨서 총수입이 늡니다.
그래서 사장님에게 드릴 답은 이렇습니다. “가격을 올려도 되는지는 우리 손님들이 얼마나 쉽게 떠나는지에 달렸습니다.” 학교 후문에 카페가 다섯 개 더 있어서 대체재가 많다면 수요는 탄력적일 가능성이 높고, 인상은 악수가 됩니다. 반대로 도서관 안 유일한 카페라면 비탄력적일 것이고, 인상이 남는 장사가 됩니다. 담배에 세금을 무겁게 매겨도 세수가 걷히는 이유, 풍년이 들면 오히려 농가 소득이 줄어드는 역설(풍년의 역설)도 전부 이 한 줄의 논리 위에 서 있습니다.
5.10 경제학 응용 2: 장바구니 물가지수
2장에서 명목 금액을 실질 금액으로 바꿀 때 물가지수로 나눈 것을 기억할 것입니다. 그때 물가지수는 통계청이 주는 숫자를 그대로 받아 썼습니다. 이번에는 그 숫자가 어떻게 만들어지는지를 직접 재현해 봅니다. 원리는 장바구니 하나로 요약됩니다. 내가 사는 물건 묶음(바스켓)을 정해 놓고, 같은 묶음을 사는 데 드는 비용이 기준연도보다 얼마나 늘었는지를 재는 것입니다.
이콘 카페의 단골인 경제학과 1학년 A씨를 관찰해 봅시다. A씨의 한 달 카페 소비는 아메리카노 20잔, 카페라떼 5잔, 샌드위치 8개, 쿠키 4개로 꽤 규칙적입니다. 필요한 데이터는 세 딕셔너리에 담깁니다. 기준연도(A씨가 입학하던 해)의 가격표, 올해의 가격표, 그리고 A씨의 소비 바스켓입니다.
# 이콘 카페의 두 시점 가격표 (단위: 원)
base_prices = {"아메리카노": 2000, "카페라떼": 2800, "샌드위치": 3500, "쿠키": 1500}
this_year_prices = {"아메리카노": 2500, "카페라떼": 3200, "샌드위치": 4200, "쿠키": 1500}
# A씨의 한 달 소비 바스켓 (단위: 개)
basket = {"아메리카노": 20, "카페라떼": 5, "샌드위치": 8, "쿠키": 4}
cost_base = 0
cost_now = 0
for item, quantity in basket.items():
cost_base += base_prices[item] * quantity # 같은 바스켓을 기준연도 가격으로
cost_now += this_year_prices[item] * quantity # 같은 바스켓을 올해 가격으로
print(f"기준연도에 이 바스켓을 사는 비용: {cost_base:,}원")
print(f"올해 같은 바스켓을 사는 비용: {cost_now:,}원")기준연도에 이 바스켓을 사는 비용: 88,000원
올해 같은 바스켓을 사는 비용: 105,600원
같은 장바구니인데 한 달 비용이 88,000원에서 105,600원으로 늘었습니다. 이 두 비용의 비율에 100을 곱한 것이 기준연도 바스켓 방식의 물가지수, 이른바 라스파이레스 지수(Laspeyres index)입니다. 실제 소비자물가지수(CPI)가 이 방식의 확장판입니다.
\[L = \frac{\sum_i p_i^{1} \, q_i^{0}}{\sum_i p_i^{0} \, q_i^{0}} \times 100\]
위첨자 0은 기준연도, 1은 비교 시점, \(\sum_i\)는 바스켓의 모든 품목 \(i\)에 대해 더하라는 뜻입니다. 방금 for 순회로 계산한 것이 정확히 이 두 합계입니다. 이제 이 절차를 함수로 포장합니다. 가격표 둘과 바스켓 하나, 딕셔너리 세 개를 받는 함수입니다.
def laspeyres_index(base_prices, new_prices, basket):
"""기준연도 바스켓 방식(라스파이레스)으로 물가지수를 계산합니다.
basket의 각 품목이 두 가격표에 모두 있어야 합니다."""
cost_base = 0
cost_new = 0
for item, quantity in basket.items():
cost_base += base_prices[item] * quantity
cost_new += new_prices[item] * quantity
return cost_new / cost_base * 100
cafe_index = laspeyres_index(base_prices, this_year_prices, basket)
print(f"A씨의 이콘 카페 물가지수: {cafe_index:.1f}")A씨의 이콘 카페 물가지수: 120.0
지수 120.0. 기준연도를 100으로 놓았을 때 A씨의 카페 물가는 20% 올랐다는 뜻입니다. 참고로 함수의 매개변수 이름 base_prices가 바깥의 전역 변수와 같지만 아무 문제가 없다는 점도 확인해 두세요. 매개변수는 지역 변수라서 함수 안의 base_prices는 호출할 때 넘겨받은 값을 가리킬 뿐, 바깥세상과 충돌하지 않습니다. 스코프 규칙이 부품의 독립성을 지켜 주는 현장입니다.
그런데 20%라는 평균 뒤에는 품목별 편차가 숨어 있습니다. 어떤 품목이 지수를 끌어올렸는지 품목별 상승률을 계산하고, 막대 그래프로 확인해 봅시다.
item_names = []
increase_rates = []
for item, base_price in base_prices.items():
new_price = this_year_prices[item]
rate = (new_price - base_price) / base_price * 100
item_names.append(item)
increase_rates.append(rate)
print(f"{item}: {base_price:,}원 -> {new_price:,}원 ({rate:+.1f}%)")
plt.figure(figsize=(7, 4))
plt.bar(item_names, increase_rates, color="tab:blue")
plt.title("이콘 카페 품목별 가격 상승률 (기준연도 대비)")
plt.ylabel("상승률 (%)")
plt.grid(True, axis="y", linestyle="--", alpha=0.5)
plt.show()아메리카노: 2,000원 -> 2,500원 (+25.0%)
카페라떼: 2,800원 -> 3,200원 (+14.3%)
샌드위치: 3,500원 -> 4,200원 (+20.0%)
쿠키: 1,500원 -> 1,500원 (+0.0%)

그래프가 말하는 경제학 문장 한 줄: “아메리카노가 25%로 가장 많이 올랐고 쿠키는 그대로인데, A씨의 체감 물가가 20%나 오른 것은 가장 많이 오른 품목이 하필 가장 많이 사는 품목이기 때문이다.”
이것이 물가지수의 핵심 성질입니다. 지수는 품목별 상승률의 단순 평균이 아니라 바스켓의 수량으로 가중치를 준 평균입니다. 네 품목의 상승률을 그냥 평균 내면 약 14.8%지만, 아메리카노를 한 달에 20잔 마시는 A씨의 지수는 120.0이 됩니다. 같은 이유로 뉴스의 공식 물가상승률과 내 체감 물가는 얼마든지 다를 수 있습니다. 통계청의 바스켓은 전체 가구의 평균 소비를 반영하는데, 대학생의 바스켓은 커피, 학식, 교통비에 잔뜩 쏠려 있기 때문입니다. 이 장 끝의 미니 프로젝트에서 여러분 자신의 바스켓으로 이 차이를 직접 재 보게 됩니다.
5.11 경제학 응용 3: 콥-더글라스 생산 함수
기본값 매개변수가 모형의 파라미터라는 감각을 한 번 더 다지고 넘어갑시다. 이번 재료는 미시경제학 생산 이론의 대표 선수인 콥-더글라스 생산 함수(Cobb-Douglas production function)입니다.
\[Q = A \, L^{\alpha} K^{1-\alpha}\]
\(L\)은 노동 투입, \(K\)는 자본 투입, \(A\)는 기술 수준, 지수 \(\alpha\)는 생산에서 노동이 기여하는 몫을 나타내는 파라미터입니다. 이콘 카페로 번역하면 \(L\)은 알바생 수, \(K\)는 에스프레소 머신 대수, \(Q\)는 하루에 만들 수 있는 음료 잔 수입니다. 사장님의 실무적 질문은 이것입니다. 머신이 4대로 고정된 지금, 알바생을 한 명씩 더 뽑으면 생산량이 얼마나 늘어날까요? 거듭제곱 연산자 **(1장)와 for 반복(4장)으로 표를 만들어 관찰합니다.
def cobb_douglas(labor, capital=4, tfp=10, alpha=0.3):
"""콥-더글라스 생산 함수 Q = A * L^alpha * K^(1-alpha) 를 계산합니다."""
return tfp * (labor ** alpha) * (capital ** (1 - alpha))
previous = 0
print("알바생 | 하루 생산량(잔) | 한 명 더 뽑아 늘어난 잔 수")
for labor in range(1, 9):
output = cobb_douglas(labor)
marginal = output - previous # 한계생산: 직전 인원 대비 증가분
print(f"{labor:4d}명 | {output:10.1f} | +{marginal:.1f}")
previous = output알바생 | 하루 생산량(잔) | 한 명 더 뽑아 늘어난 잔 수
1명 | 26.4 | +26.4
2명 | 32.5 | +6.1
3명 | 36.7 | +4.2
4명 | 40.0 | +3.3
5명 | 42.8 | +2.8
6명 | 45.2 | +2.4
7명 | 47.3 | +2.1
8명 | 49.2 | +1.9
첫 알바생은 하루 26.4잔을 보태지만, 두 번째는 6.1잔, 여덟 번째는 1.9잔밖에 보태지 못합니다. 머신은 4대 그대로인데 사람만 늘어나니, 나중에 온 알바생일수록 머신이 비기를 기다리는 시간이 길어지는 것입니다. 추가 투입 한 단위가 보태는 생산량이 점점 줄어드는 이 현상이 한계생산 체감(diminishing marginal product)이고, 콥-더글라스 함수에서는 \(\alpha\)가 1보다 작다는 사실에 내장되어 있습니다.
기본값 덕분에 실험도 한 줄이면 됩니다. cobb_douglas(5)는 기본 카페, cobb_douglas(5, capital=8)은 머신을 두 배로 늘린 카페, cobb_douglas(5, tfp=12)는 더 좋은 레시피를 도입한 카페입니다. 파라미터를 바꾼 호출 한 번이 곧 하나의 사고 실험입니다.
5.12 그림으로 확인하기: 함수로 그린 수요 곡선
4장의 첫 그래프는 리스트에 미리 담아 둔 데이터를 그렸습니다. 함수를 배운 지금은 한 걸음 더 나아갈 수 있습니다. 함수로 데이터를 만들어서 그리는 것입니다. quantity_demanded에 여러 가격을 넣어 수요량 리스트를 생성하고, 파라미터 \(b\)가 다른 두 시장을 같은 그림에 겹쳐 보겠습니다. 탄력성 절에서 숫자로 확인한 “민감한 시장과 둔감한 시장의 차이”가 그림에서는 기울기로 나타날 것입니다.
prices = list(range(0, 51, 5)) # 0, 5, 10, ..., 50원
q_gentle = [] # b=2: 가격에 둔감한 시장
q_sensitive = [] # b=4: 가격에 민감한 시장
for p in prices:
q_gentle.append(quantity_demanded(p)) # 기본 파라미터 a=100, b=2
q_sensitive.append(quantity_demanded(p, b=4)) # 기울기만 바꾼 시장
plt.figure(figsize=(8, 5))
plt.plot(prices, q_gentle, marker="o", label="Qd = 100 - 2P (둔감)")
plt.plot(prices, q_sensitive, marker="s", label="Qd = 100 - 4P (민감)")
plt.title("기울기 파라미터가 다른 두 수요 곡선")
plt.xlabel("가격 (P)")
plt.ylabel("수요량 (Qd)")
plt.legend()
plt.grid(True, linestyle="--", alpha=0.5)
plt.show()
그래프가 말하는 경제학 문장 한 줄: “같은 가격 인상이라도 기울기가 가파른(민감한) 시장에서는 수요가 훨씬 크게 줄어든다.”
코드의 구조를 다시 봅시다. 함수가 데이터를 만들고(for 반복 속의 호출), 리스트가 데이터를 모으고(append), matplotlib이 데이터를 그립니다. 함수 정의를 바꾸거나 파라미터만 바꾸면 그래프가 통째로 따라 바뀌는, 모형과 그림이 연결된 구조입니다. 앞으로 이 책의 모든 시각화가 이 패턴 위에서 만들어집니다. 참고로 경제학 교과서는 관례상 가격을 세로축에 두지만, 여기서는 “가격을 넣으면 수요량이 나온다”는 함수의 방향 그대로 가격을 가로축에 두었습니다. 교과서식 배치는 6장에서 수요와 공급을 함께 그릴 때 도입합니다. q_sensitive 곡선이 가격 25원에서 바닥에 닿아 꺾이는 것도 보이는데, max(0, ...)로 막아 둔 안전장치가 그림에 나타난 것입니다.
5.13 연습문제
손풀기 1. 한 달 학식 지출 함수 — 한 끼 가격 price와 이용 횟수 times를 받아 총지출을 반환하는 함수 meal_budget(price, times)를 정의하세요. 학식이 5,500원일 때 한 달 22회 이용한 총지출과, 6,000원으로 오른 뒤의 총지출을 각각 호출해 f-string으로 출력하세요.
손풀기 2. 복리 원리금 함수 — 원금 principal, 연이율 rate, 기간 years를 받아 원리금 \(P(1+r)^n\)을 반환하는 함수 future_value(principal, rate, years)를 정의하되, rate=0.03을 기본값으로 두세요. (1) 100만 원을 5년 맡길 때의 원리금을 기본 이율로, (2) 이율이 5%일 때의 원리금을 키워드 인자로 각각 호출해 출력하세요.
손풀기 3. 편의점 도시락 딕셔너리 — 도시락 3종의 이름과 가격을 담은 딕셔너리를 만들고, (1) 한 종의 가격을 대괄호로 꺼내 출력, (2) 신제품 도시락을 추가, (3) "제육 도시락"이 메뉴에 있는지 in으로 확인해 출력하세요.
응용 1. 공급 함수의 파라미터 실험 — 본문의 quantity_supplied(price, c=10, d=3)를 이용해 가격 0부터 50까지 10 간격으로 공급량을 for로 출력하세요. 이어서 생산 기술이 좋아져 d=5가 된 상황을 키워드 인자로 다시 계산하고, 어느 가격에서 공급량 차이가 가장 큰지 한 줄 주석으로 답하세요.
응용 2. 외화 지갑 평가 함수 — 보유 외화 딕셔너리 wallet = {"USD": 120, "JPY": 3000, "EUR": 45}와 환율 딕셔너리 rates = {"USD": 1350.0, "JPY": 9.2, "EUR": 1480.0}를 인자로 받아 원화 환산 총액을 반환하는 함수 wallet_value(wallet, rates)를 작성하세요. items() 순회와 누적 합계 패턴을 사용하고, 결과를 천 단위 쉼표로 출력하세요.
응용 3. 탄력성 리포트 함수 — 가격과 수량의 변화 전후 값 네 개를 받아 (탄력성 값, 판정 문자열) 튜플을 반환하는 함수 elasticity_report(p0, p1, q0, q1)를 작성하세요. 내부에서 본문의 midpoint_elasticity와 interpret_elasticity를 호출하면 됩니다. 본문의 두 시나리오(300잔에서 180잔, 300잔에서 280잔)를 이 함수로 다시 계산하고, 반환된 튜플을 언패킹해 “탄력성 -2.25, 판정: 탄력적” 형식으로 출력하세요.
도전 1. 빠진 가격에도 무너지지 않는 물가지수 — 본문의 laspeyres_index는 올해 가격표에 품목이 빠져 있으면 KeyError로 멈춥니다. get을 이용해, 올해 가격이 없는 품목은 기준연도 가격이 유지된 것으로 간주하고 계산을 계속하는 버전으로 고치세요. this_year_prices에서 "쿠키"를 뺀 가격표를 만들어 두 버전의 결과를 비교하면 검증이 됩니다.
도전 2. 알바생을 몇 명까지 뽑아야 할까 — 본문의 cobb_douglas를 이용해, 알바생을 1명부터 한 명씩 늘리며 한계생산(직전 대비 증가 잔 수)을 구하다가 그 값이 3잔 아래로 처음 떨어지는 인원을 찾는 코드를 작성하세요. 4장의 while(또는 for와 조건문)을 사용하고, 각 단계의 한계생산을 출력한 뒤 마지막에 “알바생 N명째부터는 한 명이 3잔도 못 보탭니다” 형식의 결론을 출력하세요.
풀이는 부록 C에 있습니다.
5.14 미니 프로젝트: 생각을 코드로, 코드를 글로
과제: 우리 학교 장바구니 물가 (예상 소요 30~60분)
뉴스의 물가상승률과 여러분의 체감 물가는 왜 다를까요? 이번 프로젝트에서는 그 차이를 여러분 자신의 데이터로 직접 잽니다. 통계청이 하는 일을 여러분의 소비 생활에 대해 그대로 재현하는 것입니다.
- 여러분이 한 달에 실제로 지출하는 품목을 5개 고르세요. 학식, 카페 커피, 지하철 요금, 편의점 도시락, OTT 구독료처럼 반복 지출이면 무엇이든 좋습니다.
- 각 품목의 작년 가격과 올해 가격을 두 딕셔너리에 담으세요. 기억이 정확하지 않으면 합리적인 추정치를 쓰되, 어떤 값이 추정인지 글에서 밝히세요.
- 품목별 한 달 소비량을 바스켓 딕셔너리에 담으세요.
- 본문의
laspeyres_index함수로 나만의 물가지수를 계산하세요. - 품목별 가격 상승률을 막대 그래프로 그리세요. 제목과 축 라벨은 필수입니다.
- 결과 아래에 마크다운 셀을 만들어 3문단 글을 쓰세요.
3문단 글쓰기 틀
- 1문단(주장): 계산 결과를 근거로 한 문장의 주장을 세우세요. 예: “나의 체감 물가 상승률은 공식 소비자물가 상승률보다 높으며, 그 차이는 주로 식비 품목에서 나온다.”
- 2문단(근거): 나의 물가지수 값, 품목별 상승률, 바스켓에서 비중이 큰 품목을 수치로 인용하며 주장을 뒷받침하세요. 그래프에서 눈에 띄는 점을 최소 하나 언급하세요. 공식 물가상승률과 비교한다면 어느 기관의 어느 시점 수치인지 밝히세요.
- 3문단(한계와 확장): 이 계산의 한계를 최소 한 가지 스스로 지적하세요. 품목 5개가 전체 소비를 대표하는지, 가격이 오르면 다른 품목으로 갈아타는 대체 행동을 무시한 것은 아닌지(라스파이레스 지수가 물가 상승을 과대평가하기 쉬운 이유입니다), 품질 변화는 반영했는지 등이 후보입니다. 품목을 늘리거나 월별로 추적하는 확장 방향도 한 문장 제안해 보세요.
자기 점검 체크리스트
- 품목 5개의 두 시점 가격과 월 소비량이 모두 딕셔너리로 정리되어 있는가.
- 물가지수 계산이 함수 호출로 이루어져, 다른 바스켓을 넣어도 그대로 작동하는가.
- 그래프에 제목과 축 라벨이 있고, 본문 글에서 그래프의 수치를 직접 인용했는가.
- 1문단의 주장이 한 문장으로 명확하게 서 있는가.
- 3문단에 계산의 한계가 최소 한 가지 구체적으로 적혀 있는가.
# [미니 프로젝트 작업 공간] 우리 학교 장바구니 물가
# TODO 1) last_year, this_year: 품목 5개의 작년/올해 가격 딕셔너리를 만드세요.
# TODO 2) my_basket: 품목별 한 달 소비량 딕셔너리를 만드세요.
# TODO 3) 본문의 laspeyres_index 함수로 나만의 물가지수를 계산해 출력하세요.
# TODO 4) 품목별 상승률을 막대 그래프로 그리세요. (제목, 축 라벨 포함)
# TODO 5) 이 셀 아래에 마크다운 셀을 추가해 3문단 글을 쓰세요.5.15 요약과 다음 장 예고
이번 장에서 우리는 코드를 부품으로 만드는 법을 배웠습니다. 함수는 반복되는 계산 절차에 이름을 붙여 두었다가 필요할 때마다 호출하는 장치이고, 기본값 매개변수는 경제 모형의 파라미터를 코드에 새겨 두는 자리였습니다. 함수 안에서 태어난 변수는 밖에서 보이지 않는다는 스코프 규칙 덕분에 부품들은 서로 간섭하지 않고, 값은 인자와 return이라는 정문으로만 드나듭니다. 딕셔너리는 값마다 이름표를 붙여 저장하는 구조로, 메뉴판과 가격표와 장부처럼 이름으로 찾는 데이터에 꼭 맞았습니다. 함수가 여러 값을 한꺼번에 돌려줄 때는 튜플이 그 값들을 묶어 나른다는 것도 확인했습니다. 이 부품들을 조합해 탄력성 계산기와 장바구니 물가지수, 콥-더글라스 생산 함수를 만들었고, 아메리카노 값을 올리면 매출이 늘지 줄지는 수요의 탄력성이 결정한다는 것을 계산으로 보였습니다.
다음 장은 조립의 시간입니다. 이번 장에서 만든 quantity_demanded와 quantity_supplied를 같은 그림 위에 올려 두 곡선이 만나는 곳, 즉 시장 균형을 찾습니다. 균형 가격을 수식으로 푸는 방법과 컴퓨터답게 반복으로 찾아내는 방법을 나란히 겪어 보고, 파라미터에 충격이 왔을 때 균형이 어디로 움직이는지, 그 과정에서 소비자와 생산자가 각각 얼마나 이득을 보는지까지 추적합니다. 이콘 카페의 아메리카노 시장이 드디어 수요와 공급을 모두 갖추게 됩니다.