CODICT

4. Python의 모듈, 패키지, 프로그램 본문

Programming/Python

4. Python의 모듈, 패키지, 프로그램

Foxism 2020. 2. 4. 08:36

지금까지 데이터 타입에서부터 큰 데이터와 자료 구조를 구축하기까지, 파이썬을 상향식으로 접근했습니다. 이번 게시글에서는 수련한 여러분들이 하산하기 전 마지막 스테이지로, 현실적이고 큰 프로그램을 작성하는 방법에 대해 서술할 계획입니다.


1. 스탠드얼론 프로그램

 

지금까지는 파이썬의 대화식 인터프리터에서 코드를 작성하고 실행했습니다.

>>> print("This interactive snippet works.")
#This interactive snippet works.

이제 첫 번째 스탠드얼론(Standalone) 프로그램을 만들어보겠습니다. 컴퓨터에 test.py 파일을 생성하고, 다음 파이썬 코드를 입력합니다.

print("This standalone program works!")

파이썬 코드 줄에 >>> 프롬프트가 없습니다. 그리고 print 앞에서 스페이스를 눌러 들여쓰기하면 안됩니다.

텍스트 터미널 혹은 터미널 창에서 파이썬을 실행할 때, 다음과 같이 파이썬 프로그램의 파일을 입력합니다.

$ python test.py
#This standalone program works!

참고로, 이 블로그에서 지금까지 대화식 인터프리터에서 실행한 코드를 파일로 저장한 뒤 직접 실행할 수 있습니다. 대화식 인터프리터의 코드를 복사한 후 붙여넣기를 할 때, >>> 등을 지우는 것에 유의하세요.

사실 그냥 IDLE창에서 ctrl + n 을 눌러 파일을 작성하거나, 별도의 IDE를 사용하여 컴파일을 하는 것이 본 방법보다 훨씬 편하긴 합니다.

 

2. 모듈과 import문

 

여러 개의 파일에 파이썬 코드를 작성해보겠습니다. 모듈(Module)은 단지 파이썬 코드의 파일입니다. 이 글은 단어, 문장, 단락, 장의 계층구조가 존재합니다. 이러한 계층구조가 없다면 글을 읽기 힘들 것입니다. 코드도 마찬가지입니다. 단어는 데이터 타입(자료형), 문장은 선언문, 단락은 함수, 장은 모듈에 비유할 수 있습니다.

import 문을 사용하여 다른 모듈의 코드를 참조합니다. 이것은 임포트한 모듈의 코드와 변수를 프로그램에서 사용할 수 있게 만들어줍니다.

 

2.1. 모듈 임포트하기

 

import 문을 사용하여 간단하게 모듈을 임포트할 수 있습니다. 모듈은 .py 확장자가 없는 파이썬 파일의 이름입니다. 기상 관측소와 날씨 리포트를 시뮬레이션해보겠습니다. 메인 프로그램은 리포트를 출력합니다. 그리고 함수가 있는 또 다른 모듈에서는 리포트에 사용되는 날씨 정보를 반환합니다.

다음은 메인 프로그램입니다(weatherman.py).

import report

description = report.get_description()
print("Today's weather:", description)

그리고 다음은 모듈입니다(report.py).

def get_description():
    from random import choice
    possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
    return choice(possibilities)

두 파일을 같은 디렉터리에 저장하고, 메인 프로그램인 weatherman.py를 실행하면 메인프로그램은 report 모듈에 접근해서 get_description() 함수를 실행합니다. 이 함수는 문자열 리스트로부터 임의의 결과를 반환받고, 메인 프로그램에서 그 결과를 출력합니다.

위 코드에서는 import 문을 두 번 사용했습니다.

  • 메인 프로그램 weatherman.py 에서 report 모듈을 임포트했습니다

  • 모듈 파일 report.py의 get_description() 함수에서 파이썬의 표준 random 모듈로부터 choice 함수를 임포트했습니다.

또한 두 가지 방법으로 임포트를 사용했습니다.

  • 메인 프로그램에서 import report를 호출하고, report.get_description()을 실행했습니다.

  • report.py의 get_description() 함수에서 from random import choice를 호출하고, choice(possibilities)를 실행했습니다.

첫 번재 경우에는 report 모듈 전체를 임포트했지만, get_description() 앞에 report.를 붙였습니다. import 문 이후에, report.py에 있는 모든 객체는 이름 앞에 report.를 붙여 메인 프로그램에서 사용할 수 있습니다. 모듈 이름으로 내용을 한정하고 정리함으로써 다른 모듈 간의 불필요한 네이밍 충돌을 피해야합니다. get_description() 함수가 다른 모듈에 있을 수도 있지만, 첫 번째 경우에는 실수로 다른 모듈의 함수를 호출하지 않았습니다.

두 번째 경우에는 함수 안에서 random 모듈로부터 choice()를 바로 임포트했습니다. 임의의 결과를 반환하는 이 함수를 다음과 같이 작성할 수도 있습니다.

def get_description():
    import random
    possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
    return random.choice(possibilities)

프로그래밍의 다양한 측면에서 어떤 스타일을 선택할 때는 자신에게 맞는 가장 분명한 것을 선택합니다. 모듈의 이름을 붙이는 것(random, choice)이 좀 더 안전하지만, 입력이 좀 귀찮아집니다.

get_description() 코드에서는 임포트를 어떻게 하는지 살펴봤습니다. 그러나 모듈의 함수 내에서만 import 문을 호출했기 때문에 어디서 임포트해야 할지에 대해서는 살펴보지 못했습니다. random 모듈을 함수 밖에서 임포트할 수도 있습니다.

>>> import random
>>> def get_description():
    import random
    possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
    return random.choice(possibilities)

>>> get_description()
#'fog'
>>> get_description()
#'sleet'

임포트된 코드가 여러 장소에서 사용되는 경우, import 문을 함수 밖으로 빼내는 것도 고려해야 합니다. 그리고 임포트된 코드의 사용이 내부에만 제한되는 경우, import 문을 내부에 놓습니다. 몇몇 사람은 코드의 모든 의존성을 명시하기 위해 모든 import 문을 파일의 맨 앞에 두는 것을 선호합니다. 어떤 방법을 사용하든 동작합니다.

 

2.2. 다른 이름으로 모듈 임포트하기

 

weatherman.py 프로그램에서 import report를 호출했습니다. 그런데 만약 같은 이름을 사용하는 다른 모듈이 있다면? 혹은 모듈의 이름을 조금 더 짧게 쓰고 싶다면? 이런 경우 앨리어스(Alias)를 사용합니다. report 모듈을 앨리어스 wr을 사용해서 임포트하겠습니다.

import report as wr
description = wr.get_description()
print("Today's weather:", description)

 

2.3. 필요한 모듈만 임포트하기

 

모듈 전체 혹은 모듈의 필요한 부분만 임포트할 수 있습니다. 모듈 각 부분에서 원래 이름이나 앨리어스를 사용할 수 있습니다. 다음은 원래 이름을 사용해서 report 모듈의 get_description()을 임포트한 것입니다.

from report import get_description
description = get_description()
print("Today's weather:", description)

다음은 do_it이라는 앨리어스를 사용해서 임포트한 것입니다.

from report import get_description as do_it
description = do_it
print("Today's weather:", description)

 

2.4. 모듈 검색 경로

 

파이썬은 임포트할 파일을 어디에서 찾을까요? 디렉터리 이름의 리스트와 표준 sys 모듈에 저장되어 있는 ZIP 아카이브 파일을 변수 path로 사용합니다. 이 리스트를 접근하여 수정할 수 있습니다. 다음은 제가 사용하는 컴퓨터에 설치된 파이썬 3.7에 대한 sys.path의 값입니다.

>>> import sys
>>> for place in sys.path:
	print(place)

	

#C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\Lib\idlelib
#C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\python37.zip
#C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\DLLs
#C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\lib
#C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64
#C:\Users\|유저명|\AppData\Roaming\Python\Python37\site-packages
#C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\lib\site-packages

전 Microsoft Visual Studio를 설치할 때 같이 설치해서 저렇게 나온듯 싶습니다.

첫 번째 라인의 공백은 현재 디렉터리를 뜻하는 빈 문자열입니다. 이와 같이 빈 문자열이 sys.path의 첫 번째 라인에 있다면, 파이썬은 임포트할 파일을 현재 디렉터리에서 먼저 찾습니다. import report는 report.py를 찾습니다.

중복된 이름의 모듈이 있다면 첫 번째 조건을 사용합니다. 이 말은 우리가 random이라는 모듈을 정의하고, 이 모듈이 표준 라이브러리를 찾기 전의 검색 경로에 있다면 표준 라이브러리의 random 모듈에 접근할 수 없다는 의미입니다.

 

3. 패키지

 

우리는 한 라인의 코드부터 여러 개의 함수, 스탠드얼론 프로그램, 같은 디렉터리의 여러 모듈까지 배웠습니다. 파이썬 애플리케이션을 좀 더 확장 가능하게 만들기 위해 모듈을 패키지(Package)라는 파일 계층구조에 구성할 수 있습니다.

한 번은 다음 날, 또 한 번은 다음 주와 같이 다른 타입의 날씨 정보가 필요하다고 하겠습니다. 코드를 구성하는 한 가지 방법은 sources 디렉터리를 만든 뒤 daily.py와 weekly.py라는 두 개의 모듈을 생성하는 것입니다. 각 모듈은 forecast 함수를 갖습니다. daily 버전은 한 문자열을 반환하고, weekly 버전은 일곱 개의 문자열이 담긴 리스트를 반환합니다.

아래에 메인 프로그램과 두 개의 모듈이 있습니다(for 문에서 enumerate() 함수는 리스트와 리스트 항목의 시작 인덱스를 취합니다. 각 리스트의 항목의 인덱스는 1씩 증가합니다).

다음은 메인 프로그램입니다(test/weather.py).

from sources import daily, weekly

print("Daily forecase:", daily.forecast())
print("Weekly forecast:")
for number, outlook in nenumerate(weekly.forecast(), 1):
    print(number, outlook)

다음은 첫 번째 모듈입니다(test/sources/daily.py).

def forecast():
    'fake daily forecast'
    return 'like yesterday'

다음은 두 번째 모듈입니다(test/sources/weekly.py).

def forecast():
    """Fake weekly forecast"""
    return ['snow', 'mores snow', 'sleet', 'freezing rain', 'rain', 'fog', 'hail']

sources 디렉터리에 한 가지 더 필요한 것이 있는데, __init__.py 파일입니다. 이 파일의 내용은 비워도 되지만, 파이썬은 이 파일을 포함하는 디렉터리르 패키지로 간주합니다.

메인 프로그램 weather.py를 실행하여 결과를 봅시다.

$ python weather.py
#Daily forecase: like yesterday
#Weekly forecast:
#1 snow
#2 mores snow
#3 sleet
#4 freezing rain
#5 rain
#6 fog
#7 hail

 

4. 파이썬 표준 라이브러리

 

파이썬의 장점 중 하나는 배터리 포함(Batteries Included)이라는 철학으로 유용한 작업을 처리하는 많은 표준 라이브러리 모듈이 있다는 것입니다. 그리고 이 모듈은 핵심 코드가 늘어나는 것을 피하기 위해 분리되어 있습니다. 파이선 코드를 작성할 때에는 원하는 기능이 표준 모듈에 있는지 먼저 확인하는 것이 좋습니다. 표준 라이브러리에는 수많은 기능들이 있습니다. 또한 파이썬은 튜토리얼과 함께 모듈에 대한 공식 문서를 제공합니다.

이번 절에서는 제네릭(Generic)을 사용할 수 있는 일부 모듈에 대해 살펴봅니다.

 

4.1. 누락된 키 처리하기: setdefault(), defaultdict()

 

존재하지 않는 키로 딕셔너리에 접근하려 하면 예외가 발생합니다. 기본값을 반환하는 딕셔너리의 get() 함수를 사용하면 이 예외를 피할 수 있습니다. setdefault() 함수는 get() 함수와 같지만, 키가 누락된 경우 딕셔너리에 항목을 할당할 수 있습니다.

>>> periodic_table = {'Hydrogen': 1, 'Helium': 2}
>>> print(periodic_table)
#{'Hydrogen': 1, 'Helium': 2}

딕셔너리에 키가 없는 경우 새 값이 사용됩니다.

>>> carbon = periodic_table.setdefault('Carbon', 12)
>>> carbon
#12
>>> periodic_table
#{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

존재하는 키에 다른 값을 할당하려 하면 키에 대한 원래 값이 반환되고 아무것도 바뀌지 않습니다.

>>> helium = periodic_table.setdefault('Helium', 947)
>>> helium
#2
>>> periodic_table
#{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

defaultdict()함수도 비슷합니다. 다른 점은 딕셔너리를 생성할 때 모든 새 키에 대한 기본값을 먼저 지정한다는 것입니다. 이 함수의 인자는 함수입니다. 다음 코드에서 int() 함수를 호출하고 정수 0을 반환하는 함수 int를 전달해 보겠습니다.

>>> from collections import defaultdict
>>> periodic_table = defaultdict(int)

이제 모든 누락된 기본값은 0입니다.

>>> periodic_table['Hydrogen'] = 1
>>> periodic_table['Lead']
#0
>>> periodic_table
#defaultdict(<class 'int'>, {'Hydrogen': 1, 'Lead': 0})

defaultdict()의 인자는 값을 누락된 키에 할당하여 반환하는 함수입니다. 다음 예제에서 no_idea() 함수는 필요할 때 값을 반환하기 위해 실행됩니다.

 

빈 기본값을 반환하기 위해 int() 함수는 정수 0, list() 함수는 빈 리스트, dict() 함수는 빈 딕셔너리를 반환합니다. 인자를 입력하지 않으면 새로운 키의 초깃값이 None으로 설정됩니다.

 

4.2. 항목 세기: Counter()

 

표준 라이브러리에는 항목을 셀 수 있는 함수가 여러 개 있습니다.

>>> from collections import Counter
>>> breakfast = ['spam', 'spam', 'eggs', 'spam']
>>> breakfast_counter = Counter(breakfast)
>>> breakfast_counter
#Counter({'spam': 3, 'eggs': 1})

most_common() 함수는 모든 요소를 내림차순으로 반환합니다. 혹은 숫자를 입력하는 경우, 그 숫자만큼 상위 요소를 반환합니다.

>>> breakfast_counter.most_common()
#[('spam', 3), ('eggs', 1)]
>>> breakfast_counter.most_common(1)
#[('spam', 3)]

카운터를 결합할 수 있습니다. 먼저 breakfast_counter를 다시 한 번 봅시다.

>>> breakfast_counter
#Counter({'spam': 3, 'eggs': 1})

이번에는 lunch 리스트와 lunch_counter 카운터를 만듭니다.

>>> lunch = ['eggs', 'eggs', 'bacon']
>>> lunch_counter = Counter(lunch)
>>> lunch_counter
#Counter({'eggs': 2, 'bacon': 1})

+ 연산자를 사용해서 두 카운터를 결합할 수 있습니다.

>>> breakfast_counter + lunch_counter
#Counter({'spam': 3, 'eggs': 3, 'bacon': 1})

- 연산자를 사용해서 한 카운터에서 다른 카운터를 뺄 수 있습니다. lunch_counter를 뺀 breakfast_counter는 무엇일까요?

>>> breakfast_counter - lunch_counter
#Counter({'spam': 3})

그 반대는 무엇일까요?

>>> lunch_counter - breakfast_counter
#Counter({'eggs': 1, 'bacon': 1})

지난 게시글에서 설명한 셋(set)과 마찬가지로, 인터섹션 연산자 &를 사용해서 공통된 항목을 얻을 수 있습니다.

>>> breakfast_counter & lunch_counter
#Counter({'eggs': 1})

인터섹션 연산으로 낮은 숫자의 공통 항목인 'eggs'를 선택했습니다. breakfast_counter에서 'eggs'가 1개이기 대문에 공통 항목으로 1이 나오는 것이 맞습니다.

유니온 연산자 |를 사용하면 모든 항목을 얻을 수 있습니다.

>>> breakfast_counter | lunch_counter
#Counter({'spam': 3, 'eggs': 2, 'bacon': 1})

'eggs'가 다시 두 카운터의 공통 항목으로 나왔습니다. 유니온 연산에서 breakfast_counter의 'eggs'가 추가되지 않았습니다. 대신 높은 숫자의 공통 항목을 선택합니다.

 

4.3. 키 정렬하기: OrderedDict()

 

딕셔너리의 키 순서는 예측할 수 없습니다. 딕셔너리의 키값을 a, b, c 순으로 추가하더라도, key() 함수는 아마 c, a, b를 반환할 것입니다. 이전의 사용한 것과는 다른 용도의 예제를 살펴봅시다.

>>> quotes = {
	'Moe': 'A wise guy, huh?',
	'Larry': 'Ow!',
	'Curly': 'Nyuk nyuk!',
	}
>>> for stooge in quotes:
	print(stooge)

	
#Larry
#Curly
#Moe

OrderedDict() 함수는 키의 추가 순서를 기억하고, 이터레이터로부터 순서대로 키값을 반환합니다. (키, 값) 튜플의 시퀀스로부터 OrderedDict를 생성해보겠습니다.

>>> quotes = OrderedDict([
	('Moe', 'Awise guy, huh?'),
	('Larry', 'Ow!'),
	('Curly', 'Nyuk nyuk!'),
	])
>>> for stooge in quotes:
	print(stooge)

	
#Moe
#Larry
#Curly

 

4.4. 스택 + 큐 == 데크

 

데크(Deque)는 스택과 큐의 기능을 모두 가진 출입구가 양 끝에 있는 큐입니다. 데크는 시퀀스의 양 끝으로부터 항목을 추가하거나 삭제할 때 유용하게 쓰입니다. 여기에서 회문(앞에서 읽으나 뒤에서 읽으나 똑같은 단어)인지 확인하기 위해 양쪽 끝에서 중간까지 문자를 확인합니다. popleft() 함수는 데크로부터 왼쪽 끝의 항목을 제거한 후, 그 항목을 반환합니다. pop() 함수는 오른쪽 끝의 항목을 제거한 후, 그 항목을 반환합니다. 양쪽 끝에서 이 두 함수가 중간 지점을 향해서 동작합니다. 양쪽 문자가 서로 일치한다면 단어 중간에 도달할 때까지 데크를 팝(Pop) 합니다.

>>> def palindrome(word):
	from collections import deque
	dq = deque(word)
	while len(dq) > 1:
		if dq.popleft() != dq.pop():
			return False
	return True

>>> palindrome('a')
#True
>>> palindrome('racecar')
#True
>>> palindrome('')
#True
>>> palindrome('rader')
#False
>>> palindrome('halibut')
#False

데크의 간단한 예제를 살펴보았습니다. 회문 코드를 더 간단하게 작성하고 싶다면 한 문자열을 반전해서 비교하면 됩니다. 파이썬은 문자열에 대한 reverse() 함수가 없지만, 다음과 같이 슬라이스로 문자열을 반전할 수 있습니다.

>>> def another_palindrome(word):
	return word == word[::-1]

>>> another_palindrome('radar')
#True
>>> another_palindrome('halibut')
#False

 

4.5. 코드 구조 순회하기: itertools

 

itertools는 특수 목적의 이터레이터 함수를 포함하고 있습니다. for ... in 루프에서 이터레이터 함수를 호출할 때 함수는 한 번에 한 항목을 반환하고, 호출 상태를 기억합니다.

chain() 함수는 순회 가능한 인자들을 하나씩 반환합니다.

>>> import itertools
>>> for item in itertools.chain([1, 2], ['a', 'b']):
	print(item)

	
#1
#2
#a
#b

accumulate() 함수는 축적된 값을 계산합니다. 기본적으로 합계를 계산합니다.

>>> import itertools
>>> for item in itertools.accumulate([1, 2, 3, 4]):
	print(item)

	
#1
#3
#6
#10

accumulate() 함수의 두 번째 인자로 함수를 전달하여, 합계를 구하는 대신 이 함수를 사용할 수 있습니다. 이 함수는 두 개의 인자를 취하여 하나의 결과를 반환합니다. 이 예제는 축적된 곱을 계산합니다.

>>> import itertools
>>> def multiply(a, b):
	return a * b

>>> for item in itertools.accumulate([1, 2, 3, 4], multiply):
	print(item)

	
#1
#2
#6
#24

itertools 모듈은 시간을 단축할 수 있는 조합(Combination)과 순열(Permutation)에 대한 더 많은 함수를 제공합니다.

 

4.6. 깔끔하게 출력하기: pprint()

 

지금까지는 모든 예제를 print() 함수를 사용해서 출력하거나 대화식 인터프리터에서 변수 이름을 사용하여 출력했습니다. 출력된 결과를 읽기 힘든 경우 pprint() 함수와 같은 멋진 프린터가 필요합니다.

>>> from pprint import pprint
>>> quotes = OrderedDict([
	('Moe', 'A wise guy, huh?'),
	('Larry', 'Ow!'),
	('Curly', 'Ntuk ntuk!'),
	])

평범한 print() 함수의 출력 결과는 다음과 같습니다.

>>> print(quotes)
#OrderedDict([('Moe', 'A wise guy, huh?'), ('Larry', 'Ow!'), ('Curly', 'Ntuk ntuk!')])

그러나 pprint() 함순느 가독성을 위해 요소들을 정렬하여 출력합니다.

>>> pprint(quotes)
#OrderedDict([('Moe', 'A wise guy, huh?'),
#             ('Larry', 'Ow!'),
#             ('Curly', 'Ntuk ntuk!')])

 

5. 다른 파이썬 코드 가져오기

 

표준 라이브러리에 원하는 모듈이 없거나 그 모듈이 예상대로 동작하지 않을 때가 있습니다. 파이썬은 전 세계적으로 서드파티 오픈소스가 있습니다. 여기에 좋은 참고 자료를 소개하겠습니다.

  • PyPI(몬티 파이튼의 촌극 이후 치즈숍(Cheese Shop)으로도 알려져 있습니다)

  • github

  • readthedocs

activestate 사이트에서 작은 코드 예제를 많이 찾을 수 있습니다.

이전-이후 게시글에 있는 파이썬 코드 대부분은 표준 파이썬 설치에 포함된 내장 함수와 표준 라이브러리를 사용합니다. 게시글의 몇몇 부분에서 외부 패키지가 등장합니다. 이전 게시글에서는 requests를 사용했습니다. requests에 대한 자세한 내용은 추후 게시글을 통해 알려드리겠습니다.