모듈과 패키지는 무엇?

지금까지 우리는 하나의 파일에서만 프로그램을 실행해왔지만 실제로 개발을 할 때는 다수의 파이썬 파일을 만들어 유기적으로 연결해야 할 뿐만 아니라 남이 만든 코드도 적극적으로 활용해야 한다. 어떤 파이썬 파일에서 import 키워드를 통해 외부 파이썬 파일의 함수, 변수, 클래스를 가져다 쓸 수 있다. 외부 코드는 기능별로 함수나 클래스 등을 묶어서 파일에 저장하는데 코드 규모가 작으면 하나의 파일에 담을 수도 있고 규모가 크면 여러 파일에 구현한 다음 하나의 폴더에 담을 수도 있다. 이때 각각의 .py 파일을 모듈(Module)이라 하고 여러 모듈을 묶은 폴더를 패키지(Package)라고 한다. 파이썬 오픈소스는 패키지 단위로 설치가 되기 때문에 패키지라는 용어를 자주 사용한다.

모듈 만들기

모듈이란 쉽게 말해 파이썬 파일(.py)이다. 어떤 파이썬 파일에 함수와 변수를 만들고 그것들을 다른 파이썬 파일에서 가져다 쓸 수 있다. 실습을 위해 list_ops.py라는 리스트 연산 모듈을 만들어보자.

# list_ops.py
def add(foo, bar):
    out = []
    for f, b in zip(foo, bar):
        out.append(f + b)
    return out

def subtract(foo, bar):
    out = []
    for f, b in zip(foo, bar):
        out.append(f - b)
    return out

def multiply(foo, bar):
    out = []
    for f, b in zip(foo, bar):
        out.append(f * b)
    return out

def divide(foo, bar):
    out = []
    for f, b in zip(foo, bar):
        out.append(f / b)
    return out

spam = [51, 23]
ham = [34, 67]

if __name__ == '__main__':
    eggs = add(spam, ham)

리스트 원소에 대한 사칙연산 함수 외에 spam, ham, eggs라는 변수를 만들었다. 함수에서 쓰인 foo, bar나 아래서 선언된 변수명들을 프로그래밍에서 Metasyntactic variable 이라고 한다. syntactic이라고 하지만 프로그래밍 언어의 문법과는 아무상관이 없고 그냥 예시에서 관습적으로 많이 쓰이는 변수명들을 말한다.

list_ops.py 옆에 use_list_ops.py라는 새로운 파일을 만들어 저 함수와 변수들을 사용해보자. import를 활용해 외부 모듈을 가져오는 방법은 다양하다.

1. 모듈 이름 그대로 가져오기

foo = [1, 2, 3, 4, 5]
bar = [24, 52, 13, 27]

import list_ops

goo = list_ops.add(foo, bar)
print(f"{foo} + {bar} = {goo}")
goo = list_ops.multiply(list_ops.spam, list_ops.ham)
print(f"{list_ops.spam} * {list_ops.ham} = {goo}"
print("list_ops.spam: {list_ops.spam}")
# => list_ops.spam: [51, 23]

외부 모듈에서 어떤 객체를 가져올 때는 모듈명.객체명으로 가져오면 된다. 위 예제에서 addmultiply 함수가 성공적으로 수행된 것을 확인할 수 있다. 마지막 줄을 보면 import 한 모듈에서 변수를 가져올 수도 있다. 얼핏 보면 당연한 것 같지만 C언어를 생각해보면 당연하지는 않다. list_ops.spam을 프린트 했을 때 [51, 23]이 나온다는 것은 list_ops.py라는 스크립트가 실행되었다는 것을 뜻하기 때문이다. list_ops를 import 만 했지만 내부적으로는 list_ops.py가 실행된 것이다.

list_ops.py에 실행 가능한 코드를 넣되 외부에서 import 할 때는 실행되지 않게 하고 싶다면 함수에서 배웠듯이 __name__ 변수를 써야한다. list_ops.py에서는 eggs라는 변수가 if __name__ == '__main__': 조건문 아래 있기 때문에 외부에서 import 할 때는 선언되지 않는다.

try:
    print("list_ops.eggs: {}".format(list_ops.eggs))
except Exception as e:
    print(e)
    # => module 'list_ops' has no attribute 'eggs'

2. 모듈 이름을 바꿔서 가져오기

일반적으로 모듈 이름은 모듈의 기능을 이해하기 쉽게 몇 개의 단어를 이어서 길게 짓는것이 보통이다. 게다가 패키지에서 복잡한 폴더 구조 아래 있는 모듈을 가져올 때는 더욱 import 문이 길어진다. 하지만 모듈의 객체를 불러올 때마다 긴 모듈 이름을 모두 쓰는 것은 번거로우므로 짧은 별명을 지어 가져올 수 있다. import A as B 라고 하면 A 모듈을 B라는 이름으로 가져오겠다는 뜻이다. 아래 코드에서도 list_ops 대신 lo를 써서 모듈 객체를 불러왔다.

import list_ops as lo

goo = lo.subtract(foo, bar)
print("{} - {} = {}".format(foo, bar, goo))
goo = lo.divide(bar, foo)
print("{} / {} = {}".format(bar, foo, goo))

3. 모듈에서 지정한 객체만 가져오기

아예 외부 모듈명을 쓰지 않고 바로 함수나 변수를 사용하고 싶을 때는 from module import object를 쓰면 된다. from을 써서 가져온 객체는 모듈명을 생략하고 사용가능하다.

from list_ops import add, subtract, spam

goo = add(foo, bar)
print(f"{foo} + {bar} = {goo}")
goo = subtract(bar, foo)
print(f"{bar} - {foo} = {goo}")
print(f"spam = {spam}")

패키지 만들기

패키지는 여러 모듈이 폴더 단위로 모인 것으로 폴더 안에 폴더를 넣으면 계층적인 패키지를 만들 수도 있다. 여러 모듈을 모은 패키지를 만들기 위해 package란 폴더를 만들자. 위에서 만든 list_op.py를 그 아래로 옮기고 폴더 내부에서 dict_ops.py도 다음과 같이 만들어보자. 함수를 만들 때 원래는 함수 이름을 list_add나 dict_add처럼 함수 이름에 기능이 모두 표현되는 것이 좋으나 앞에 붙은 접미사는 모듈명으로 대체 가능하기 때문에 함수 이름을 간단하게 지어도 된다.

# dict_ops.py
def add(foo, bar):
    out = {}
    for key in foo.keys():
        if key in bar:
            out[key] = foo[key] + bar[key]
    return out

def subtract(foo, bar):
    out = {}
    for key in foo.keys():
        if key in bar:
            out[key] = foo[key] - bar[key]
    return out

def multiply(foo, bar):
    out = {}
    for key in foo.keys():
        if key in bar:
            out[key] = foo[key] * bar[key]
    return out

def divide(foo, bar):
    out = {}
    for key in foo.keys():
        if key in bar:
            out[key] = foo[key] / bar[key]
    return out

이제 프로젝트 폴더 바로 아래 새로운 파이썬 파일을 만들고 (package 폴더 옆에) 예제를 실행해보자. 여러 사람의 키와 몸무게를 입력하여 BMI를 계산하는 코드다. 첫 줄을 보면 package 라는 패키지 안의 list_ops라는 모듈을 lo라는 약칭으로 불러왔다. C언어에서 #include를 항상 코드 맨 위에서 쓰듯이 파이썬에서도 import는 그 모듈이 언제 쓰이던지 항상 맨 윗줄에 사용하는 것을 권장한다.

import package.list_ops as lo
import package.dict_ops as do

weights = [65, 90, 42, 76]
heights = [1.65, 1.78, 1.59, 1.80]
heights_sq = lo.multiply(heights, heights)
bmi = lo.divide(weights, heights_sq)
print("BMI:", bmi)
# => BMI: [23.875114784205696, 28.40550435551067, 16.61326688026581, 23.456790123456788]

그런데 알고보니 키와 몸무게를 사람 순서대로 넣은 것이 아니라 임의로 섞여서 들어간 것이었다. 이를 정확히 처리하기 위해 리스트를 딕셔너리로 바꿔서 연산을 해보자. w_namesweights에 해당하는 이름이고 h_names는 heights에 해당하는 이름이다. 이름의 순서와 구성이 다르다는 것을 알 수 있다. 같은 사람의 키와 몸무게를 이용해 BMI를 계산하기 위해 dict_ops를 사용하였다.

w_names = ["RM", "Suga", "Jin", "V"]
h_names = ["Jimin", "RM", "Suga", "Jin"]
weights = dict(zip(w_names, weights))
heights = dict(zip(h_names, heights))
print("dict weights:", weights)
print("dict heightss:", heights)
heights_sq = do.multiply(heights, heights)
bmi = do.divide(weights, heights_sq)
print("BMI:", bmi)
# => BMI: {'RM': 20.515086478979924, 'Suga': 35.59985760056959, 'Jin': 12.962962962962962}

dict_ops는 입력인자 foo, bar에 공통적으로 있는 key에 대해서만 연산을 하여 결과를 출력한다. 예시의 결과를 보면 키와 몸무게에서 이름이 겹치는 RM, Suga, Jin 세 명의 BMI만 나온 것을 확인할 수 있다.