코드 추상화: 모듈

모듈을 활용한 추상화를 예제를 이용하여 설명한다.

모듈식 프로그래밍과 코드 추상화

프로그램과 라이브러리를 모듈 단위로 나누어 관리하는 것을 모듈식 프로그래밍(modular programming)이라 부른다.

파이썬에서 모듈은 간단하게 말하면 하나의 파이썬 소스코드 파일이다. 파일의 확장자가 py이며, 모듈에는 서로 연관된 기능을 수행하는 함수, 상수, 변수, 클래스, 객체 등이 저장된다.

모듈에 포함된 대상을 사용하려면 모듈이름과 함께 점(.)을 다음과 같이 활용한다. 예를 들어, 데이터셋 다루기에서 urllib.request 모듈에 포함된 urlopen 함수를 이용하여 인터넷 상에 있는 파일 내용을 가져왔다. 이 때 사용한 형식은 다음과 같다.

urllib.request.urlopen(fileURL)

이렇게 작성한 이유는 urllib.request 는 모듈이며, 그 안에 urlopen 함수가 정의되어 있기 때문이다. 물론 그 전에 urllib.request 모듈을 아래와 같이 불러왔다.

import urllib.request

모듈을 먼저 불러오지 않으면 모듈 내에서 정의된 대상을 사용할 수 없다.

이렇게 서로 연관된 기능을 수행하는 여러 대상을 모듈로 묶어 놓고 대상의 정의는 알 필요 없이 기능만 알면 사용할 수 있도록 만든다는 의미에서 모듈 또한 코드 추상화 기법으로 사용된다.

모듈 종류

모듈은 크게 세 종류로 나뉜다.

  • 내장 모듈(built-in module): 파이썬에서 기본적으로 제공되는 모듈
    • 파이썬을 설치와 동시에 제공되는 모듈
    • 예제: math, urllib.request, random, turtle, os, sys 등등
  • 제3자 라이브러리 모듈: 제3자에 의해 제공된 라이브러리에 포함된 모듈
    • 제3자가 제공한 라이브러리를 설치할 때 제공되는 모듈
    • 예제: numpy.random, matplotlib.pyplot, pygame.mixer 등등
  • 사용자 정의 모듈: 개인 프로젝트를 진행하면서 작성한 모듈
    • 프로젝트 관리를 위해 사용되는 모듈
    • 예제: 데이터셋 다루기에서 사용한 myWget 함수의 정의가 포함된 파일 wgetFiles.py, 챕터별로 작성한 프로그램을 담은 소스코드 파일 등등

패키지와 라이브러리

패키지는 모듈을 계층적으로 관리할 수 있게 도와주는 체계이다. 보통 관련된 여러 개의 모듈을 하나의 디렉토리(폴더)에 담는 형식으로 패키지를 생성한다. 라이브러리는 하나의 패키지 또는 관련된 패키지의 모음을 가리킨다.

예를 들어, urllib.request 모듈은 urllib 패키지(디렉토리)에 포함되어 있다. 패키지와 모듈은 점(.)으로 구분된다. 또한 간단한 그래프 관련 도구(함수, 클래스, 객체 등)가 정의되어 있는 matplotlib.pyplotmatplotlib 이라는 패키지 않에 포함되어 있으며, 게임 프로그래밍을 위해 가장 많이 사용되는 패키지는 pygame이며, 그 안에서 소리(사운드) 관련 도구가 포함된 모듈이 mixer이다.

__init__.py 파일

특정 디렉토리가 파이썬 패키지임을 명시하려면 디렉토리 안에 __init__.py 라는 파일을 저장해 두어야 한다. 그렇게 하면 파이썬 패키지로서 아래와 같은 기능을 수행하게 만들 수 있다.

  • 파이썬 인터프리터가 다룰 수 있는 패키지의 경로로 지정할 수 있다.
  • 해당 패키지를 불러올 때, 기본적으로 수행하는 일을 지정할 수 있다.

예를 들어, 파이썬 표준 라이브러리에 포함된 tkinter 패키지는 그래픽 유저 인터페이스(GUI)와 관련된 다양한 도구를 제공한다. 그런데 tkinter 패키지를 불러오면 __init__.py 에서 정의된 설정에 따라 다른 모듈을 굳이 불러올 필요 없이 많은 도구들을 바로 사용할 수 있다. tkinter 패키지의 내용은 tkinter 소스코드에서 확인할 수 있다.

주요 예제

카페 두 곳에서 사용하는 POS(Point-Of-Sale)기에서 주문을 받아 계산 영수증을 생성하는 프로그램을 모듈을 활용하여 구현한다.

파이썬 카페

아래 프로그램은 파이썬 카페의 POS기에 사용되는 프로그램이다. 손님이 선택하는 메뉴를 고를 때마다 개수와 가격을 저장한 후에 합산된 가격을 알려주는 동시에 주문 내용과 가격을 receipt.txt 파일에 저장한다.

주의사항

  • 아래 코드는 receipt.txt 파일을 codes 라는 하위폴더에 저장한다.
  • 저장 정보: 선택 메뉴, 개수, 가격 및 합계
In [1]:
# 메뉴와 가격 리스트
itemsPython = {"Donut":2000, "Cafe Latte":4000, "Americano":3500, "Espresso":3000}

# 주문내역을 영수증 파일에 저장하는 함수
def saveReceipt(items, selections, price):
    with open("./codes/receipt.txt", "w") as receipt:
        for item in selections:                             # 선택된 항목 나열
            receipt.write(f'{item:>10}\t{items[item]:5}\t{selections[item]:2}\n')
            
        receipt.write("==========\n")
        receipt.write(f'     Total\t{price}\n')             # 가격 합계 표시

print("점원: 아래 메뉴 중에서 고르세요")

print("============")
for item in itemsPython:
    print(f"{item:>10}: {itemsPython[item]}원")  # 메뉴 오른쪽 정렬
print("============")

# 손님이 주문하기
running = True                                 # 아래 while 문이 실행되는 조건
selected = {}                                  # 선택한 메뉴 및 개수 목록
totalPrice = 0                                 # 가격 합계

while running:
    choice = input("점원: 선택하실래요? ")

    if choice == "N":                           # 더 이상 선택하지 않을 경우
        running = False                        # 주문 종료
        print("점원: 알겠습니다.")
    else:                                      # 목록에 있는 메뉴를 선택한 경우
        count = int(input("점원: 몇 개 드릴까요"))
        selected[choice] = count               # 선택 메뉴 및 개수 추가
        totalPrice += itemsPython[choice] * count    # 가격 합계 계산

# 주문내역 저장하기
saveReceipt(itemsPython, selected, totalPrice)
        
print(f'다 합해서 {totalPrice}원입니다.')
점원: 아래 메뉴 중에서 고르세요
============
     Donut: 2000원
Cafe Latte: 4000원
 Americano: 3500원
  Espresso: 3000원
============
점원: 선택하실래요? Donut
점원: 몇 개 드릴까요2
점원: 선택하실래요? Americano
점원: 몇 개 드릴까요1
점원: 선택하실래요? Espresso
점원: 몇 개 드릴까요1
점원: 선택하실래요? N
점원: 알겠습니다.
다 합해서 10500원입니다.

위 코드의 실행결과로 receipt.txt 파일에 아래 내용으로 저장된다.

In [2]:
with open("./codes/receipt.txt", "r") as receipt:
    for line in receipt:
        print(line,end='')                          # 사용된 서식 그대로 출력
     Donut	 2000	 2
 Americano	 3500	 1
  Espresso	 3000	 1
==========
     Total	10500

아나콘다 커피

아래 프로그램은 아나콘다 커피의 POS에서 사용되는 프로그램이다.

전제조건

  • 파이썬 카페와 아나콘다 커피는 주인이 동일하다.
  • 영수증 내용은 앞서 사용한 receipt.txt 파일을 사용한다.

따라서 아래 프로그램은 메뉴와 가격만이 다를 뿐 나머지는 동일하다.

In [3]:
# 메뉴와 가격 리스트
itemsAnaconda = {"Bagle":2500, "Caffuccino":4000, "Americano":3500, "Affogato":4500}

# 주문내역을 영수증 파일에 저장하는 함수
def saveReceipt(items, selections, price):
    with open("./codes/receipt.txt", "w") as receipt:
        for item in selections:  
            receipt.write(f'{item:>10}\t{itemsAnaconda[item]:5}\t{selections[item]:2}\n')
            
        receipt.write("==========\n")
        receipt.write(f'     Total\t{price}\n') 

print("점원: 아래 메뉴 중에서 고르세요")

print("============")
for item in itemsAnaconda:
    print(f"{item:>10}: {itemsAnaconda[item]}원") 
print("============")

# 손님이 주문하기
running = True                           
selected = {}                            
totalPrice = 0                           

while running:
    choice = input("점원: 선택하실래요? ")

    if choice == "N":                    
        running = False                 
        print("점원: 알겠습니다.")
    else:                               
        count = int(input("점원: 몇 개 드릴까요"))
        selected[choice] = count        
        totalPrice += itemsAnaconda[choice] * count

# 주문내역 저장하기
saveReceipt(itemsAnaconda, selected, totalPrice)
        
print(f'다 합해서 {totalPrice}원입니다.')
점원: 아래 메뉴 중에서 고르세요
============
     Bagle: 2500원
Caffuccino: 4000원
 Americano: 3500원
  Affogato: 4500원
============
점원: 선택하실래요? Bagle
점원: 몇 개 드릴까요1
점원: 선택하실래요? Affogato
점원: 몇 개 드릴까요1
점원: 선택하실래요? Americano
점원: 몇 개 드릴까요1
점원: 선택하실래요? N
점원: 알겠습니다.
다 합해서 10500원입니다.

위 코드의 실행결과로 receipt.txt 파일에 아래 내용으로 저장된다.

In [4]:
with open("./codes/receipt.txt", "r") as receipt:
    for line in receipt:
        print(line,end='')       
     Bagle	 2500	 1
  Affogato	 4500	 1
 Americano	 3500	 1
==========
     Total	10500

코드 중복 피하기

파이썬 카페의 POS와 아나콘다 커피의 POS는 거래내역을 codes 디렉토리에 receipt.txt 파일로 저장한다. 그런데 두 코드는 saveReceipt 함수를 동일하게 사용한다.

모듈 사용

saveReceipt 함수를 codes 디렉토리에 receipt.py라는 파일에 저장하여 두 개의 POS 에서 공유하도록 해보자.

주의사항

  • codes 디렉토리에 __init__.py 파일을 생성하자. 일단은 아무런 내용이 없어도 된다.

receipt.py 모듈의 내용으로 아래 그림과 같이 saveReceipt 함수의 정의만 추가한다.

이제 두 매장에서 사용하는 코드는 아래와 같이 모듈을 불러오는 형식으로 구현될 수 있다.

  • receipt 모듈에 포함되어 있는 모든 함수 불러오기
from codes.receipt import *
  • receipt 모듈에 포함되어 있는 특정 함수만 불러오기
from codes.receipt import saveReceipt

주의사항

만약에 receipt.py 파일이 현재 작업디렉토리에 있거나
프로그래밍 기본 요소: 모듈과 패키지에서 설명되었듯이 codes 디렉토리가 파이썬 라이브러리 경로에 추가되어 있다면 아래와 같이 불러온다.

from receipt import *

또는

from receipt import saveReceipt

여기서는 라이브러리 경로 설정을 하지 않은 방식으로 모듈을 사용한다.

파이썬 카페 프로그램 업그레이드

이제 두 매장에서 사용되는 코드를 좀 더 간단하게 구현할 수 있다.

In [5]:
from codes.receipt import saveReceipt

# 메뉴와 가격 리스트
itemsPython = {"Donut":2000, "Cafe Latte":4000, "Americano":3500, "Espresso":3000}

print("점원: 아래 메뉴 중에서 고르세요")

print("============")
for item in itemsPython:
    print(f"{item:>10}: {itemsPython[item]}원")
print("============")

# 손님이 주문하기
running = True                          
selected = {}                           
totalPrice = 0                          

while running:
    choice = input("점원: 선택하실래요? ")

    if choice == "N":                    
        running = False                 
        print("점원: 알겠습니다.")
    else:                               
        count = int(input("점원: 몇 개 드릴까요"))
        selected[choice] = count        
        totalPrice += itemsPython[choice] * count

# 주문내역 저장하기
saveReceipt(itemsPython, selected, totalPrice)
        
print(f'다 합해서 {totalPrice}원입니다.')
점원: 아래 메뉴 중에서 고르세요
============
     Donut: 2000원
Cafe Latte: 4000원
 Americano: 3500원
  Espresso: 3000원
============
점원: 선택하실래요? Donut
점원: 몇 개 드릴까요1
점원: 선택하실래요? Cafe Latte
점원: 몇 개 드릴까요1
점원: 선택하실래요? N
점원: 알겠습니다.
다 합해서 6000원입니다.
In [6]:
with open("./codes/receipt.txt", "r") as receipt:
    for line in receipt:
        print(line,end='')
     Donut	 2000	 1
Cafe Latte	 4000	 1
==========
     Total	6000

아나콘다 커피 프로그램 업그레이드

In [7]:
from codes.receipt import saveReceipt

# 메뉴와 가격 리스트
itemsAnaconda = {"Bagle":2500, "Caffuccino":4000, "Americano":3500, "Affogato":4500}

print("점원: 아래 메뉴 중에서 고르세요")

print("============")
for item in itemsAnaconda:
    print(f"{item:>10}: {itemsAnaconda[item]}원") 
print("============")

# 손님이 주문하기
running = True                           
selected = {}                            
totalPrice = 0                           

while running:
    choice = input("점원: 선택하실래요? ")

    if choice == "N":                    
        running = False                 
        print("점원: 알겠습니다.")
    else:                               
        count = int(input("점원: 몇 개 드릴까요"))
        selected[choice] = count        
        totalPrice += itemsAnaconda[choice] * count

# 주문내역 저장하기
saveReceipt(itemsAnaconda, selected, totalPrice)
        
print(f'다 합해서 {totalPrice}원입니다.')
점원: 아래 메뉴 중에서 고르세요
============
     Bagle: 2500원
Caffuccino: 4000원
 Americano: 3500원
  Affogato: 4500원
============
점원: 선택하실래요? Caffuccino
점원: 몇 개 드릴까요1
점원: 선택하실래요? Affogato
점원: 몇 개 드릴까요1
점원: 선택하실래요? N
점원: 알겠습니다.
다 합해서 8500원입니다.
In [8]:
with open("./codes/receipt.txt", "r") as receipt:
    for line in receipt:
        print(line,end='')     
Caffuccino	 4000	 1
  Affogato	 4500	 1
==========
     Total	8500

함수 이름 충돌 피하기

멤버십 카드를 가지고 있는 사람들에게 10% 할인혜택을 주고자 한다면 예를 들어 다음 코드를 사용할 수 있다.

def discount_10(price):
    return price * 0.1         # 10% 할인

반면에 특별한 날 모든 손님에게 20% 할인혜택을 주고자 할 때도 가격을 낮추는 함수를 사용할 수 있다.

def discount_20(price):
    return price * 0.2         # 20% 할인

하지만 위와 같이 함수 이름을 매번 달리 하면서 코딩을 하면 여러 모로 불편하다. 무엇보다도 사실상 동일한 일을 수행하는 함수를 매번 다른 이름을 주면 나중에 프로그램을 관리하거나 수정할 때 불편하다.

그렇다고 해서 아래와 같이 동일한 이름을 사용하면 문제가 발생할 수 있다.

def discount(price):
    return price * 0.1         # 10% 할인

def discount(price):
    return price * 0.2         # 20% 할인

위와 같이 하면 discount 함수가 호출될 때 가격을 무조건 20% 할인한다.

이런 문제를 해결하기 위해서는 두 함수를 서로 다른 모듈에 저장하는 것이다. 예를 들어, 멤버십 할인함수는 앞서 언급한 receipt.py 모듈에 저장하고 특별한 날 할인행사용 함수는 codes 디렉토리의 special.py 모듈에 저장하도록 하자.

이제 멤버십 할인과 특별한 날 할인행사를 지원하는 코드를 아래와 같이 작성할 수 있다. special 모듈을 불러올 때 receipt 모듈을 불러오는 방식과는 다른 방식을 사용해야 한다. 즉,

import codes.special

이렇게 하면 special 모듈에 포함된 discount 함수를 호출할 때 모듈이름을 함께 사용해야 한다.

codes.special.discount(price)

별칭 사용

codes.special 처럼 긴 이름 대신에 아래와 같이 별칭을 사용하면 보다 편리해진다.

import codes.special as sale

그러면 모듈 안에 있는 대상을 별칭과 함께 사용해야 한다.

sale.discount(price)

주의: 별칭을 지정했으면 반드시 별칭을 사용해야 한다.

예제

아래 코드는 파이썬 카페에서 도넛을 구입할 때 특별한 날 할인, 멤버십 할인 여부를 확인하여 경우에 따라 동일한 제품을 다른 가격으로 판매되는 것을 보여준다.

주의: 중복할인을 지원하지 않는 코드이다.

특별 할인

In [9]:
# receipt.py 모듈의 모든 함수 불러오기
from codes.receipt import *

# special.py 모듈만 불러오기
import codes.special as sale

# 메뉴와 가격 리스트
itemsPython = {"Donut":2000, "Cafe Latte":4000, "Americano":3500, "Espresso":3000}

print("점원: 아래 메뉴 중에서 고르세요")

print("============")
for item in itemsPython:
    print(f"{item:>10}: {itemsPython[item]}원") 
print("============")

# 손님이 주문하기
running = True                           
selected = {}                            
totalPrice = 0                           

while running:
    choice = input("점원: 선택하실래요? ")

    if choice == "N":                    
        running = False                 
        print("점원: 알겠습니다.")
    else:                               
        count = int(input("점원: 몇 개 드릴까요"))
        selected[choice] = count        
        totalPrice += itemsPython[choice] * count

# 할인 추가 (중복할인 없음)
if input("특별한 날 확인: ") == "Y":
    dc = int(sale.discount(totalPrice))
elif input("점원: 멤버십 카드 있으세요? ") == "Y":
    dc = int(discount(totalPrice))

# 주문내역 저장하기
saveReceipt(itemsPython, selected, totalPrice-dc)
        
print(f'다 합해서 {totalPrice}원에서 {dc}원 할인된 {totalPrice-dc}원입니다.')
점원: 아래 메뉴 중에서 고르세요
============
     Donut: 2000원
Cafe Latte: 4000원
 Americano: 3500원
  Espresso: 3000원
============
점원: 선택하실래요? Donut
점원: 몇 개 드릴까요1
점원: 선택하실래요? Americano
점원: 몇 개 드릴까요2
점원: 선택하실래요? Cafe Latte
점원: 몇 개 드릴까요1
점원: 선택하실래요? N
점원: 알겠습니다.
특별한 날 확인: Y
다 합해서 13000원에서 2600원 할인된 10400원입니다.
In [10]:
with open("./codes/receipt.txt", "r") as receipt:
    for line in receipt:
        print(line,end='')       
     Donut	 2000	 1
 Americano	 3500	 2
Cafe Latte	 4000	 1
==========
     Total	10400

멤버십 할인

In [11]:
# receipt.py 모듈의 모든 함수 불러오기
from codes.receipt import *

# special.py 모듈만 불러오기
import codes.special as sale

# 메뉴와 가격 리스트
itemsPython = {"Donut":2000, "Cafe Latte":4000, "Americano":3500, "Espresso":3000}

print("점원: 아래 메뉴 중에서 고르세요")

print("============")
for item in itemsPython:
    print(f"{item:>10}: {itemsPython[item]}원") 
print("============")

# 손님이 주문하기
running = True                           
selected = {}                            
totalPrice = 0                           

while running:
    choice = input("점원: 선택하실래요? ")

    if choice == "N":                    
        running = False                 
        print("점원: 알겠습니다.")
    else:                               
        count = int(input("점원: 몇 개 드릴까요"))
        selected[choice] = count        
        totalPrice += itemsPython[choice] * count

# 할인 추가 (중복할인 없음)
if input("특별한 날 확인: ") == "Y":
    dc = int(sale.discount(totalPrice))
elif input("점원: 멤버십 카드 있으세요? ") == "Y":
    dc = int(discount(totalPrice))

# 주문내역 저장하기
saveReceipt(itemsPython, selected, totalPrice-dc)
        
print(f'다 합해서 {totalPrice}원에서 {dc}원 할인된 {totalPrice-dc}원입니다.')
점원: 아래 메뉴 중에서 고르세요
============
     Donut: 2000원
Cafe Latte: 4000원
 Americano: 3500원
  Espresso: 3000원
============
점원: 선택하실래요? Cafe Latte
점원: 몇 개 드릴까요1
점원: 선택하실래요? Espresso
점원: 몇 개 드릴까요1
점원: 선택하실래요? Americano
점원: 몇 개 드릴까요1
점원: 선택하실래요? N
점원: 알겠습니다.
특별한 날 확인: N
점원: 멤버십 카드 있으세요? Y
다 합해서 10500원에서 1050원 할인된 9450원입니다.
In [12]:
with open("./codes/receipt.txt", "r") as receipt:
    for line in receipt:
        print(line,end='')             
Cafe Latte	 4000	 1
  Espresso	 3000	 1
 Americano	 3500	 1
==========
     Total	9450

연습문제

  1. POS기에 사용되는 프로그램을 아래 언급한 두 함수를 이용하여 보다 효율적인 코드로 구현하라. 단, 두 함수는 codes.receipt 모듈에서 정의되어야 한다.

    1. orderAccept 함수의 명세
      • 하나의 인자 사용.
        • 인자: 사전 자료형으로 구성된 메뉴(메뉴 항목과 가격)
      • 호출하면 먼저 메뉴 항목 가격과 함께 나열해서 보여줌.
      • 고객으로부터 주문받기.
      • 주의: 주문 합산가격 다루지 말 것.
      • 리턴값: 사전 자료형으로 정리된 주문 내용(메뉴 항목과 개수)

    2. saveReceipt 함수의 명세
      • 네 개의 인자 사용.
        • 첫째 인자: 사전 자료형으로 구성된 메뉴(메뉴 항목과 가격)
        • 둘째 인자: 사전 자료형으로 구성된 선택 메뉴(메뉴 항목과 개수)
        • 셋째 인자: 특별할인 여부
        • 넷째 인자: 멤버십할인 여부
      • 중복할인 없음.
      • 기존처럼 ./codes/receipt.txt 파일에 주문내역 저장하는 기능 유지.
      • 얼마 할인되었는지도 영수증에 명시.
      • 리턴값: 주문 합산가격과 할인값으로 구성된 튜플

    3. orderAccept 함수를 아래 명세를 만족하도록 한 번 더 수정하라.
      • 주문받을 때 오타 또는 메뉴에 없는 항목을 입력하면 다시 입력하는 것을 요구해야 함.
      • 힌트: if 조건문 또는 예외처리 기능을 활용.