파이써닉한 파이썬을 배워보자 - 10일차 파이썬의 패키지와 모듈

0

pythonic

목록 보기
10/10

모듈과 패키지

파이썬 프로그램은 import문으로 로드되는 모듈과 패키지로 구성되어 있다. 참고로, 파이썬의 모든 파일은 module이고, 이 module들을 하나로 묶어 package라고 한다. 물론 쓰다보면 이들이 혼용되어 쓰이기도 하므로, 굳이 딱딱하게 구분할 필요까지는 없다.

module과 import문

파이썬 소스파일은 모듈 형태로 불러올 수 있다.

  • module.py
a = 37

def func():
    print(f'func says that is {a}')

class SomeClass:
    def method(self):
        print('method says hi')
        
print("loaded module")

해당 파일은 전역 변수, 함수, 클래스 정의, 독립된 문을 포함한 일반적인 프로그래밍 요소가 있다. 이 예제는 모듈 로딩의 몇 가지 중요한 특징을 보여준다.

  • main.py
import module # loaded module

print(module.a) # 37
s = module.SomeClass()
s.method() # method says hi

import문을 실행하는 동안 몇가지 일이 발생한다.

  1. 모듈 소스 코드의 위치를 찾는다. 찾을 수 없다면 ImportError예외가 발생한다.
  2. 새로운 모듈 객체가 생성된다. 이 객체는 모듈에 포함된 모든 전역 정의(global definitions)의 컨테이너 역할을 한다. 때로는 이를 네임스페이스라 지칭한다.
  3. 모듈 소스 코드는 새로 생성된 모듈 네임스페이스 내에서 실행된다.
  4. 에러가 발생하지 않으면 새로운 모듈 객체를 참조하는 이름이 호출자 안에서 생성된다. 이 이름은 모듈의 이름과 일치하지만, 어떤 종료의 파일 이름 확장자도 없다. 가령, 코드가 module.py파일에 있으면 모듈의 이름은 module이다.

첫번째로 모듈의 소스 코드를 찾는데, 해당 파일은 sys.path에서 찾을 수 있는 디렉터리 중 하나에 위치하거나, 파일이어야 한다. 이는 실행되는 파일인 main.py의 위치를 기반으로 한다.

모듈에 있는 모든 정의는 해당 module에 격치된 채 남아있다. 따라서, 변수, 함수, 클래스 이름은 다른 모듈의 동일한 이름과 충돌하지 않는다. 모듈의 정의에 접근할 때는 module.func()과 같이 완전한 형태의 이름을 사용한다.

import문은 로드된 소스파일의 문장을 모두 실행한다. 이 모듈이 객체를 정의하는 것과 더불어 계산을 수행하거나 출력을 생성하면, 이 예제 코드처럼 loaded module과 같은 메시지를 출력한다.

import문에 as 한정어를 사용해 모듈을 참조할 때 사용하는 이름을 변경할 수 있다.

import module as mo
mo.func()

참고로 모듈은 파이썬의 일급 객체이다. 따라서, 변수에 할당하고 자료구조에 배치하며 함수의 파라미터, 반환값으로 쓸 수 있다.

모듈 캐싱

모듈의 소스 코드는 import문을 사용하는 빈도와 상관없이 한 번만 로드되고 실행된다. 후속 import문은 모듈 이름을 이전에 이미 import문 생성한 모듈 객체에 연결한다.

때문에, 대화형 세션에서 import로 패키지를 가져온 다음, 패지키의 코드를 수정하고, 다시 대화형 세션에서 import를 해도 수정한 내용이 반영되지 않는다. 이는 모듈 캐시 때문이다. 파이썬은 소스 코드가 업데이트되더라도 이전에 불러온 모듈을 다시 로드하지 않는다.

sys.modules에서 현재 로드된 모듈의 캐시를 모두 찾을 수 있다. 이는 모듈 이름을 모듈 객체에 매핑한 dict이다. 만약, 캐시에서 모듈을 삭제하면, 다음 import문에서 강제로 다시 로드된다. 하지만 이 방법은 안전하지 않으므로 쓰지 않는 것이 좋다.

다음과 같이 함수내에서 import문을 쓰는 경우를 종종 볼 수 있다.

def f(x):
    import math
    return math.sin(x) + math.cos(x)

import문을 함수에서 사용하는 것은 권장되지 않지만, 한번 캐시되었다면 매번 호출하지않으므로 성능상의 큰 변함은 없다. 호출이 잘 안되는 특수 함수가 있을 때는 함수 본문 내에 import문의 종속성을 추가하여 프로그램 로딩 속도를 높일 수 있다. 위처럼 실제로 필요한 경우에만 필수 모듈을 로드한다.

모듈에서 선택된 이름만 가져오기

from module import name문을 사용하면 현 네임스페이스 모듈에서 특수한 정의를 로드할 수 있다. 새로 생성된 모듈 네임스페이스를 참조하는 이름을 만드는 대신, 모듈에서 정의된 하나 이상의 객체에 대한 참조를 현재의 네임스페이스에 두는 것을 빼고는 import문과 동일하다.

from module import func # module을 불러오고 현재 네임스페이스에 func을 추가
func() # module에 정의된 func() 호출
module.func() # 실패, NameError: module

module은 해당 지역 네임스페이스에 할당되지 않으므로, NameError가 발생하게 되는 것이다.

모듈 내에 있는 여러 정의를 사용하고 싶다면 from 문에서 콤마로 구분하여 이름을 나열하면 된다.

from module import func, SomeClass

의미상으로 from module import name문은 이름을 모듈 캐시에서 지역 네임스페이스로 복사한다. 즉, 파이썬은 뒤에서 먼저 import모듈을 실행한다. 그런 다음 name = sys.modules['module'].name과 같이 캐시로부터 지역 이름으로 할당한다.

여기서 흔히하는 오해는 from module import name문이 모듈의 일부만 로딩하기 때문에 더 효율적이라고 생각하는 것이다. 사실 그렇지 않다. 어느 쪽이든 전체 모듈은 캐시에 로드되고 저장된다.

from문법을 사용하여 함수를 가져와도 그들의 유효 범위 규칙은 변경되지 않는다. 함수가 변수를 찾을 때 함수가 정의된 파일에서만 찾을 뿐, 함수를 불러오고 호출하는 네임스페이스에서 찾지 않는다.

from module import func # loaded module

a = 47
func() # func says that is 37
print(func.__module__) # module
print(func.__globals__['a']) # 37

전역 변수 동작 방식과 관련해 약간의 혼란이 있을 수 있다. 가령, func과 그것이 사용하는 전역 변수 a를 불러와서 사용하는 코드를 생각해보자.

from module import a, func # loaded module

a = 42
func() # func says that is 37
print(a) # 42

func에서 참조하는 a의 값이 42로 변경되지 않는 것을 확인할 수 있다.

별표(*) 문자는 밑줄로 시작하는 정의를 제외하고, 모듈에 있는 정의를 모두 로드할 때 사용된다. 다음은 한 예이다.

# 정의를 모두 현재 네임스페이스로 불러오기
from module import *

from module import *은 모듈의 최상위 범위에서만 사용할 수 있다. 특히 함수 내에서 이러한 형식의 import문은 사용할 수 없다.

모듈은 __all__리스트를 정의하여 from module import *로 불러올 이름을 정확하게 제어할 수 있다.

  • module.py
__all__ = ['func', 'SomeClass']

a = 37 # export 실패

def func(): # export 성공
    print(f'func says that is {a}')

class SomeClass: # export 성공
    def method(self):
        print('method says hi')
        
print("loaded module")

그러나 from module import *방식은 지역 네임 스페이스를 오염시키고 혼란을 초래할 수 있다.

from math import *
from random import *
from statistics import *

a = gauss(1.0, 0.25) # 어떤 모듈에서 가져온 것일까?

일반적으로 이름을 명시하는 것이 좋다.

from math import sin, cos, sqrt
from random import gauss
from statistics import mean

순환 import

두 모듈이 서로를 불러오면 특이한 문제가 발생한다. 다음과 같이 두 개의 파일이 있다고 하자.

  • moda.py
import modb

def func_a():
    modb.func_b()
    
class Base:
    pass
  • modb.py
import moda

def func_b():
    print('B')

class Child(moda.Base):
    pass

이 코드는 서로를 순환해서 참조한다. 재밌는 것은 python으로 moda.py를 실행하면 성공하고, modb.py를 실행하면 실패한다. 즉, moda.py에서 import modb는 성공하지만 modb.py에서 import moda를 하면 순환 참조 때문에 실패한다.

해당 부분의 흐름을 알 필요가 있다. 실패하는 modb.py를 실행하면 import modamoda.py파일을 실행한다. 첫번째로 접하는 문장은 import modb이다. 그러면 제어는 modb.py로 전환된다. modb.py파일의 첫번째 문장은 import moda이다. 재귀 순환이 일어나는 대신 모듈 캐시덕분에 modb.py의 다음 문장에서 계속 진행된다.

순환 불러오기로 인해 파이썬이 교착상태에 빠지거나 새로운 시공간으로 진입하진 않는다. 하지만 실행 시점에 moda 모듈은 부분적으로만 평가되었다. 제어 흐름이 class child(moda.Base)문에 도달하면 에러가 발생한다. 필요한 Base 클래스가 아직 정의되지 않았기 때문이다.

이 문제를 해결하는 한 가지 방법은 import modb문을 다른 곳으로 옮기는 것이다.

그러나 이는 끔찍한 방법으로, 순환 불러오기 자체의 구조가 문제가 있으므로 구조 자체를 수정해야한다.

모듈 컴파일

모듈을 처음 불러올 때 이들은 인터프리터 바이트코드로 컴파일된다. 이 코드는 특별한 __pycache__ 디렉터리 내에서 .pyc 파일로 작성된다. 이 디렉터리는 일반적으로 원본.py 파일과 같은 디렉터리에서 찾을 수 있다. 프로그램이 다시 실행되어 동일한 import문을 실행하면 컴파일된 바이트코드가 대신 로드된다. 이는 불러오는 속도를 크게 높인다.

바이트 코드 캐싱은 거의 걱정할 필요가 없는 자동 과정이다. 원본 소스가 변경되면 파일이 자동으로 다시 생성된다.

단, 이 캐싱 및 컴파일 과정을 알 필요가 있다. 먼저 사용자가 필수 __pycache__ 디렉터리를 생성할 수 있는 운영체제 권한이 없는 환경에서 파이썬 파일이 설치되는 경우가 있다. 파이썬은 여전히 동작하지만 불러올 때마다 매번 원본 소스 코드를 로드하고 바이트코드로 컴파일 할 것이다. 즉 프로그램 로딩이 필요한 것보다 훨씬 느려진다. 마찬가지로 파이썬 응용 프로그램을 배포하거나 패키징할 때 컴파일된 바이트코드를 포함하는 것이 유리할 수 있다. 이는 프로그램 시작 속도를 크게 높일 수 있다.

모듈 캐싱을 알아야 하는 또 다른 이유는 일부 프로그래밍 기술이 모듈 캐싱을 방해하기 때문이다. 동적 코드 생성과 exec() 함수와 관련된 고급 메타 프로그래밍 기술은 바이트 코드 캐싱의 이점을 무효로 한다. 대표적인 예가 dataclass를 사용할 때이다.

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

dataclass는 메서드 함수를 텍스트 조각으로 생성하고, 이드를 exec()으로 실행해 동작하는 방식이다. 생성 코드는 import시스템으로 캐시되지 않는다. 단일 클래스 정의하면 눈치채지 못할 수도 있다. 일반적이지만 덜 간편한 방식으로 클래스를 작성하는 모듈보다 거의 20배 느리게 불러온다는 것을 알 수 있다.

모듈 탐색 경로

모듈을 가져올 때 인터프리터는 sys.path안의 디렉터리 목록을 탐색한다. sys.path안의 딕레터리 목록을 탐색한다. sys.path의 첫번째 항목은 현재 작업 디렉터리를 나타내는 빈 문자열 ''이다. 그 대신에 스크립트를 실행하는 경우라면 sys.path의 첫번째 항목은 스크립트가 있는 디렉터리이다.

sys.path의 다른 항목은 일반적으로 디렉터리 이름과 .zip아카이브 파일들이 혼재되어 구성된다. sys.path에서 나열되는 항목 순서가 모듈을 가져올 때 사용되는 탐색 순서를 결정한다. 탐색 경로에 새 항목을 추가하려면 sys.path에 추가하면 된다. 이 작업은 직접 수행하거나 PYTHONPATH 환경 변수에서 설정할 수 있다.

env PYTHONPATH=/some/path python3 script.py

zip아카이브 파일은 모듈 모음을 단일 파일로 묶는 편리한 방법이다. 가령, foo.py, bar.py라는 두 모듈을 만들고 이를 mymodules.zip 파일에 배치했다고 가정하자. 다음과 같이 이 파일을 파이썬 탐색 경로에 추가할 수 있다.

import sys
sys.path.append('mymodules.zip')
import foo, bar

.zip 파일의 디렉터리 구조 안에 있는 특정 위치는 경로로 사용될 수 있다. 또한, .zip 파일은 다음과 같이 일반 경로 이름 요소와 혼합해 사용할 수 있다.

sys.path.append('/tmp/modules.zip/lib/python')

zip파일에 .zip 파일 확장자를 사용할 필요는 없다. 과거에는 .egg 파일도 탐색 경로에서 흔히 만났다.

메인 프로그램으로 실행

파이썬 파일은 메인 스크립트로 실행되곤 한다.

python3 module.py

각각의 모듈에는 모듈의 이름을 지닌 __name__변수가 있다. 코드는 이 변수를 검사하여 실행할 모듈을 결정할 수 있다. 인터프리터의 최상위 모듈 이름은 __main__이다. 명령중에서 지정하거나 대화식으로 입력한 프로그램은 __main__ 모듈 내에서 실행된다.

프로그램을 모듈로 가져왔는지 아니면 __main__에서 실행하는 지에 따라 동작 방식을 때론 변경할 수 있다. 가령, 다음은 모듈이 메인 프로그램으로 사용되는 경우에는 실행되지만, 다른 모듈에서 단순히 불러온 경우에는 실행되지 않는 코드를 작성한 것이다.

if __name__ == '__main__':
    # main script
else:
    # module call

이렇게 main script로 호출될 때 module로 호출할 때의 경우를 나누어 추가 테스트, 예제 코드를 사용할 수 있다. 가령, 특정 모듈에 위와 같이 if-else를 추가한 다음, main script로 호출되면 디버깅을 할 수 있는 로그를 출력하도록 하고, 라이브러리로 모듈이 불러지면 로깅을 안하도록 하는 것이다.

파이썬 코드가 있는 디렉터리를 만들었을 때, 디렉터리 안에 __main__.py파일이 있으면 그 디렉터리를 실행할 수 있다. 가령, 다음과 같이 디렉터리를 만들었다고 하자.

myapp/
    foo.py
    bar.py
    __main__.py

python3 myapp을 입력하여 파이썬을 실행할 수 있다. 실행은 ___main__.py파일에서 시작된다. 이 기능은 myapp 디렉터리를 zip 아카이브로 변환하는 경우에서도 동작한다. python3 myapp.zip을 입력하면 최상위 __main__.py파일을 찾아 실행한다.

패키지

파이썬 코드는 패키지로 구성된다. package는 공통의 최상위 이름 아래에서 그룹화된 모듈 집합이다. 이 그룹화는 서로 다른 응용 프로그램에서 사용되는 모듈 이름 간의 충돌을 해결하며, 코드를 다른 사용자의 코드와 별도로 유지할 수 있도록 해준다. 패키지는 고유한 이름을 가진 디렉터리를 생성하여 정의된다. 처음에는 비어있는 __init__.py를 해당 디렉터리에 배치한다. 그 다음 필요에 따라 이 디렉터리에 추가로 파이썬 파일과 하위 패키지를 배치한다. 가령, 패키지는 다음과 같이 구성할 수 있다.

graphics/
    __init__.py
    primitive/
        __init__.py
        lines.py
        fill.py
        text.py
        ...
    graph2d/
        __init__.py
        plot2d.py
        ...
    graph3d/
        __init__.py
        plot3d.py
        ...
    formats/
        __init__.py
        gif.py
        png.py
        tiff.py
        jpeg.py

패키지에서 모듈을 로드하는 데 import문이 사용된다. 더 긴 이름을 사용한다는 점을 제외하고는 단순 모듈을 로드하는 것과 동일하다. 다음의 예를 살펴보자.

# 전체 경로
import graphics.primitive.fill

graphics.primitive.fill.floodfill(img, x, y, color)

# 특정 하위 모듈 로드
from graphics.primitive import fill
fill.floodfill(img, x, y, color)

# 하위 모듈의 특정 함수를 로드
from graphics.primitive.fill import floodfill
...
floodfill(img, x, y, color)

패키지의 일부를 처음 가져올 때마다 __init__.py파일이 있으면, 이 파일 내의 코드가 먼저 실행된다. 해당 파일이 없을 수도 있지만, 패키지별 초기화(package-specific initializations)를 수행하는 코드를 포함할 수도 있다.

깊이 중첩된 하위 모듈을 로드하는 경우, 디렉터리 구조를 탐색할 때마다 발견하는 __init__.py파일을 모두 실행한다. 따라서 import graphics.primitive.fill문은 graphics/ 디렉터리의 __init__.py파일을 먼저 실행하고 다음으로는 primitive/ 디렉터리의 __init__.py 파일을 실행한다. 물론, 이미 실행되었던 __init__.py은 계속해서 실행되진 않는다. import할 때 한 번 호출되었다면 그 다음부터는 실행되지 않는다.

__init__.py 파일이 없더라도 패키지는 동작한다. 그러나 이는 매우 고급 기능인 네임스페이스 패키지를 만드는 방법이며, 권장하지 않는다. 때문에 항상 적절한 __init__.py파일을 만드는 것이 좋다.

패키지 내에서 불러오기

import문의 중요한 특징 하나는 모듈을 불러올 때 절대 또는 완전한 패키지 경로가 필요하다는 점이다. 만약, graphics.primitive.fill모듈이 graphics.primitive.lines을 불러오길 원한다고 하자. 같은 디렉터리 위치니까 import lines하면 될 것 같지만 안된다.

그 대신 다음과 같이 완전한 경로를 작성해야 한다.

  • graphics/primitive/fill.py
from graphics.primitive import lines

안타깝게도 이처럼 완전하게 패키지 이름을 작성하는 것은 성가시며 잘못 작성할 가능성도 높다. 가령, 패키지 이름이 변경되면 이와 연관된 import는 모두 변경해야한다. 따라서, 다음과 같이 상태 경로로 패키지를 가져오는게 더 나은 선택이다.

  • graphics/primitive/fill.py
from . import lines

여기서 from . import lines 문에서 사용되는 .은 가져올 모듈과 동일한 디렉터리를 참조한다. 따라서 이 문장은 fill.py 파일과 동일한 디렉터리에서 모듈 lines를 찾는다.

상대 경로 import문은 동일한 패키지의 다른 디렉터리에 있는 하위 모듈도 지정할 수 있다. 가령, graphics.graph2d.plot2d 모듈이 graphics.primitive.lines를 불러오는 경우에는 다음과 같은 문장을 사용하면 된다.

  • graphics/graph2d/plot2d.py
from ..primitive import lines

여기서 ..는 한 디렉터리 수준 위로 이동하고, primitive 디렉터리는 다른 하위 디렉터리로 이동한다.

상대 경로 import문은 from module import symbol 형식을 사용해야만 지정할 수 있다. import ..primitive.lines 도는 import .lines와 같은 문장는 구문 오류다. 또한 symbol은 단순 식별자여야 하므로 from .. import primitive.lines와 같은 문장 또한 구문 오류이다. 마지막으로 상대 경로 import문은 패키지 안에서만 사용할 수 있다. 파일 시스템의 다른 디렉터리에 있는 모듈을 참조하기 위해 상태 경로 import문을 사용하면 에러가 발생한다.

패키지 하위 모듈을 스크립트로 실행

메인이 되는 코드가 따로 있고, 메인에 쓰이는 모듈을 python3 module으로 실행시키려고 한다. 해당 모듈이 다른 모듈들을 참조하고 있으면 어떻게될까?

패키지로 구성된 코드는 간단한 스크립트와는 런타임 환경이 다르다. 패키지 이름과 하위 모듈을 감싸는 것, 패키지에서만 동작하는 상대 경로 import문을 사용하는 것 등이 있다. 패키지 소스 파일 내에서 파이썬을 직접 실행하는 기능은 더 이상 동작하지 않는다.

가령, graphics/graph2d/plot2d.py 파일은 main.py에서 쓰이는 모듈이다.

  • graphics/graph2d/plot2d.py
from ..primitive import lines, text

class Plot2D:
    ...

if __name__ == '__main__':
    print('')
    p = Plot2D()
    ...

이 코드를 직접 실행하면 상대 경로 import문에서 에러가 발생한다.

python3 graphics/graph2d/plot2d.py

...
Traceback (most recent call last):
    File "graphics/graph2d/plot2d.py", line 1 in <module>
        from ..primitive import lines, text
ValueError: attempted relative import beyond top-level package

패키지 디렉터리로 이동해 실행해도 똑같이 에러가 발생할 것이다.

이 처럼, 모듈을 메인스크립트로 실행하려면 인터프리터에 -m옵션을 사용해야 한다.

python3 -m graphics/graph2d/plot2d.py

-m은 모듈 도는 패키지를 메인 프로그램으로 지정한다. 파이썬은 import문이 동작하는 지 확인하기 위해 적절한 환경에서 모듈을 실행한다. 파이썬의 많은 내장 패키지에는 -m을 사용할 수 있는 비밀 기능이 있는 패키지들이 있다. 가령, python3 -m http.server를 사용하여 현재 디렉터리에서 웹서버를 실행하는 기능이 가장 잘 알려져있다.

사용자 자신이 만든 고유 패키지에서도 유사한 기능을 제공할 수 있다. python -m name문으로 제공된 name이 패키지 디렉터리에 해당한다면, 파이썬은 해당 디렉터리에 __main__.py가 있는 지 찾아 스크립트로 실행한다.

정리하자면, __init__.py은 package를 import할 때 한 번 동작하고, __main__.py는 해당 패키지가 __main__으로 실행될 때 실행된다. 따라서, python3 -m으로 모듈을 실행하면 __main__으로 실행되어 __main__.py가 실행된다.

패키지 네임스페이스 제어

패키지의 주목적은 코드의 최상위 컨테이너 역할을 하는 것이다. 사용자는 때때로 최상위 이름만 불러오고, 그 외 다른 것은 불러오지 않을 때가 있다. 다음은 한 예이다.

import graphics

import문은 특정 하위 모듈을 지정하지 않는다. 또한, 패키지의 다른 부분에 접근하지도 않는다. 예를 들어 다음과 같은 코드는 실패한다.

import graphics
graphics.primitive.fill.floodfill(img, x, y, color) # 실패

최상위 패키지 import문만 제공하는 경우, 불러오는 파일은 연결된 해당 __init__.py파일뿐이다. 이 예에서는 graphics/__init__.py 파일이다.

__init__.py 파일의 주목적은 최상위 패키지 네임스페이스의 내용을 구축 또는 관리하는 것이다. 여기에 낮은 수준의 하위 모듈에서 선택한 함수, 클래스, 기타 객체를 가져오는 것이 포함되곤한다. 예를 들어, 이전 예제에서 graphics 패키지가 수백 개의 하위 수준 함수로 구성되어 있지만, 이러한 세부 정보 대부분이 소수의 상위 수준 클래스로 캡슐화되어 있는 경우, __init__.py파일은 상위 수준 클래스만 보이도록 선택할 수 있다.

  • graphics/init.py
from .graph2d.plot2d import Plot2D
from .graph3d.plot3d import Plot3D

__init__.py파일을 사용하면 Plot2DPlot3D 이름은 패키지의 최상위 수준에 나타난다. 그러면 사용자는 graphics가 단순한 모듈인 것처럼 이러한 이름을 사용할 수 있다.

from graphics import Plot2D

plt = Plot2D(100, 100)
plt.clear()

이는 사용자들이 코드가 실제로 어떻게 구성되었는지 알 필요가 없기 때문에 훨씬 더 편리한 방법이다. 어떤 의미에서는 기존 코드 구조보다 더 높은 추상화 계층을 둔 것이라 할 수 있다. 파이썬 표준 라이브러리의 많은 모듈이 이러한 방식으로 구성되어있다. 가령, 인기있는 collections모듈은 실제로 패키지이다. collections/__init_-.py파일은 몇 가지 다른 위치에 있는 정의를 하나로 합치고, 이들을 단일 네임스페이스로 통합해 사용자에게 제공한다.

패키지 내보내기 제어

__init__.py와 낮은 수준 하위 모듈의 상호작용에는 한 가지 문제점이 있다. 가령, 패키지 사용자는 최상위 패키지 네임스페이스에 있는 객체 및 함수에만 관심이 있다. 하지만 패키지 구현자는 코드를 유지 관리하는 가능한 하위 모듈로 구성하는 문제에 관심이 많다.

이러한 구조적 복잡성을 잘 관리하기 위해, 패키지 하위 모듈은 __all__변수를 정의하여 명시적인 내보내기 목록을 선언하곤 한다. 이는 패키지 네임스페이스에 한 수준 위로 올라가야 하는 이름 목록이다.

  • graphics/graph2d/plot2d.py
__all__ = ['Plot2D']

class Plot2D:
    ...

관련 __init__.py파일은 다음과 같이 *import문을 사용하여 하위 모듈을 불러온다.

  • graphics/graph2d/init.py
# __all__ 변수에 나열된 이름만 명시적으로 불러옴
from .plot2d import *

# __all__을 다음 수준으로 전파 ( 필요한 경우만 사용 )
__all__ = plot2d.__all__

그런 다음 끌어올리는(lifting) 과정은 최상위 패키지 __init__.py까지 계속 수행된다.

  • graphics/init.py
from .graph2d import *
from .graph3d import *

# 내보내기 통합
__all__ = [
    *graph2d.__all__,
    *graph3d.__all__
]

이 코드의 골자는 패키지의 모든 구성 요소가 __all__ 변수를 사용하여 내보내기를 명시한다는 것이다. 그런 다음 __init__.py파일은 내보내기를 위쪽으로 전파한다. 실제로 복잡할 수 있지만, 이 방식은 특정 내보내기 이름을 __init__.py 파일에 직접 작성해야 하는 문제를 해결할 수 있다. 대신 하위 모듈이 무언가를 내보내려는 경우, 해당 하위 모듈의 이름은 __all__변수에서 얻을 수 있다.

패키지 데이터

패키지에서 데이터를 필요로 할 때는 pkgutil.get_data(package, resource)를 사용해 패키지 데이터를 읽는다. 이는 패키지 내에서 특정 위치에 있는 정보를 읽기 어렵기 때문이다.

다음의 구조로 파일이 구성되어 있다고 해보자.

mycode/
    resources/
        - data.json
    - __init__.py
    - spam.py
    - yow.py

mycode/spam.py에서 resources/data.json 파일을 불러오고 싶다고 하자. open을 쓸 수 있지만, 패키지는 패키지를 기점으로 파일을 읽는 것이 좋다. 때문에 pkgutil을 사용하는 것이다.

  • mycode/spam.py
import pkgutil
import json

def func():
    rawdata = pkgutil.get_data(__package__, 'resources/data.json')
    textdata = rawdata.decode('utf-8')
    data = json.loads(textdata)
    print(data)

get_data()함수는 지정된 resource를 찾으려고 시도하고, 해당 내용을 원시 바이트 문자열로 반환한다. __package__변수는 둘러싼 패키지의 이름을 가진 문자열이다. 바이트를 텍스트로 변환하는 것과 같은 추가적인 디코딩과 해석은 사용자에게 달려 있다. 이 예에서 데이터는 JSON에서 파이썬 dict로 디코딩되고 파싱된다.

패키지는 대용량 데이터 파일을 저장하기에 적합하지 않으므로, 알아만 두도록 하자.

모듈 객체

모듈은 1급 객체로 속성을 갖는다.

메서드설명
name전체 모듈 이름
doc문서화 문자열
dict모듈 dict
file정의된 파일 이름
package둘러산 패키지 이름
path패키지의 하위 모듈을 탐색할 하위 디렉터리 목록
annotations모듈 수준 타입 힌트

__dict__속성은 모듈 네임스페이스를 나타내는 dict이다. 모듈에 정의된 모든 것들이 이 dict에 저장된다.

__name__속성은 스크립트에서 자주 사용된다. if __name__ == '__main__'과 같은 조건부 검사는 파일이 독립적인 프로그램으로 실행되는 지 확인하기 위해 수행된다.

__package__ 속성은 둘러싼 패키지가 있는 경우에 그 패키지의 이름을 포함한다.

가령, graphics/primitives/graphics.py에서 __name__, __package__를 호출하면

__name__: graphics.primitives.graphics
__package__: graphics.primitives

다음과 같이 나온다.

__path__속성은 설정되어 있다면, 해당 패키지의 전체 경로를 반환한다. 단, 이는 __init__.py와 같은 곳에서만 호출이 가능하다.

  • graphics/init.py에 __path__를 호출하면 다음과 같다.
__path__: '/temp/python/project/graphics'

파이썬 패키지 배포

가장 단순한 방법은 setuptools모듈 또는 내장 distutils 모듈을 사용하는 것이다. 코드를 작성한 프로젝트가 다음과 같다고 하자.

spam-project/
    - README.txt
    - Documentation.txt
    - spam/ # 코드 패키지
        - __init__.py
        - foo.py
        - bar.py
    - runspam.py # python runspam.py로 스크립트 실행

배포하기 위해 최상위 디렉터리(spam-project/)에 setup.py 파일을 생성한다. 이 파일에서 다음과 같은 코드를 추가하자.

  • spam-project/setup.py
from setuptools import setup

setup(name="spam", version="0.0", packages=['spam'], scripts=['runspam.py'])

setup()호출에서 packages는 모든 패키지 디렉터리이고, scripts는 스크립트 파일 목록이다. name은 패키지 이름이며, version은 문자열로 된 버전 번호이다. setup() 호출은 패키지에 다양한 메터 데이터를 제공하는 여러가지 다른 매개변수를 지원한다.

setup.py파일을 만들면 소프트웨어 소스를 얼마든지 배포할 수 있다. 다음에 명령어로 소스 배포 파일을 만들 수 있다.

python3 setup.py sdist

이렇게 하면 spam/dist 디렉터리에 spam-1.0.tar.gz 또는 spam-1.0.zip과 같은 아카이브 파일이 만들어진다. 아 파일은 소프트웨어를 설치하려는 다른 사람에게 제공된다. 사용자는 pip와 같은 명령어를 사용해 설치할 수 있다.

python3 -m pip install spam-1.0.tar.gz

이 파일은 소프트웨어를 로컬 파이썬 배포판으로 설치하고 이용할 수 있게 해준다. 코드는 일반적으로 파이썬 라이브러리의 site-packages라는 디렉터리에 설치된다. sys.path의 값을 검사하여 디렉터리의 정확한 위치를 찾을 수 있다. 스크립트는 일반적으로 파이썬 인터프리터가 실치된 곳과 동일한 디렉터리에 설치된다.

스크립트 첫 줄이 #!으로 시작하고 python으로 쉬뱅이 적혀져 있다면 설치 프로그램이 파이썬의 local 설치를 가리키도록 줄을 다시 작성한다. 따라서, 스크립트들이 특정 파이썬 위치 (가령, /usr/local/bin/python)로 하드 코딩되어 있더라도, 파이썬의 설치 위치가 다른 시스템에 설치했을 때 이 스크립트들은 여전히 잘 동작해야 한다.

setuptools는 정말 다양한 사용 방법과 case들이 있지만 여기서는 이 정도만 다룬다.

0개의 댓글