1. 모듈과 패키지는 무엇?

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

2. 모듈 만들기

모듈이란 쉽게 말해 파이썬 파일(.py)이다. 어떤 파이썬 파일에 함수와 변수를 만들고 그것들을 다른 파이썬 파일에서 가져다 써보자. 실습을 위해 프로젝트 폴더에 package라는 폴더를 만들고 그 아래 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이라고 하지만 프로그래밍 언어의 문법과는 아무상관이 없고 그냥 예시에서 관습적으로 많이 쓰이는 변수명들을 말한다. foo, bar는 모든 프로그래밍 언어에서 일반적으로 사용되는 이름이고 spam, ham, eggs는 주로 파이썬에서 많이 쓰인다고 한다. 그냥 참고사항으로 알아두자.

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

모듈 이름 그대로 가져오는 방법

import list_ops

foo = [1, 2, 3, 4, 5]
bar = [24, 52, 13, 27]
goo = list_ops.add(foo, bar)
print("{} + {} = {}".format(foo, bar, goo))
# => [1, 2, 3, 4, 5] + [24, 52, 13, 27] = [25, 54, 16, 31]
print("list_ops.spam: {}".format(list_ops.spam))
# => list_ops.spam: [51, 23]
goo = list_ops.multiply(list_ops.spam, list_ops.ham)
print("{} * {} = {}".format(list_ops.spam, list_ops.ham, goo))
# => [51, 23] * [34, 67] = [1734, 1541]

가장 기본적인 방식이지만 가장 덜 쓰이는 방법이다. 모듈에서 어떤 객체를 가져올 때는 모듈명.객체명으로 가져오면 된다. 위 예제에서 더하기 함수와 곱하기 함수가 성공적으로 수행된 것을 확인할 수 있다. 여기서 봐야할 것은 list_ops에서 변수를 가져올 수 있다는 것이다. 얼핏 보면 당연한 것 같지만 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'

모듈 이름을 바꿔서 가져오는 방법

일반적으로 모듈 이름은 모듈의 기능을 이해하기 쉽게 몇 개의 단어를 이어서 길게 짓는것이 보통이다. 게다가 패키지에서 복잡한 폴더 구조 아래 있는 모듈을 가져올 때는 더욱 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))

모듈에서 지정한 객체만 가져오는 방법

아예 외부 모듈명을 쓰지 않고 바로 함수나 변수를 사용하고 싶을 때는 from module import object를 쓰면 된다. from을 써서 가져온 객체는 내부에서 정의한 것과 동일하게 사용가능하다.

from list_ops import add, subtract, spam

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

3. 패키지 만들기

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

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

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

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

def divide(foo, bar):
    out = {}
    for key in foo:
        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(zip(list1, list2))로 두 개의 리스트를 딕셔너리로 만들고 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만 나온 것을 확인할 수 있다.

연습문제

  1. 위에서 만든 list_ops.pydict_ops.py 함수들을 list comprehension, dict comprehension을 이용해 한줄로 구현한 iter_ops라는 패키지를 만들어 보시오.
  2. numeric이란 패키지를 만들고 → 패키지에 prime_number라는 모듈을 만들고 → 모듈에 소수를 확인하는 check_prime_number(numbers) 함수를 구현하시오. 이 함수는 임의의 숫자 리스트를 입력으로 받아서 각 숫자가 소수인지 확인하여 bool의 리스트로 출력한다. (예시: [341, 12, 523, 59] => [False, False, True, True])