PEP 402 – 단순화된 패키지 레이아웃과 분할
- Author:
- Phillip J. Eby
- Status:
- Rejected
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 12-Jul-2011
- Python-Version:
- 3.3
- Post-History:
- 20-Jul-2011
- Replaces:
- 382
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 공지
US PyCon 2012 스프린트 첫날, 우리는 PEP 382와 PEP 402에 대해 길고 유익한 논의를 했습니다. 결국 둘 다 거부하기로 했지만, PEP 402의 정신을 이어 가는 새 PEP가 작성될 것입니다. Martin von Löwis가 요약을 작성했습니다: [3].
초록
이 PEP는 Python의 패키지 임포트를 다음과 같이 개선할 것을 제안합니다:
- 다른 언어 사용자가 덜 놀라도록 하고,
- 모듈을 패키지로 더 쉽게 전환할 수 있도록 하며,
- 패키지를 별도로 설치되는 구성 요소로 나누는 것을 지원합니다(PEP 382에서 설명한 “네임스페이스 패키지”와 같은 방식으로)
제안된 개선 사항은 현재 임포트할 수 있는 어떤 디렉터리 레이아웃의 의미도 바꾸지 않지만, 패키지가 (현재는 임포트할 수 없는) 단순화된 디렉터리 레이아웃을 사용할 수 있게 합니다.
그러나 제안된 변경 사항은 기존 모듈이나 패키지를 임포트할 때 성능 오버헤드를 전혀 추가하지 않으며, 새로운 디렉터리 레이아웃의 성능도 이전의 “네임스페이스 패키지” 해법(pkgutil.extend_path() 등)과 거의 같을 것입니다.
문제
“대부분의 패키지는 모듈과 같습니다. 그 내용은 서로 강하게 의존하고 있어서 떼어 낼 수 없습니다. [하지만] 일부 패키지는 별도의 네임스페이스를 제공하기 위해 존재합니다. … 이러한 [네임스페이스 패키지]의 하위 패키지나 하위 모듈은 독립적으로 배포할 수 있어야 합니다.”—Jim Fulton, Python 2.3 릴리스 직전 [1]
다른 언어에서 Python으로 넘어온 새 사용자는 Python의 패키지 임포트 의미 때문에 혼란을 겪는 경우가 많습니다. 예를 들어 Google에서 Guido는 “쇠스랑을 든 대규모 군중” [2]으로부터, 패키지가 __init__ 모듈을 포함해야 한다는 요구 사항은 “잘못된 기능”이므로 없애야 한다는 불만을 받았습니다.
또한 Java나 Perl 같은 언어에서 넘어온 사용자는 Python의 임포트 경로 탐색 방식의 차이 때문에 혼란을 겪기도 합니다.
Python의 sys.path와 비슷한 경로 메커니즘을 가진 대부분의 다른 언어에서 패키지는 모듈이나 클래스를 담는 네임스페이스일 뿐이므로, 해당 언어의 경로에 있는 여러 디렉터리에 걸쳐 나뉘어 있을 수 있습니다. 예를 들어 Perl에서 Foo::Bar 모듈은 처음 발견된 Foo/ 하위 디렉터리에서만이 아니라, 모듈 포함 경로 전체에 걸친 Foo/ 하위 디렉터리에서 검색됩니다.
더 나쁜 것은, 이것이 새 사용자만의 문제가 아니라는 점입니다: 이 때문에 누구도 패키지를 별도로 설치할 수 있는 구성 요소로 쉽게 나눌 수 없습니다. Perl로 말하자면, CPAN에 있을 수 있는 모든 Net:: 모듈을 하나의 tarball로 묶어서 배포해야 하는 것과 같습니다!
그래서 후자의 제약을 우회하는 다양한 방법이 존재하며, “네임스페이스 패키지”라는 용어로 통용되고 있습니다. Python 표준 라이브러리는 Python 2.3부터 이러한 우회 방법 중 하나를 (pkgutil.extend_path() 함수를 통해) 제공해 왔으며, “setuptools” 패키지도 (pkg_resources.declare_namespace()를 통해) 또 다른 방법을 제공합니다.
그러나 이러한 우회 방법 자체도 Python이 파일 시스템에 패키지를 배치하는 방식이 가진 세 번째 문제를 피하지 못합니다.
패키지는 __init__ 모듈을 반드시 포함해야 하므로, 해당 패키지의 모듈을 배포하려는 모든 시도는 그 모듈들을 임포트할 수 있게 하려면 필연적으로 그 __init__ 모듈을 포함해야 합니다.
그런데 한 패키지의 모듈 배포판마다 이 (중복된) __init__ 모듈을 포함해야 한다는 바로 그 사실 때문에, 이러한 모듈 배포판을 패키징하는 OS 벤더는 여러 모듈 배포판이 그 __init__ 모듈을 파일 시스템의 같은 위치에 설치하면서 발생하는 충돌을 어떻게든 처리해야 합니다.
이로 인해 PEP 382 (“네임스페이스 패키지”)가 제안되었습니다. 이는 모듈 배포마다 고유한 파일 이름을 사용하여 디렉터리를 임포트할 수 있음을 Python의 임포트 메커니즘에 알리는 방법입니다.
그러나 이 접근 방식에는 단점이 하나 이상 있었습니다. 모든 임포트 작업의 성능에 영향을 주게 되며, 패키지를 지정하는 과정은 훨씬 더 복잡해졌습니다. 해법을 설명하기 위해 새로운 용어를 만들어 내야 했고, 그 밖에도 여러 문제가 있었습니다.
Import-SIG에서 용어에 관한 논의가 계속되면서, “네임스페이스 패키지”와 관련된 개념을 설명하기가 그토록 어려웠던 주된 이유가 Python의 현재 패키지 처리 방식이 다른 언어에 비해 다소 기능이 부족하기 때문이라는 점이 곧 분명해졌습니다.
즉, 패키지 시스템을 갖춘 다른 인기 있는 언어에서는 “네임스페이스 패키지”를 설명하기 위한 특별한 용어가 필요하지 않습니다. 모든 패키지가 일반적으로 원하는 방식대로 동작하기 때문입니다.
다른 언어의 패키지는 (Python처럼) 특수한 표지 모듈을 가진 고립된 단일 디렉터리가 아니라, 일반적으로 전체 임포트 경로나 포함 경로에 걸쳐 있는 적절한 이름의 디렉터리들의 합집합일 뿐입니다.
예를 들어 Perl에서 Foo 모듈은 항상 Foo.pm 파일에서 발견되고, Foo::Bar 모듈은 항상 Foo/Bar.pm 파일에서 발견됩니다. (다시 말해, 특정 모듈의 위치를 찾는 한 가지 명백한 방법이 있습니다.)
이는 Perl이 모듈을 패키지와 다른 것으로 간주하기 때문입니다. 패키지는 순전히 다른 모듈들이 자리할 수 있는 네임스페이스이며, 우연히 모듈의 이름이기도 할 뿐입니다.
그러나 현재 버전의 Python에서는 모듈과 패키지가 더 긴밀하게 묶여 있습니다. Foo는 Foo.py에서 발견되든 Foo/__init__.py에서 발견되든 항상 모듈이며, (하위 모듈이 있다면) 자신의 하위 모듈과 긴밀하게 연결되어 있고, 그 하위 모듈은 __init__.py가 발견된 바로 그 디렉터리에 반드시 있어야 합니다.
긍정적인 측면에서, 이 설계 선택 덕분에 패키지는 상당히 자기 완결적이며, 패키지의 루트 디렉터리에 대해 작업을 수행하는 것만으로 하나의 단위로서 설치, 복사 등을 할 수 있습니다.
하지만 부정적인 측면에서, 이는 초보자에게 직관적이지 않으며, 모듈을 패키지로 바꾸려면 더 복잡한 단계를 거쳐야 합니다. Foo가 처음에 Foo.py로 시작했다면, 이를 옮기고 Foo/__init__.py로 이름을 바꿔야 합니다.
반대로, 처음부터 Foo.Bar 모듈을 만들려고 하지만 Foo 자체에 넣을 특별한 모듈 내용이 없다면, 단지 Foo.Bar를 임포트할 수 있도록 하기 위해 비어 있고 겉보기에 무관해 보이는 Foo/__init__.py 파일을 만들어야 합니다.
(그리고 이러한 문제는 언어를 처음 접하는 사람들만 혼란스럽게 하는 것이 아닙니다. 많은 숙련된 개발자들도 이 문제로 불편을 겪습니다.)
그래서 Import-SIG에서 얼마간 논의를 거친 후, “네임스페이스 패키지” 사용 사례뿐 아니라 위의 문제 모두를 해결하기 위한 시도로서 PEP 382의 대안으로 이 PEP가 만들어졌습니다.
그리고 반가운 부수 효과로, 이 PEP에서 제안하는 해법은 일반 모듈이나 자기 완결적인(즉, __init__ 기반) 패키지의 임포트 성능에 영향을 주지 않습니다.
해법
과거에 패키지 디렉터리 배치에 대해 더 직관적인 접근 방식을 허용하자는 다양한 제안이 있었습니다. 그러나 대부분은 명백한 하위 호환성 문제 때문에 실패했습니다.
즉, __init__ 모듈에 대한 요구 사항을 단순히 없애 버리면, 예컨대 sys.path에 있는 string이라는 이름의 디렉터리가 표준 라이브러리 string 모듈의 임포트를 가로막을 가능성이 생깁니다.
하지만 역설적이게도, 이 접근 방식의 실패는 __init__ 요구 사항을 없앤 데서 비롯되는 것이 아닙니다!
오히려 실패의 원인은, 바탕에 깔린 접근 방식이 패키지를 두 가지가 아닌 단 하나의 것으로 당연하게 여긴다는 데 있습니다.
사실 패키지는 서로 별개이지만 관련된 두 개체로 이루어집니다. 바로 (자체적인 선택적 내용을 지닌) 모듈과, 다른 모듈이나 패키지를 찾을 수 있는 네임스페이스입니다.
그러나 현재 버전의 Python에서는 (__init__에 있는) 모듈 부분과 (__path__ 속성으로 표현되는) 하위 모듈 임포트용 네임스페이스가 패키지를 처음 임포트할 때 동시에 초기화됩니다.
그리고 이것이 이 두 가지를 초기화하는 유일한 방법이라고 가정하면, 기존 디렉터리 배치와의 하위 호환성을 유지하면서 __init__ 모듈의 필요성을 없앨 방법은 없습니다.
어쨌든 sys.path에서 원하는 이름과 일치하는 디렉터리를 만나는 순간 패키지를 “찾은” 것이니, 검색을 멈춰야 하지 않겠습니까?
사실은 꼭 그렇지만은 않습니다.
사고 실험
잠시 타임머신을 타고, Python 패키지와 __init__.py가 발명되기 직전인 1990년대 초로 돌아갔다고 가정해 봅시다. 하지만 우리가 Perl 방식의 패키지 임포트에 실제로 익숙하며, Python에도 비슷한 시스템을 구현하고 싶어 한다고 상상해 봅시다.
그래도 Python의 모듈 임포트를 기반으로 삼을 수 있으므로, Foo 패키지의 부모 Foo 모듈로 Foo.py를 두는 방식은 충분히 생각해 볼 수 있습니다. 하지만 하위 모듈과 하위 패키지 임포트는 어떻게 구현하겠습니까?
음, 아직 __path__ 속성이라는 개념이 없었다면, 아마 그냥 sys.path를 검색하여 Foo/Bar.py를 찾았을 것입니다.
하지만 누군가 실제로 Foo.Bar를 임포트하려고 할 때에만 그렇게 할 것입니다.
Foo를 임포트할 때는 그렇게 하지 않습니다.
그리고 바로 그 점 덕분에, 지금 이곳 2011년에 __init__ 요구 사항을 없애는 데 따르는 하위 호환성 문제를 해결할 수 있습니다.
어떻게 말입니까?
음, import Foo를 할 때는 sys.path에서 Foo/ 디렉터리를 찾아보지조차 않습니다. 아직은 관심이 없기 때문입니다. 관심을 갖게 되는 유일한 시점은 누군가 실제로 Foo의 하위 모듈이나 하위 패키지를 임포트하려고 하는 시점입니다.
즉, (예를 들어) Foo가 표준 라이브러리 모듈이고, 제가 마침 sys.path에 (물론 __init__.py 없이) Foo 디렉터리를 갖고 있더라도, 아무것도 깨지지 않습니다. Foo 모듈은 여전히 그냥 모듈이며, 여전히 평소대로 임포트됩니다.
자체 완결형 패키지 대 “가상” 패키지
물론 오늘날의 Python에서는 Foo가 단지 Foo.py 모듈일 뿐이라면(따라서 __path__ 속성이 없다면) import Foo.Bar를 시도할 때 실패합니다.
그래서 이 PEP는 __path__가 없는 경우 이를 동적으로 생성할 것을 제안합니다.
즉, 제가 import Foo.Bar를 시도하면, 제안된 임포트 메커니즘 변경은 Foo 모듈에 __path__가 없다는 것을 알아차리고, 따라서 진행하기 전에 __path__를 만들려고 시도할 것입니다.
그리고 이는 sys.path에 나열된 디렉터리들의 하위 디렉터리 중 존재하는 모든 Foo/ 디렉터리의 목록을 만드는 방식으로 이루어질 것입니다.
목록이 비어 있으면, 오늘날과 마찬가지로 임포트는 ImportError를 일으키며 실패합니다. 하지만 목록이 비어 있지 않다면, 이 목록은 새로운 Foo.__path__ 속성에 저장되고, 그 모듈은 “가상 패키지”가 됩니다.
즉, 이제 유효한 __path__가 있으므로 평소와 같은 방식으로 하위 모듈이나 하위 패키지를 임포트할 수 있습니다.
이제 이 변경이 내부에 __init__ 모듈이 있는 “고전적인” 자체 완결형 패키지에는 영향을 주지 않는다는 점에 주목하십시오. 그런 패키지에는 이미 (임포트 시점에 초기화된) __path__ 속성이 있으므로, 임포트 메커니즘이 나중에 또 다른 것을 만들려고 하지 않습니다.
이는 (예를 들어) sys.path에 email이라는 이름의 관련 없는 디렉터리가 잔뜩 있더라도 표준 라이브러리 email 패키지는 전혀 영향을 받지 않는다는 뜻입니다. (그 디렉터리들에 *.py 파일이 들어 있더라도 말입니다.)
하지만 이는 Foo 모듈을 Foo 패키지로 바꾸고 싶다면, sys.path 어딘가에 Foo/ 디렉터리를 추가하고 거기에 모듈을 추가하기 시작하기만 하면 된다는 뜻이기도 합니다.
하지만 “네임스페이스 패키지”만 원한다면 어떻게 해야 합니까? 즉, 따로따로 배포되는 여러 하위 모듈과 하위 패키지를 위한 네임스페이스 역할만 하는 패키지를 원한다면 어떻습니까?
예를 들어 여러분이 Zope Corporation이고 zc.buildout 같은 개별 도구 수십 개를 각각 zc 네임스페이스 아래의 패키지로 배포한다면, 출시하는 모든 도구에 빈 zc.py를 만들어 넣어야 하는 상황은 원하지 않을 것입니다. (또한 여러분이 Linux나 다른 OS 공급업체라면, 같은 위치에 zc.py 사본 열 개를 설치하려다 생기는 패키지 설치 충돌을 처리하고 싶지 않을 것입니다!)
문제없습니다. 임포트 과정을 한 가지만 더 약간 조정하면 됩니다: “고전적인” 임포트 과정이 자체 완결형 모듈이나 패키지를 찾지 못하면(예를 들어 import zc가 zc.py나 zc/__init__.py를 찾지 못하면), sys.path에 있는 모든 zc/ 디렉터리를 검색하여 목록에 넣는 방식으로 다시 한번 __path__를 만들려고 시도합니다.
이 목록이 비어 있으면 ImportError를 발생시킵니다. 하지만 비어 있지 않다면, 빈 zc 모듈을 만들고 그 목록을 zc.__path__에 넣습니다. 축하합니다: 이제 zc는 네임스페이스 전용의 “순수 가상” 패키지입니다! 이 패키지에는 모듈 내용이 없지만, 하위 모듈과 하위 패키지가 sys.path의 어디에 있든 상관없이 여전히 이 패키지에서 임포트할 수 있습니다.
(그런데 임포트 프로토콜에 대한 이 두 가지 추가 사항(즉, 동적으로 추가되는 __path__와 동적으로 생성되는 모듈)은 모두 자식 패키지에 재귀적으로 적용되며, 자식 __path__를 생성하는 기반으로 sys.path 대신 부모 패키지의 __path__를 사용합니다. 이는 자체 완결형 패키지와 가상 패키지가 제한 없이 서로를 포함할 수 있다는 뜻인데, 단 자체 완결형 패키지 안에 가상 패키지를 넣으면 그 __path__는 정말 짧아질 것이라는 점에 유의하십시오!)
하위 호환성과 성능
이 두 변경은 현재라면 ImportError가 발생하는 임포트 작업에만 영향을 준다는 점에 주의하십시오. 그 결과, 가상 패키지와 관련 없는 임포트의 성능은 영향을 받지 않으며, 잠재적인 하위 호환성 문제도 매우 제한적입니다.
현재는 __path__가 없는 모듈에서 하위 모듈이나 하위 패키지를 임포트하려 하면 즉시 오류가 발생합니다. 그리고 물론, 현재 sys.path 어딘가에 zc.py나 zc/__init__.py가 없다면 import zc 역시 실패합니다.
따라서 잠재적인 하위 호환성 문제는 다음뿐입니다:
- 패키지 디렉터리에
__init__모듈이 있으리라 기대하거나,__init__모듈이 없는 디렉터리는 임포트할 수 없으리라 기대하거나,__path__속성이 정적이리라 기대하는 도구는 가상 패키지를 패키지로 인식하지 못합니다.(실제로 이는 도구가 가상 패키지를 지원하도록 갱신되어야 한다는 의미일 뿐입니다. 예를 들어 하드코딩된 파일 시스템 검색 대신
pkgutil.walk_modules()를 사용하는 식입니다.) - 특정 임포트가 실패하리라고 기대하는 코드는 이제 예상치 못한 동작을 할 수 있습니다. 대부분의 정상적인 비테스트 코드는 존재하지 않으리라 예상되는 것을 임포트하지 않으므로, 실제로 이런 경우는 상당히 드물 것입니다!
위의 내용에 대한 가장 큰 예외는 어떤 코드가 패키지를 임포트해 봄으로써 그 패키지가 설치되어 있는지 확인하려는 경우일 것입니다. 이 확인을 최상위 모듈을 임포트하는 것만으로 수행하고(즉, __version__이나 다른 속성을 확인하지 않고), 그리고 sys.path 어딘가에 찾으려는 패키지와 같은 이름의 디렉터리가 있으며, 그리고 실제로는 그 패키지가 설치되어 있지 않다면, 이러한 코드는 실제로 설치되지 않은 패키지가 설치되어 있다고 착각할 수 있습니다.
예를 들어, 누군가 다음 코드를 담은 스크립트(datagen.py)를 작성한다고 가정해 봅시다:
try:
import json
except ImportError:
import simplejson as json
그리고 이것을 다음과 같이 구성된 디렉터리에서 실행한다고 해 봅시다:
datagen.py
json/
foo.js
bar.js
json/ 하위 디렉터리가 단지 존재한다는 이유만으로 import json이 성공한다면, 코드는 json 모듈을 사용할 수 있다고 잘못 판단하고, 이후 오류와 함께 실패하게 됩니다.
하지만 지금까지 제시한 알고리즘을 한 가지만 조금 바꾸면 이러한 코너 케이스가 발생하지 않도록 간단히 막을 수 있습니다. (zc 같은) “순수 가상” 패키지의 임포트를 허용하는 대신, 가상 패키지의 내용물만 임포트하도록 허용합니다.
즉, sys.path에 zc.py나 zc/__init__.py가 없다면 import zc 같은 문은 ImportError를 발생시켜야 합니다. 하지만 sys.path에 zc/buildout.py나 zc/buildout/__init__.py가 있기만 하면 import zc.buildout은 여전히 성공해야 합니다.
다시 말해, 순수 가상 패키지를 직접 임포트하는 것은 허용하지 않고, 모듈과 자기 완결적인 패키지만 허용합니다. (그러한 패키지를 단독으로 임포트하는 것에는 기능적인 가치가 없으므로, 이는 수용 가능한 제약입니다. 어쨌든 해당 모듈 객체는 그 하위 패키지나 하위 모듈 중 적어도 하나를 임포트하기 전까지는 아무런 내용물도 갖지 않기 때문입니다!)
하지만 일단 zc.buildout이 성공적으로 임포트되고 나면 sys.modules에 zc 모듈이 존재하게 되며, 이를 임포트하려는 시도는 물론 성공합니다. 우리는 충돌하는 하위 디렉터리가 sys.path에 있을 때 임포트가 잘못 성공하는 거짓 양성을 막기 위해, 최초의 임포트가 성공하는 것만 막을 뿐입니다.
따라서 이 작은 변경으로 위의 datagen.py 예제는 올바르게 동작합니다. 이 코드가 import json을 수행할 때, json/ 디렉터리는 .py 파일을 포함하고 있더라도 그 존재만으로는 임포트 과정에 전혀 영향을 주지 않습니다. json/ 디렉터리는 여전히 import json.converter 같은 임포트를 시도하는 경우에만 검색됩니다.
한편, 디렉터리 트리를 순회하여 패키지와 모듈을 찾으려는 도구는 기존 pkgutil.walk_modules() API를 사용하도록 갱신할 수 있으며, 메모리에 있는 패키지를 검사해야 하는 도구는 아래의 Standard Library Changes/Additions 절에서 설명하는 다른 API를 사용해야 합니다.
명세
적어도 하나의 .을 포함하는 이름, 즉 부모 패키지가 있는 모듈을 임포트할 때의 기존 임포트 과정이 변경됩니다.
구체적으로, 부모 패키지가 존재하지 않거나, 존재하지만 __path__ 속성이 없다면, 먼저 (아래 Virtual Paths 절에 설명된 알고리즘에 따라) 부모 패키지를 위한 “가상 경로”를 생성하려고 시도합니다.
계산된 “가상 경로”가 비어 있으면, 현재와 마찬가지로 ImportError가 발생합니다. 하지만 비어 있지 않은 가상 경로를 얻으면, 하위 모듈이나 하위 패키지의 일반적인 임포트가 진행되며, 그 가상 경로를 사용하여 하위 모듈이나 하위 패키지를 찾습니다. (부모 패키지가 존재하고 __path__가 있었다면 부모의 __path__로 했을 것과 마찬가지입니다.)
하위 모듈이나 하위 패키지가 발견되면(아직 로드되지는 않았더라도), 부모 패키지가 (이전에 존재하지 않았다면) 생성되어 sys.modules에 추가되고, 그 __path__는 (아직 설정되지 않았다면) 계산된 가상 경로로 설정됩니다.
이렇게 하면 하위 모듈이나 하위 패키지를 실제로 로드할 때 부모 패키지가 존재하는 것으로 보이게 되며, 모든 상대 임포트가 올바르게 동작합니다. 그러나 하위 모듈이나 하위 패키지가 존재하지 않으면 부모 패키지는 생성되지 않으며, 독립 모듈이 (불필요한 __path__ 속성이 추가되어) 패키지로 변환되지도 않습니다.
덧붙여, 이 변경은 재귀적으로 적용되어야 한다는 점에 유의하십시오. 즉, foo와 foo.bar가 순수 가상 패키지라면 import foo.bar.baz는 foo.bar.baz가 발견될 때까지 기다린 뒤에야 foo와 foo.bar 모두의 모듈 객체를 생성해야 하며, 이때 둘을 함께 생성하면서 foo 모듈의 .bar 속성이 foo.bar 모듈을 가리키도록 올바르게 설정해야 합니다.
이렇게 하면 순수한 가상 패키지는 결코 직접 임포트할 수 없습니다. import foo나 import foo.bar 자체는 실패하며, 해당 모듈은 성공적으로 임포트된 하위 모듈이나 독립적인 하위 패키지를 가리키는 데 필요해질 때까지 sys.modules에 나타나지 않습니다.
가상 경로
가상 경로는 sys.path에서(최상위 모듈의 경우) 또는 부모 __path__에서(하위 모듈의 경우) 발견된 각 경로 항목에 대해 PEP 302 “임포터” 객체를 얻어 생성됩니다.
(참고: sys.meta_path 임포터는 sys.path나 __path__ 항목 문자열과 연관되어 있지 않으므로, 이러한 임포터는 이 과정에 참여하지 않습니다.)
각 임포터에 get_subpath() 메서드가 있는지 확인하고, 있으면 경로를 구성하는 대상 모듈/패키지의 전체 이름을 인자로 하여 그 메서드를 호출합니다. 반환값은 요청된 패키지의 하위 디렉터리를 나타내는 문자열이거나, 그런 하위 디렉터리가 없으면 None입니다.
임포터가 반환한 문자열은 발견된 순서대로, 구성 중인 경로 리스트에 추가됩니다. (None 값과 get_subpath() 메서드가 없는 경우는 그냥 건너뜁니다.)
이렇게 만들어진 리스트는 (비어 있든 아니든) 모듈 이름을 키로 하여 sys.virtual_package_paths 딕셔너리에 저장됩니다.
이 딕셔너리에는 두 가지 목적이 있습니다. 첫째, 가상 패키지의 하위 모듈을 두 번 이상 임포트하려고 시도하는 경우에 대비한 캐시 역할을 합니다.
둘째, 더 중요한 것은, 런타임에 sys.path를 확장하는 코드가 이 딕셔너리를 사용하여 임포트된 패키지의 __path__ 속성을 그에 맞게 갱신할 수 있다는 점입니다. (자세한 내용은 아래 Standard Library Changes/Additions를 참조하십시오.)
Python 코드로 나타내면 가상 경로 구성 알고리즘은 대략 다음과 같습니다:
def get_virtual_path(modulename, parent_path=None):
if modulename in sys.virtual_package_paths:
return sys.virtual_package_paths[modulename]
if parent_path is None:
parent_path = sys.path
path = []
for entry in parent_path:
# Obtain a PEP 302 importer object - see pkgutil module
importer = pkgutil.get_importer(entry)
if hasattr(importer, 'get_subpath'):
subpath = importer.get_subpath(modulename)
if subpath is not None:
path.append(subpath)
sys.virtual_package_paths[modulename] = path
return path
그리고 이와 같은 함수를 표준 라이브러리에서 예컨대 imp.get_virtual_path() 같은 형태로 공개하여, __import__ 대체물이나 sys.meta_path 훅을 만드는 사람들이 재사용할 수 있도록 해야 합니다.
표준 라이브러리 변경/추가 사항
pkgutil 모듈은 extend_path(), iter_modules() 등에 필요한 변경을 포함하여, 이 명세를 적절히 처리하도록 갱신되어야 합니다.
구체적으로 pkgutil에 제안하는 변경 및 추가 사항은 다음과 같습니다:
- 새로운
extend_virtual_paths(path_entry)함수: 이미 임포트된 기존 가상 패키지의__path__속성을 확장하여, 새sys.path항목에서 발견되는 모든 부분(portion)을 포함하도록 합니다. 이 함수는 런타임에sys.path를 확장하는 애플리케이션이, 예를 들어 플러그인 디렉터리나 egg를 경로에 추가할 때 호출해야 합니다.이 함수의 구현은
sys.virtual_package_paths를 위에서 아래로 단순하게 순회하면서,path_entry가sys.path에 추가되었다는 전제하에 해당 패키지의 가상 경로에 어떤 경로 항목을 추가해야 하는지 파악하기 위해 필요한get_subpath()호출을 수행합니다. (또는 서브패키지의 경우, 부모 패키지의 가상 경로를 기반으로 파생된 하위 경로 항목을 추가합니다.)(참고: 일반적인 경우에는 둘 다 같은
list객체이겠지만, 이 함수는sys.virtual_package_paths의 경로 값과sys.modules에 있는 해당 모듈의__path__속성을 모두 갱신해야 합니다.) - 새로운
iter_virtual_packages(parent='')함수:parent의 자식 가상 패키지를 산출(yield)함으로써,sys.virtual_package_paths로부터 가상 패키지를 위에서 아래로 순회할 수 있게 합니다. 예를 들어,iter_virtual_packages("zope")를 호출하면zope.app과zope.products를 산출할 수 있지만(이들이sys.virtual_package_paths에 나열된 가상 패키지라면),zope.foo.bar는 산출하지 않습니다. (이 함수는extend_virtual_paths()를 구현하는 데 필요하지만, 임포트된 가상 패키지를 검사해야 하는 다른 코드에도 잠재적으로 유용합니다.) ImpImporter.iter_modules()는 가상 패키지에서 발견된 모듈의 이름도 감지하여 산출하도록 변경되어야 합니다.
위의 변경 사항 외에도 zipimport 임포터의 iter_modules() 구현도 비슷하게 변경해야 합니다. (참고: 현재 버전의 Python은 이를 pkgutil의 심(shim)을 통해 구현하므로, 엄밀히 말하면 이 또한 pkgutil에 대한 변경입니다.)
끝으로, 앞의 것들 못지않게 중요한 점은 imp 모듈(또는 적절하다면 importlib)이 위의 Virtual Paths 절에서 설명한 알고리즘을 get_virtual_path(modulename, parent_path=None) 함수로 공개하여, __import__ 대체물을 만드는 사람들이 사용할 수 있도록 해야 한다는 것입니다.
구현 참고 사항
가상 패키지의 사용자, 개발자, 배포자를 위한 사항:
- 가상 패키지는 설정하고 사용하기 쉽지만, 자체 완결형 패키지가 적합한 경우도 여전히 있습니다. 반드시 필요한 것은 아니지만, 자체 완결형 패키지에
__init__모듈을 추가하면 패키지 사용자(그리고 Python 자체)가 그 패키지의 모든 코드를 그 하위 디렉터리 하나에서 찾을 수 있다는 것을 알 수 있습니다. 또한__all__을 정의하고, 공개 API를 노출하고, 패키지 수준의 독스트링을 제공하는 등 단순한 “네임스페이스” 패키지보다는 자체 완결형 프로젝트에 더 어울리는 일들을 할 수 있습니다. sys.virtual_package_paths에는 존재하지 않거나 아직 임포트되지 않은 패키지 이름에 대한 항목이 들어 있을 수 있습니다. 그 내용을 사용하는 코드는 이 딕셔너리의 모든 키가sys.modules에도 있다거나 그 이름을 임포트하면 반드시 성공한다고 가정해서는 안 됩니다.- 현재 자체 완결형인 패키지를 가상 패키지로 바꾸는 경우, 패키지 디렉터리에 저장된 데이터 파일을 찾는 데 더 이상 그 패키지의
__file__속성을 사용할 수 없다는 점에 유의해야 합니다. 그 대신__path__를 검색하거나, 원하는 파일과 같은 위치에 있는 하위 모듈의__file__을 사용하거나, 원하는 파일을 포함하는 자체 완결형 하위 패키지의__file__을 사용해야 합니다.(참고: 이 주의 사항은 현재 “네임스페이스 패키지”를 사용하는 기존 사용자에게도 이미 해당합니다. 즉, 원하는 데이터 파일이 어느 파티션에 있는지 알아야 한다는 것은 패키지를 분할할 수 있다는 데서 비롯되는 본질적인 결과입니다. 여기서 이를 언급하는 것은 단지 자체 완결형 패키지에서 가상 패키지로 전환하는 새로운 사용자도 이를 알 수 있도록 하기 위해서입니다.)
- XXX “순수 가상” 패키지의 __file__ 값은 무엇입니까?
None입니까? 임의의 문자열입니까? 끝에 구분자가 붙은 첫 번째 디렉터리의 경로입니까? 무엇으로 정하든 일부 코드는 깨지겠지만, 마지막 선택지라면 어떤 코드는 우연히 동작할 수도 있습니다. 그것은 좋은 일입니까, 나쁜 일입니까?
PEP 302 임포터 객체를 구현하는 분들을 위한 내용입니다.
iter_modules()메서드(pkgutil이 임포트 가능한 모듈과 패키지를 찾는 데 사용)를 지원하면서 가상 패키지 지원을 추가하려는 임포터는 표준 모듈과 패키지뿐 아니라 가상 패키지도 찾아서 나열하도록iter_modules()메서드를 수정해야 합니다. 이를 위해 임포터는 자신이 관할하는 범위에서 유효한 Python 식별자인 바로 아래 하위 디렉터리 이름을 모두 나열하기만 하면 됩니다.XXX 이렇게 하면 실제로는 패키지가 아닌 것이 많이 나열될 수 있습니다. 임포트 가능한 내용이 존재할 것을 요구해야 합니까? 그렇다면 얼마나 깊이 검색해야 하며, 링크 순환이나 다른 파일 시스템으로 넘어가는 것 등은 어떻게 방지해야 합니까? 난감합니다. 또한 가상 패키지가 나열되더라도 여전히 임포트할 수는 없으며, 이는 현재
pkgutil.walk_modules()가 구현된 방식에 문제가 됩니다.- “메타” 임포터(즉,
sys.meta_path에 놓인 임포터)는get_subpath()를 구현할 필요가 없습니다. 이 메서드는sys.path항목과__path__항목에 대응하는 임포터에 대해서만 호출되기 때문입니다. 메타 임포터가 가상 패키지를 지원하려면 전적으로 자신의find_module()구현 안에서 처리해야 합니다.안타깝게도 그러한 구현이 자신의 패키지 하위 경로를 다른 메타 임포터나
sys.path임포터의 하위 경로와 병합할 수 있을 가능성은 낮으므로, 메타 임포터에서 “가상 패키지 지원”이 무엇을 의미하는지는 현재 정의되어 있지 않습니다!(하지만 메타 임포터의 의도된 사용 사례는 일부 모듈에 대해 Python의 일반적인 임포트 과정을 완전히 대체하는 것이며, 현재 구현된 그러한 임포터의 수도 매우 적으므로 실제로 큰 문제가 될 가능성은 낮아 보입니다.)
참고 문헌
Copyright
This document has been placed in the public domain.