Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 302 – 새 임포트 훅

Author:
Just van Rossum <just at letterror.com>, Paul Moore <p.f.moore at gmail.com>
Status:
Final
Type:
Standards Track
Created:
19-Dec-2002
Python-Version:
2.3
Post-History:
19-Dec-2002

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Warning

import에 대한 언어 레퍼런스 [10]와 importlib 문서 [11]가 이제 이 PEP를 대체합니다. 이 문서는 더 이상 갱신되지 않으며 역사적인 목적으로만 제공됩니다.

초록

이 PEP는 Python 임포트 메커니즘을 더 잘 사용자 정의할 수 있게 해 주는 새로운 임포트 훅 집합을 추가할 것을 제안합니다. 현재의 __import__ 훅과 달리 새로운 방식의 훅은 기존 체계에 주입할 수 있으므로, 모듈을 찾는 방식과 로드하는 방식을 더 세밀하게 제어할 수 있습니다.

동기

현재 임포트 메커니즘을 사용자 정의하는 유일한 방법은 내장 __import__ 함수를 재정의하는 것입니다. 하지만 __import__를 재정의하는 데에는 많은 문제가 있습니다. 우선 다음과 같은 문제가 있습니다:

  • __import__ 대체물은 전체 임포트 메커니즘을 완전히 다시 구현하거나, 사용자 정의 코드의 앞이나 뒤에서 원래의 __import__를 호출해야 합니다.
  • 의미 체계와 책임이 매우 복잡합니다.
  • __import__는 이미 sys.modules에 있는 모듈에 대해서도 호출되는데, 일종의 모니터링 도구를 작성하는 경우가 아니라면 이는 거의 언제나 원하지 않는 동작입니다.

C에서 임포트 메커니즘을 확장해야 할 때는 상황이 더 나빠집니다. Python의 import.c를 고치거나 import.c의 상당 부분을 처음부터 다시 구현하는 것 외에는 현재로서는 불가능합니다.

__import__ 훅을 기반으로 임포트 메커니즘을 다양한 방식으로 확장할 수 있게 해 주는, Python으로 작성된 도구들에는 꽤 오랜 역사가 있습니다. 표준 라이브러리에는 이러한 도구가 두 가지 포함되어 있습니다. ihooks.py (GvR 작성)와 imputil.py [1] (Greg Stein 작성)입니다. 하지만 아마 가장 유명한 것은 Gordon McMillan의 iu.py로, 그가 만든 설치 프로그램인 Installer 패키지의 일부로 제공됩니다. 이 도구들은 Python으로 작성되었기 때문에 유용성이 다소 제한됩니다. 훅을 포함하는 모듈을 그 훅 자체로 로드할 수는 없으므로 부트스트래핑 문제를 우회해야 합니다. 따라서 전체 표준 라이브러리를 임포트 훅에서 로드할 수 있게 하려면 그 훅은 C로 작성되어야 합니다.

사용 사례

이 절에서는 임포트 훅에 의존하는 몇 가지 기존 애플리케이션을 나열합니다. 이들 사이에서는 많은 중복 작업이 이루어졌는데, 당시에 더 유연한 임포트 훅이 있었다면 이를 줄일 수 있었을 것입니다. 이 PEP는 앞으로 비슷한 프로젝트들의 작업을 훨씬 수월하게 만들어 줄 것입니다.

비표준 방식으로 저장된 모듈을 로드하려면 임포트 메커니즘을 확장해야 합니다. 예를 들어 아카이브에 함께 묶여 있는 모듈, pyc 형식 파일에 저장되지 않은 바이트코드, 네트워크를 통해 데이터베이스에서 로드되는 모듈 등이 있습니다.

이 PEP 작업은 부분적으로 PEP 273의 구현에서 촉발되었습니다. 이 PEP는 Zip 아카이브로부터의 임포트를 Python의 내장 기능으로 추가합니다. PEP 자체는 반드시 필요한 기능으로 널리 받아들여졌지만, 그 구현에는 아쉬운 점이 몇 가지 있었습니다. 우선 이 구현은 import.c와 통합하기 위해 상당히 무리를 했는데, Zip 파일 임포트에 특화된 코드이거나 Zip 임포트에 특화되지는 않았지만 일반적으로 유용하지도 (심지어 바람직하지도) 않은 코드를 많이 추가했습니다. 그렇지만 이를 PEP 273 구현의 탓으로 돌리기는 어렵습니다: import.c의 현재 상태를 고려하면 이는 그저 극도로 어려운 일이기 때문입니다.

최종 사용자를 위한 애플리케이션 패키징은 임포트 훅의 전형적인 사용 사례이며, 가장 전형적인 사용 사례라고 해도 과언이 아닙니다. 많은 소스 파일이나 pyc 파일을 그대로 배포하는 것이 항상 적절하지는 않습니다(별도의 Python 설치는 말할 것도 없습니다). 그래서 필요한 모듈을 모두 단일 파일로 패키지화하려는 요구가 자주 있습니다. 실제로 그 요구가 너무 잦아서 수년에 걸쳐 여러 해결책이 구현되었습니다.

가장 오래된 것은 Python 소스 코드에 포함되어 있는 Freeze [2]입니다. 이 도구는 마샬링된 바이트코드를 C 소스 코드의 정적 객체에 넣습니다. Freeze의 “임포트 훅”은 import.c에 하드 코딩되어 있으며, 몇 가지 문제가 있습니다. 이후에 나온 해결책으로는 Fredrik Lundh의 Squeeze, Gordon McMillan의 설치 프로그램인 Installer, Thomas Heller의 py2exe [3] 등이 있습니다. MacPython에는 BuildApplication이라는 도구가 함께 제공됩니다.

Squeeze, 설치 프로그램인 Installer, py2exe는 __import__ 기반 방식을 사용합니다(py2exe는 현재 Installer의 iu.py를 사용하고, Squeeze는 ihooks.py를 사용했습니다). MacPython에는 import.c에 하드코딩된 Mac 전용 임포트 훅이 두 개 있으며, 이는 Freeze 훅과 비슷합니다. 이 PEP에서 제안하는 훅을 사용하면 import.c에 하드 코딩된 훅을 제거할 수 있으며(적어도 이론적으로는 그렇습니다. 단기 목표는 아닙니다), __import__ 기반 도구들도 import.c 에뮬레이션 코드의 대부분을 제거할 수 있게 됩니다.

이 PEP의 설계와 구현 작업이 시작되기 전에, Mac OS X용의 새로운 BuildApplication 같은 도구를 계기로 이 PEP의 작성자 중 한 명(JvR)이 동결된(frozen) 모듈의 테이블을 imp 모듈을 통해 Python에 노출했습니다. 주된 이유는 freeze 임포트 훅을 사용하면서도(복잡한 __import__ 지원을 피하면서), 런타임에 모듈 집합을 제공할 수도 있게 하려는 것이었습니다. 그 결과 이슈 #642578 [4]이 만들어졌는데, 이 이슈는 어쩐 일인지 받아들여졌습니다(대체로 아무도 어느 쪽이든 신경 쓰지 않는 것 같았기 때문입니다 ;-). 하지만 이 PEP가 채택되면 같은 일을 훨씬 더 깔끔하고 일반적인 방식으로 할 수 있으므로, 그 기능은 완전히 불필요해집니다.

근거

내장 Zip import를 구현하기 위해 여러 대안적인 구현 아이디어를 실험하던 중, import.c를 꽤 적게 변경하는 것만으로도 이를 달성할 수 있다는 사실이 밝혀졌습니다. 이를 통해 Zip 전용 부분을 새 소스 파일로 분리해 낼 수 있었고, 동시에 범용적인 새 임포트 훅 방식, 즉 지금 읽고 계신 방식을 만들 수 있었습니다.

이전 설계에서는 sys.path에 문자열이 아닌 객체를 허용했습니다. 그러한 객체는 import를 처리하는 데 필요한 메서드를 갖추게 됩니다. 이 방식에는 두 가지 단점이 있습니다. 1) sys.path의 모든 항목이 문자열이라고 가정하는 코드가 깨집니다. 2) PYTHONPATH 환경 변수와 호환되지 않습니다. 후자는 Zip import에 직접적으로 필요합니다. Jython에서 절충안이 나왔습니다. sys.path에 문자열의 서브클래스를 허용하고, 이것이 임포터 객체로 동작하게 하는 것입니다. 이렇게 하면 일부 호환성 문제를 피할 수 있고 Jython에서도 잘 동작하는 것으로 보이지만(Jython에서는 .jar 파일에서 모듈을 로드하는 데 사용됩니다), “보기 흉한 꼼수”로 여겨졌습니다.

이로 인해 좀 더 정교한 방식이 나왔는데(대부분 McMillan의 iu.py에서 가져왔습니다), 후보 목록의 각 항목에 sys.path 항목을 처리할 수 있는지 차례로 물어서 처리할 수 있는 항목을 찾을 때까지 계속하는 방식입니다. 이 후보 목록은 sys 모듈의 새 객체인 sys.path_hooks입니다.

새로운 import마다 각 경로 항목에 대해 sys.path_hooks를 순회하면 비용이 클 수 있으므로, 그 결과는 sys 모듈의 또 다른 새 객체인 sys.path_importer_cache에 캐시됩니다. 이 객체는 sys.path 항목을 임포터 객체에 매핑합니다.

import.c에 미치는 영향을 최소화하고 추가 오버헤드를 피하기 위해, 기존 파일 시스템 import 로직에 대해 명시적인 훅과 임포터 객체를 추가하지 않고(iu.py에는 있습니다), sys.path_hooks의 어떤 훅도 경로 항목을 처리할 수 없으면 그냥 내장 로직으로 폴백하는 방식을 택했습니다. 이 경우에는 역시 반복적인 조회를 피하기 위해 sys.path_importer_cache에 None 값이 저장됩니다. (나중에 더 나아가 내장 메커니즘을 위한 실제 임포터 객체를 추가할 수도 있지만, 지금은 None 대체 방식으로 충분할 것입니다.)

한 가지 질문이 제기되었습니다. sys.path에 항목이 전혀 필요하지 않은 임포터는 어떻게 합니까? (내장 모듈과 동결된 모듈이 이 범주에 속합니다.) 이번에도 Gordon McMillan이 해결책을 내놓았습니다. iu.py에는 그가 metapath라고 부르는 것이 있습니다. 이 PEP의 구현에서 이것은 sys.path보다 먼저 순회되는 임포터 객체의 목록입니다. 이 목록은 sys 모듈의 또 하나의 새 객체인 sys.meta_path입니다. 현재 이 목록은 기본적으로 비어 있으며, 동결된 모듈과 내장 모듈의 import는 sys.meta_path를 순회한 뒤에, 그러나 여전히 sys.path보다는 먼저 수행됩니다.

명세 1부: 임포터 프로토콜

이 PEP는 새로운 프로토콜인 “임포터 프로토콜(Importer Protocol)”을 도입합니다. 프로토콜이 동작하는 맥락을 이해하는 것이 중요하므로, 여기서는 임포트 메커니즘의 외곽 셸을 간략히 살펴봅니다.

import 문을 만나면 인터프리터는 내장 네임스페이스에서 __import__ 함수를 찾습니다. 그런 다음 __import__가 네 개의 인자로 호출되는데, 그중에는 임포트되는 모듈의 이름(점으로 구분된 이름일 수 있음)과 현재 전역 네임스페이스에 대한 참조가 있습니다.

그런 다음 내장 __import__ 함수(import.c에서는 PyImport_ImportModuleEx()로 알려져 있습니다)는 임포트를 수행하는 모듈이 패키지인지, 또는 패키지의 하위 모듈인지 확인합니다. 실제로 패키지(의 하위 모듈)라면, 먼저 해당 패키지(하위 모듈의 경우 부모 패키지)에 상대적으로 임포트를 시도합니다. 예를 들어 “spam”이라는 패키지가 “import eggs”를 수행하면, 먼저 “spam.eggs”라는 모듈을 찾습니다. 그것이 실패하면 임포트는 절대 임포트로 계속 진행되어 “eggs”라는 모듈을 찾습니다. 점으로 구분된 이름의 임포트도 거의 같은 방식으로 동작합니다. 패키지 “spam”이 “import eggs.bacon”을 수행하면(그리고 “spam.eggs”가 존재하며 그 자체가 패키지라면) “spam.eggs.bacon”을 시도합니다. 그것이 실패하면 “eggs.bacon”을 시도합니다. (여기서 설명하지 않는 더 미묘한 사항들이 있지만, 이는 임포터 프로토콜 구현자와는 관련이 없습니다.)

메커니즘의 더 깊은 곳에서는 점으로 구분된 이름의 임포트가 구성 요소별로 나뉩니다. “import spam.ham”의 경우, 먼저 “import spam”이 수행되고, 그것이 성공했을 때에만 “ham”이 “spam”의 하위 모듈로 임포트됩니다.

임포터 프로토콜은 이러한 개별 임포트 수준에서 동작합니다. 임포터가 “spam.ham”에 대한 요청을 받을 때에는 모듈 “spam”이 이미 임포트되어 있습니다.

이 프로토콜에는 파인더(finder)와 로더(loader)라는 두 객체가 관여합니다. 파인더 객체에는 메서드가 하나 있습니다:

finder.find_module(fullname, path=None)

이 메서드는 모듈의 완전한 정규화된 이름을 인자로 하여 호출됩니다. 파인더가 sys.meta_path에 설치되어 있으면 두 번째 인자를 받는데, 이는 최상위 모듈의 경우 None이고, 하위 모듈이나 하위 패키지의 경우 package.__path__입니다 [5]. 모듈을 찾으면 로더 객체를 반환하고, 찾지 못하면 None을 반환해야 합니다. find_module()이 예외를 발생시키면 그 예외는 호출자에게 전파되어 임포트가 중단됩니다.

로더 객체에도 메서드가 하나 있습니다:

loader.load_module(fullname)

이 메서드는 로드된 모듈을 반환하거나 예외를 발생시키는데, 기존 예외를 전파하는 경우가 아니라면 ImportError를 발생시키는 것이 바람직합니다. load_module()이 로드할 수 없는 모듈을 로드하도록 요청받으면 ImportError를 발생시켜야 합니다.

많은 경우 파인더와 로더는 하나의 같은 객체일 수 있습니다. 이때 finder.find_module()은 그냥 self를 반환합니다.

두 메서드의 fullname 인자는 완전한 정규화된 모듈 이름으로, 예를 들면 “spam.eggs.ham”입니다. 앞서 설명했듯이 finder.find_module("spam.eggs.ham")이 호출될 때에는 “spam.eggs”가 이미 임포트되어 sys.modules에 추가되어 있습니다. 그러나 find_module() 메서드가 실제 임포트 중에 항상 호출되는 것은 아닙니다. 임포트 의존성을 분석하는 메타 도구(freeze, 설치 프로그램인 Installer, py2exe 등)는 실제로 모듈을 로드하지 않으므로, 파인더는 부모 패키지가 sys.modules에 있다는 것에 의존해서는 안 됩니다.

load_module() 메서드에는 코드를 실행하기 전에 반드시 이행해야 하는 몇 가지 책임이 있습니다.

  • sys.modules에 ‘fullname’이라는 이름의 기존 모듈 객체가 있으면, 로더는 그 기존 모듈을 사용해야 합니다. (그렇지 않으면 내장 함수 reload()가 올바르게 동작하지 않습니다.) sys.modules에 ‘fullname’이라는 이름의 모듈이 없으면, 로더는 새 모듈 객체를 만들어 sys.modules에 추가해야 합니다.

    로더가 모듈 코드를 실행하기 전에 모듈 객체가 sys.modules에 반드시 있어야 한다는 점에 유의하십시오. 이는 매우 중요한데, 모듈 코드가 (직접 또는 간접적으로) 자기 자신을 임포트할 수 있기 때문입니다. 미리 sys.modules에 추가해 두면 최악의 경우에는 무한 재귀를, 최선의 경우에도 중복 로딩을 방지할 수 있습니다.

    로드에 실패하면 로더는 자신이 sys.modules에 삽입했을 수 있는 모든 모듈을 제거해야 합니다. 모듈이 이미 sys.modules에 있었다면 로더는 그 모듈을 그대로 두어야 합니다.

  • __file__ 속성을 설정해야 합니다. 이 값은 문자열이어야 하지만, 예를 들어 “<frozen>”과 같은 더미 값이어도 됩니다. __file__ 속성을 아예 갖지 않는 특권은 내장 모듈에만 주어집니다.
  • __name__ 속성을 설정해야 합니다. imp.new_module()을 사용하면 이 속성이 자동으로 설정됩니다.
  • 패키지인 경우에는 __path__ 변수를 반드시 설정해야 합니다. 이 값은 리스트여야 하지만, __path__가 임포터에게 더 이상 의미가 없다면 비어 있어도 됩니다(이에 대해서는 뒤에서 자세히 설명합니다).
  • __loader__ 속성은 로더 객체로 설정해야 합니다. 이는 주로 인트로스펙션과 다시 로드하기를 위한 것이지만, 임포터에 연관된 데이터를 가져오는 것과 같은 임포터별 부가 기능에도 사용할 수 있습니다.
  • __package__ 속성을 설정해야 합니다(PEP 366).

    모듈이 (내장 모듈이나 동적으로 로드되는 확장과 달리) Python 모듈이라면, 모듈의 전역 네임스페이스(module.__dict__)에서 모듈의 코드를 실행해야 합니다.

    다음은 load_module() 메서드를 위한 최소한의 패턴입니다:

    # Consider using importlib.util.module_for_loader() to handle
    # most of these details for you.
    def load_module(self, fullname):
        code = self.get_code(fullname)
        ispkg = self.is_package(fullname)
        mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
        mod.__file__ = "<%s>" % self.__class__.__name__
        mod.__loader__ = self
        if ispkg:
            mod.__path__ = []
            mod.__package__ = fullname
        else:
            mod.__package__ = fullname.rpartition('.')[0]
        exec(code, mod.__dict__)
        return mod
    

명세 2부: 훅 등록하기

임포트 훅에는 메타 훅과 경로 훅이라는 두 가지 유형이 있습니다. 메타 훅은 임포트 처리가 시작될 때 다른 어떤 임포트 처리보다도 먼저 호출됩니다(따라서 메타 훅은 sys.path 처리, 프로즌 모듈, 심지어 내장 모듈까지도 재정의할 수 있습니다). 메타 훅을 등록하려면 파인더 객체를 sys.meta_path (등록된 메타 훅의 리스트)에 추가하기만 하면 됩니다.

경로 훅은 sys.path (또는 package.__path__) 처리의 일부로, 연관된 경로 항목을 만나는 시점에 호출됩니다. 경로 훅은 sys.path_hooks에 임포터 팩토리를 추가하여 등록합니다.

sys.path_hooks는 호출 가능 객체의 리스트이며, 주어진 경로 항목을 처리할 수 있는지 판단하기 위해 차례대로 검사됩니다. 호출 가능 객체는 경로 항목이라는 하나의 인자로 호출됩니다. 호출 가능 객체는 경로 항목을 처리할 수 없으면 ImportError를 발생시켜야 하고, 경로 항목을 처리할 수 있으면 임포터 객체를 반환해야 합니다. 호출 가능 객체가 특정 sys.path 항목에 대해 임포터 객체를 반환하면, 나중에 그 임포터 객체가 특정 모듈을 찾지 못하더라도 해당 항목을 처리하기 위해 내장 임포트 메커니즘이 더 이상 호출되지 않는다는 점에 유의하십시오. 호출 가능 객체는 일반적으로 임포트 훅의 클래스이며, 따라서 클래스의 __init__() 메서드가 호출됩니다. (이것이 ImportError를 발생시켜야 하는 이유이기도 합니다. __init__() 메서드는 아무것도 반환할 수 없기 때문입니다. 새 스타일 클래스에서는 __new__() 메서드로 이것이 가능하겠지만, 우리는 훅이 구현되는 방식에 대해 어떤 것도 요구하고 싶지 않습니다.)

경로 훅 검사 결과는 sys.path_importer_cache에 캐시되는데, 이는 경로 항목을 임포터 객체에 매핑하는 딕셔너리입니다. 캐시는 sys.path_hooks를 탐색하기 전에 검사됩니다. sys.path_hooks를 강제로 다시 탐색해야 한다면, sys.path_importer_cache의 전부 또는 일부를 수동으로 비울 수 있습니다.

sys.path 자체와 마찬가지로, 새 sys 변수들도 특정 타입을 가져야 합니다.

  • sys.meta_path와 sys.path_hooks는 Python 리스트여야 합니다.
  • sys.path_importer_cache는 Python dict여야 합니다.

이 변수들을 제자리에서 수정하는 것도, 새 객체로 교체하는 것도 허용됩니다.

패키지와 __path__의 역할

모듈에 __path__ 속성이 있으면 임포트 메커니즘은 그 모듈을 패키지로 취급합니다. 패키지의 하위 모듈을 임포트할 때는 sys.path 대신 __path__ 변수가 사용됩니다. 따라서 sys.path에 대한 규칙은 pkg.__path__에도 적용됩니다. 그러므로 pkg.__path__를 순회할 때도 sys.path_hooks를 참조합니다. 메타 임포터는 작업을 수행할 때 sys.path를 전혀 사용하지 않을 수도 있으므로 pkg.__path__의 값을 무시할 수 있습니다. 이 경우에도 이 값을 리스트로 설정하는 것이 권장되며, 이 리스트는 비어 있어도 됩니다.

임포터 프로토콜의 선택적 확장

임포터 프로토콜은 세 가지 선택적 확장을 정의합니다. 첫 번째는 데이터 파일을 가져오는 것이고, 두 번째는 모듈 패키징 도구 및/또는 모듈 의존성을 분석하는 도구(예: Freeze)를 지원하는 것이며, 마지막은 모듈을 스크립트로 실행하는 것을 지원하는 것입니다. 뒤의 두 범주의 도구는 보통 모듈을 실제로 로드하지 않으며, 모듈을 사용할 수 있는지와 어디에 있는지만 알면 됩니다. 범용 임포터에는 세 가지 확장을 모두 구현할 것을 강력히 권장하지만, 해당 기능이 필요하지 않다면 생략해도 무방합니다.

하부 저장소 백엔드에서 임의의 “파일”에 대한 데이터를 가져오기 위해, 로더 객체는 get_data()라는 이름의 메서드를 제공할 수 있습니다:

loader.get_data(path)

이 메서드는 데이터를 문자열로 반환하거나, “파일”을 찾지 못하면 IOError를 발생시킵니다. 데이터는 항상 “바이너리” 모드를 사용한 것처럼 반환됩니다. 예를 들어 텍스트 파일에 대한 CRLF 변환은 없습니다. 이 메서드는 파일 시스템과 유사한 속성을 가진 임포터를 위한 것입니다. ‘path’ 인자는 예를 들어 module.__file__(또는 pkg.__path__ 항목)을 os.path.* 함수로 가공하여 구성할 수 있는 경로입니다:

d = os.path.dirname(__file__)
data = __loader__.get_data(os.path.join(d, "logo.gif"))

(예를 들어) Freeze와 유사한 도구를 지원하고자 한다면 다음 메서드들을 구현할 수 있습니다. 이는 세 가지 추가 메서드로 구성되며, 호출자의 편의를 위해 세 메서드를 모두 구현하거나 아예 하나도 구현하지 않아야 합니다:

loader.is_package(fullname)
loader.get_code(fullname)
loader.get_source(fullname)

세 메서드 모두 모듈을 찾지 못하면 ImportError를 발생시켜야 합니다.

loader.is_package(fullname) 메서드는 ‘fullname’으로 지정된 모듈이 패키지이면 True를, 그렇지 않으면 False를 반환해야 합니다.

loader.get_code(fullname) 메서드는 모듈에 연결된 코드 객체를 반환해야 하며, 내장 모듈이나 확장 모듈인 경우에는 None을 반환해야 합니다. 로더에 코드 객체는 없지만 소스 코드는 있는 경우에는 소스 코드를 컴파일하여 반환해야 합니다. (이는 호출자가 코드 객체만 필요한 경우 get_source()까지 확인하지 않아도 되도록 하기 위함입니다.)

loader.get_source(fullname) 메서드는 모듈의 소스 코드를 문자열로(줄 끝에는 줄바꿈 문자를 사용하여) 반환하거나, 소스를 사용할 수 없으면 None을 반환해야 합니다(다만 임포터가 모듈을 아예 찾을 수 없는 경우에는 여전히 ImportError를 발생시켜야 합니다).

모듈을 스크립트로 실행하는 것(PEP 338)을 지원하려면, 모듈에 연결된 코드를 찾기 위한 위의 세 메서드를 반드시 구현해야 합니다. 이 메서드들 외에도, runpy 모듈이 __file__ 속성을 올바르게 설정할 수 있도록 다음 메서드를 제공할 수 있습니다:

loader.get_filename(fullname)

이 메서드는 지정된 모듈이 로드되었을 경우 __file__에 설정될 값을 반환해야 합니다. 모듈을 찾지 못하면 ImportError를 발생시켜야 합니다.

‘imp’ 모듈과의 통합

새 임포트 훅은 기존 imp.find_module() 및 imp.load_module() 호출에 쉽게 통합되지 않습니다. 코드를 망가뜨리지 않고 그렇게 하는 것이 애초에 가능한지조차 의문이므로, imp 모듈에 새 함수를 추가하는 편이 낫습니다. 기존 imp.find_module() 및 imp.load_module() 호출의 의미는 “내장 임포트 메커니즘을 노출하는 것”에서 “기본적인 훅이 적용되지 않은 내장 임포트 메커니즘을 노출하는 것”으로 바뀝니다. 이 호출들은 어떤 임포트 훅도 호출하지 않습니다. get_loader()라는 이름의 새 imp 모듈 함수가 제안되었으며(아직 구현되지는 않았습니다), 다음 패턴처럼 사용됩니다:

loader = imp.get_loader(fullname, path)
if loader is not None:
    loader.load_module(fullname)

imp.find_module() 함수가 처리할 수 있는 “기본” 임포트의 경우, 로더 객체는 imp.find_module()의 현재 출력을 감싸는 래퍼가 되고, loader.load_module()은 그 출력을 사용하여 imp.load_module()을 호출합니다.

이 래퍼는 현재 아직 구현되지 않았다는 점에 유의하십시오. 다만 패치에 포함된 test_importhooks.py 스크립트(ImpWrapper 클래스)에 Python 프로토타입이 있습니다.

상위 호환성

기존 __import__ 훅은 대체 수단으로 원래의 __import__ 함수를 호출하지 않는 한, 새로운 방식의 훅을 저절로 호출하지 않습니다. 예를 들어 ihooks.py, iu.py, imputil.py는 이런 의미에서 이 PEP와 상위 호환되지 않습니다.

미해결 문제

모듈은 제 역할을 하기 위해 보조 데이터 파일을 필요로 하는 경우가 많으며, 특히 복잡한 패키지나 완전한 애플리케이션의 경우 더욱 그렇습니다. 현재 관행은 일반적으로 sys.path(또는 package.__path__ 속성)를 통해 이러한 파일을 찾는 것입니다. 이 방식은 일반적으로 임포트 훅을 통해 로드된 모듈에서는 작동하지 않습니다.

이 문제를 해결할 수 있는 방법은 여러 가지가 있습니다:

  • “그렇게 하지 마십시오.” 패키지가 __path__를 통해 데이터 파일을 찾아야 한다면, 그 패키지는 임포트 훅을 통한 로드에 적합하지 않습니다. 그런 패키지도 지금처럼 sys.path에 있는 디렉터리에 둘 수 있으므로, 이를 중대한 문제로 볼 필요는 없습니다.
  • 모듈 파일을 기준으로 한 상대 위치가 아니라 표준 위치에서 데이터 파일을 찾으십시오. 비교적 간단한 방법(distutils에서 지원합니다)은 sys.prefix(또는 sys.exec_prefix)를 기준으로 데이터 파일을 찾는 것입니다. 예를 들어 os.path.join(sys.prefix, "data", package_name)에서 찾는 방식입니다.
  • 임포트 훅이 모듈 파일을 기준으로 한 상대 위치로 데이터 파일에 접근하는 표준 방법을 제공할 수 있습니다. 표준 zipimport 객체는 name이라는 “파일”의 내용을 문자열로 반환하는 get_data(name) 메서드를 제공합니다. 모듈이 임포터 객체에 접근할 수 있도록 zipimport는 모듈에 __loader__ 속성도 추가하며, 이 속성에는 모듈을 로드하는 데 사용된 zipimport 객체가 들어 있습니다. 이런 방식을 사용한다면 get_data() 메서드를 사용할 수 없을 때 클라이언트 코드가 망가지지 않도록 주의하는 것이 중요하므로, 이 방식이 이 문제에 대한 일반적인 해답이 되는지는 분명하지 않습니다.

python-dev에서 임포터로부터 사용 가능한 모듈 목록 및/또는 get_data() 메서드에 사용할 수 있는 데이터 파일 목록을 받을 수 있으면 유용하겠다는 제안이 있었습니다. 프로토콜에 list_modules()와 list_files() 같은 두 가지 확장을 추가할 수 있습니다. 후자는 get_data() 메서드가 있는 로더 객체에서 의미가 있습니다. 하지만 list_modules()를 어느 객체가 구현해야 하는지는 다소 불분명합니다. 임포터입니까, 로더입니까, 아니면 둘 다입니까?

이 PEP는 다른 위치에서 모듈을 로드하는 쪽에 치우쳐 있습니다. 현재로서는 다른 파일 형식의 모듈을 로드하거나 다른 컴파일러로 모듈을 로드하기 위한 전용 해결책을 제공하지 않습니다. 반면 표준 라이브러리의 ihooks 모듈에는 이를 위한 꽤 간단한 방법이 있습니다. Quixote 프로젝트 [7]는 이 기법을 사용하여 PTL 파일을 일반 Python 모듈처럼 임포트합니다. 새로운 훅으로 같은 일을 하려면 ihooks의 일부를 새로운 방식의 임포터로 구현하는 새 모듈을 추가하거나, 훅을 걸 수 있는 내장 경로 임포터 객체를 추가해야 합니다.

이 PEP에는 훅을 “쌓는” 것에 대한 특별한 지원이 없습니다. 예를 들어 .tar 파일에서 모듈을 로드하는 훅과 .gz 파일에서 모듈을 로드하는 훅을 각각 따로 두고 이를 조합하여 tar.gz 파일에서 모듈을 로드하는 훅을 작성하는 방법은 분명하지 않습니다. 그러나 기존 훅 메커니즘(기본적인 “__import__ 교체” 방식이든, 기존 임포트 훅 모듈 중 어느 것이든)에도 이러한 쌓기에 대한 지원은 없으므로, 이 기능이 새 메커니즘에 반드시 필요한 요구 사항이라고 보기는 어렵습니다. 다만 향후 개선 사항으로 고려해 볼 만합니다.

(sys.meta_path를 통해) sys.path가 처리되기 전에 실행되는 훅을 추가할 수 있습니다. 그러나 sys.path가 처리된 후에 실행되는 훅을 추가하는 동등한 방법은 없습니다. 당분간은 sys.path가 처리된 후에 훅이 필요하다면, sys.path 끝에 임의의 “쿠키” 문자열을 추가하고 일반적인 sys.path_hooks 처리를 통해 필요한 훅을 이 쿠키와 연결하는 방식으로 이를 흉내 낼 수 있습니다. 장기적으로는 경로 처리 코드가 sys.meta_path의 “진짜” 훅이 될 것이며, 그 단계에서는 사용자 정의 훅을 그 앞이나 뒤에 삽입할 수 있게 됩니다.

구현

PEP 302 구현은 2.3a1부터 Python에 통합되었습니다. 이전 버전은 패치 #652586 [9]로 제공되지만, 더 흥미로운 점은 해당 이슈에 개발과 설계의 역사가 꽤 자세히 담겨 있다는 것입니다.

PEP 273은 PEP 302의 임포트 훅을 사용하여 구현되었습니다.

참고 문헌 및 각주