[Rejected] PEP 402 - Simplified Package Layout and Partitioning
원문 링크: PEP 402 - Simplified Package Layout and Partitioning
상태: Rejected 유형: Standards Track 작성일: 12-Jul-2011
PEP 402 – 패키지 레이아웃 및 분할 간소화
작성자: Phillip J. Eby 상태: Rejected (거부됨) 유형: Standards Track 주제: Packaging 작성일: 2011년 7월 12일 Python 버전: 3.3 대체하는 PEP: 382
거부 고지 (Rejection Notice)
이 PEP는 US PyCon 2012 스프린트에서 PEP 382와 함께 긴 논의 끝에 거부되었습니다. 하지만 PEP 402의 정신을 이어받는 새로운 PEP가 작성될 예정이었습니다. Martin von Löwis가 요약한 내용을 참고하십시오.
개요 (Abstract)
이 PEP는 Python의 패키지 임포트(import) 메커니즘을 다음과 같이 개선할 것을 제안합니다:
- 다른 언어 사용자들에게 덜 혼란을 주도록 합니다.
- 모듈을 패키지로 전환하는 것을 더 쉽게 만듭니다.
- 패키지를 독립적으로 설치 가능한 컴포넌트(PEP 382에서 설명하는 “네임스페이스 패키지”와 유사)로 분할할 수 있도록 지원합니다.
제안된 개선 사항은 현재 임포트 가능한 디렉토리 레이아웃의 의미를 변경하지 않지만, (현재는 임포트 불가능한) 간소화된 디렉토리 레이아웃을 패키지가 사용할 수 있도록 합니다.
이 변경 사항은 기존 모듈이나 패키지를 임포트하는 데 성능 오버헤드를 추가하지 않으며, 새로운 디렉토리 레이아웃의 성능은 이전의 “네임스페이스 패키지” 솔루션(pkgutil.extend_path()
등)과 거의 동일할 것입니다.
문제점 (The Problem)
Jim Fulton은 Python 2.3 출시 직전에 “대부분의 패키지는 모듈과 같습니다. 그 내용물은 서로 의존성이 높아 분리할 수 없습니다. [하지만] 일부 패키지는 별도의 네임스페이스를 제공하기 위해 존재합니다. … 이러한 [네임스페이스 패키지]의 서브 패키지 또는 서브 모듈은 독립적으로 배포할 수 있어야 합니다.”라고 언급했습니다.
새로운 사용자들이 다른 언어에서 Python으로 넘어올 때, Python의 패키지 임포트 의미론에 종종 혼란을 겪습니다. 예를 들어, Google에서 Guido는 패키지에 __init__
모듈이 포함되어야 한다는 요구사항이 “잘못된 기능”이며 삭제되어야 한다는 “성난 군중”으로부터 불만을 들었습니다.
또한, Java나 Perl과 같은 언어 사용자들은 Python의 임포트 경로 검색 방식의 차이로 인해 혼란을 겪기도 합니다. 대부분의 다른 언어에서는 Python의 sys.path
와 유사한 경로 메커니즘을 가지고 있는데, 패키지는 단순히 모듈이나 클래스를 포함하는 네임스페이스이며, 따라서 언어의 경로에 있는 여러 디렉토리에 걸쳐 분산될 수 있습니다. 예를 들어, Perl에서는 Foo::Bar
모듈이 모듈 포함 경로를 따라 모든 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처럼 특별한 마커 모듈을 가진 고립된 단일 디렉토리가 아니라, 다른 언어의 패키지는 일반적으로 전체 임포트 또는 포함 경로를 가로지르는 적절한 이름의 디렉토리들의 합집합(union)입니다. 예를 들어, Perl에서는 모듈 Foo
는 항상 Foo.pm
파일에서 발견되며, 모듈 Foo::Bar
는 항상 Foo/Bar.pm
파일에서 발견됩니다. (즉, 특정 모듈의 위치를 찾는 한 가지 명확한 방법이 있습니다.)
이는 Perl이 모듈을 패키지와 다르다고 간주하기 때문입니다. 패키지는 순전히 다른 모듈이 위치할 수 있는 네임스페이스이며, 우연히 모듈의 이름이기도 합니다.
하지만 현재 버전의 Python에서는 모듈과 패키지가 더 밀접하게 연결되어 있습니다. Foo
는 항상 모듈입니다. (Foo.py
에서 발견되든 Foo/__init__.py
에서 발견되든) 그리고 그 __init__.py
가 발견된 정확히 동일한 디렉토리에 서브 모듈(있는 경우)이 위치해야 합니다.
긍정적인 측면에서는, 이러한 설계 선택은 패키지가 상당히 자급자족(self-contained)하며, 패키지의 루트 디렉토리에 대한 작업을 수행하기만 하면 단일 단위로 설치, 복사 등이 가능하다는 것을 의미합니다.
그러나 부정적인 측면에서는, 초보자에게 직관적이지 않으며, 모듈을 패키지로 전환하려면 더 복잡한 단계를 거쳐야 합니다. Foo
가 Foo.py
로 시작했다면, Foo/__init__.py
로 이동하고 이름을 변경해야 합니다.
반대로, Foo.Bar
모듈을 처음부터 만들려고 하지만, Foo
자체에 넣을 특정 모듈 내용이 없다면, Foo.Bar
를 임포트할 수 있도록 비어 있고 겉보기에 무관해 보이는 Foo/__init__.py
파일을 생성해야 합니다.
(그리고 이러한 문제들은 언어 초보자들만을 혼란스럽게 하는 것이 아닙니다. 많은 숙련된 개발자들도 이것에 짜증을 냅니다.)
그래서 Import-SIG에서 논의를 거쳐, 이 PEP는 PEP 382의 대안으로, “네임스페이스 패키지” 사용 사례뿐만 아니라 위의 모든 문제들을 해결하기 위한 시도로 만들어졌습니다.
그리고 즐거운 부수 효과로, 이 PEP에서 제안된 해결책은 일반 모듈이나 자급자족(즉, __init__
기반) 패키지의 임포트 성능에 영향을 미치지 않습니다.
해결책 (The Solution)
과거에는 패키지 디렉토리 레이아웃에 대한 보다 직관적인 접근 방식을 허용하기 위한 다양한 제안이 있었습니다. 그러나 대부분은 명백한 하위 호환성 문제로 인해 실패했습니다.
즉, __init__
모듈에 대한 요구사항이 단순히 삭제되면, 예를 들어 sys.path
에 string
이라는 디렉토리가 표준 라이브러리 string
모듈의 임포트를 차단할 가능성이 열릴 수 있었습니다.
그러나 역설적으로, 이 접근 방식의 실패는 __init__
요구사항의 제거에서 비롯된 것이 아닙니다! 오히려, 실패는 근본적인 접근 방식이 패키지를 두 가지가 아닌 단지 ‘하나’의 것으로 당연하게 여기기 때문에 발생합니다.
사실, 패키지는 두 개의 분리되어 있지만 관련된 엔티티로 구성됩니다:
- 모듈 (자체적인, 선택적 내용을 가짐)
- 다른 모듈이나 패키지를 찾을 수 있는 네임스페이스
그러나 현재 버전의 Python에서는 모듈 부분(__init__
에서 발견됨)과 서브 모듈 임포트를 위한 네임스페이스(__path__
속성으로 표현됨)가 패키지가 처음 임포트될 때 동시에 초기화됩니다.
그리고 이 두 가지를 초기화하는 유일한 방법이라고 가정하면, 기존 디렉토리 레이아웃과의 하위 호환성을 유지하면서 __init__
모듈의 필요성을 없앨 방법이 없습니다.
결국, sys.path
에서 원하는 이름과 일치하는 디렉토리를 발견하는 즉시, 패키지를 “찾았다”는 의미이며, 검색을 중단해야 합니다. 그렇죠? 음, 꼭 그렇지는 않습니다.
사고 실험 (A Thought Experiment)
잠시 타임머신을 타고 1990년대 초반, Python 패키지와 __init__.py
가 발명되기 직전으로 돌아가 봅시다. 하지만 우리는 Perl과 유사한 패키지 임포트에 익숙하며 Python에서 유사한 시스템을 구현하고 싶다고 상상해 봅시다.
우리는 여전히 Python의 모듈 임포트에 기반을 두고 있으므로, Foo.py
를 Foo
패키지의 부모 Foo
모듈로 가질 수 있다고 확실히 생각할 수 있습니다. 하지만 서브 모듈 및 서브 패키지 임포트를 어떻게 구현할까요?
글쎄요, 아직 __path__
속성 개념이 없었다면, 아마도 sys.path
를 검색해서 Foo/Bar.py
를 찾았을 것입니다. 하지만 그것은 누군가가 실제로 Foo.Bar
를 임포트하려고 할 때만 그렇게 했을 것입니다. Foo
를 임포트할 때가 아닙니다.
그리고 이것은 2011년 현재 우리가 __init__
요구사항을 삭제함으로써 발생하는 하위 호환성 문제를 해결할 수 있게 해줍니다. 어떻게 그럴 수 있을까요?
음, 우리가 Foo
를 임포트할 때, sys.path
에서 Foo/
디렉토리를 찾지도 않습니다. 왜냐하면 아직 신경 쓸 필요가 없기 때문입니다. 우리가 신경 쓰는 유일한 시점은 누군가가 실제로 Foo
의 서브 모듈이나 서브 패키지를 임포트하려고 할 때입니다.
이는 만약 Foo
가 표준 라이브러리 모듈이고 (예를 들어), sys.path
에 Foo
디렉토리가 있다고 해도 (__init__.py
없이), 아무것도 깨지지 않는다는 의미입니다. Foo
모듈은 여전히 모듈일 뿐이며, 여전히 정상적으로 임포트됩니다.
자급자족 패키지(Self-Contained) 대 “가상” 패키지 (Virtual Packages)
물론, 오늘날의 Python에서는 Foo
가 단순히 Foo.py
모듈(따라서 __path__
속성이 없음)이라면 Foo.Bar
를 임포트하려는 시도는 실패할 것입니다.
그래서 이 PEP는 __path__
가 없는 경우 동적으로 __path__
를 생성할 것을 제안합니다. 즉, Foo.Bar
를 임포트하려고 할 때, 임포트 메커니즘에 대한 제안된 변경 사항은 Foo
모듈에 __path__
가 없다는 것을 알아차리고 진행하기 전에 __path__
를 빌드하려고 시도할 것입니다.
그리고 이것은 sys.path
에 나열된 디렉토리의 모든 기존 Foo/
서브디렉토리 목록을 만들어서 수행할 것입니다.
목록이 비어 있으면 오늘날과 마찬가지로 ImportError
와 함께 임포트가 실패합니다. 그러나 목록이 비어 있지 않으면 새 Foo.__path__
속성에 저장되어 모듈을 “가상 패키지(virtual package)”로 만듭니다.
즉, 이제 유효한 __path__
를 가지므로, 일반적인 방식으로 서브 모듈이나 서브 패키지를 임포트할 수 있습니다.
이 변경 사항이 __init__
모듈을 포함하는 “고전적인” 자급자족 패키지에는 영향을 미치지 않는다는 점에 유의하십시오. 이러한 패키지는 이미 (임포트 시점에 초기화된) __path__
속성을 가지고 있으므로 임포트 메커니즘은 나중에 다른 __path__
를 생성하려고 시도하지 않을 것입니다.
이것은 (예를 들어) 표준 라이브러리 email
패키지가 sys.path
에 email
이라는 이름의 관련 없는 디렉토리들이 많이 있더라도 어떤 식으로든 영향을 받지 않는다는 것을 의미합니다. (심지어 *.py
파일을 포함하더라도 말입니다.)
하지만 이것은 Foo
모듈을 Foo
패키지로 바꾸고 싶다면, sys.path
의 어딘가에 Foo/
디렉토리를 추가하고 그 안에 모듈을 추가하기 시작하기만 하면 된다는 것을 의미합니다.
하지만 “네임스페이스 패키지”만 원한다면 어떨까요? 즉, 다양한 독립적으로 배포되는 서브 모듈과 서브 패키지를 위한 네임스페이스일 뿐인 패키지는요?
예를 들어, Zope Corporation이 zc.buildout
와 같은 수십 개의 개별 도구를 zc
네임스페이스 아래의 패키지로 배포하는 경우, 배포하는 모든 도구에 빈 zc.py
를 만들고 포함할 필요가 없습니다. (그리고 Linux 또는 다른 OS 벤더라면, zc.py
사본 10개를 동일한 위치에 설치하려고 할 때 발생하는 패키지 설치 충돌을 처리하고 싶지 않을 것입니다!)
문제 없습니다. 임포트 프로세스에 한 가지 사소한 조정을 더하면 됩니다. “고전적인” 임포트 프로세스가 자급자족 모듈이나 패키지를 찾지 못하면 (예: import zc
가 zc.py
또는 zc/__init__.py
를 찾지 못하면), sys.path
의 모든 zc/
디렉토리를 검색하여 __path__
를 다시 빌드하고 목록에 넣습니다.
이 목록이 비어 있으면 ImportError
를 발생시킵니다. 그러나 비어 있지 않으면 빈 zc
모듈을 만들고 목록을 zc.__path__
에 넣습니다. 축하합니다. zc
는 이제 네임스페이스 전용의 “순수 가상(pure virtual)” 패키지입니다! 모듈 내용은 없지만, sys.path
의 어디에 있든 서브 모듈과 서브 패키지를 계속 임포트할 수 있습니다.
(덧붙여서, 임포트 프로토콜에 대한 이 두 가지 추가 사항(즉, 동적으로 추가되는 __path__
및 동적으로 생성되는 모듈)은 부모 패키지의 __path__
를 sys.path
대신 사용하여 자식 __path__
를 생성하는 기반으로 재귀적으로 자식 패키지에 적용됩니다. 이는 자급자족 및 가상 패키지가 제한 없이 서로를 포함할 수 있음을 의미합니다. 단, 가상 패키지를 자급자족 패키지 안에 넣으면 __path__
가 매우 짧아질 수 있습니다!)
하위 호환성 및 성능 (Backwards Compatibility and Performance)
이 두 가지 변경 사항은 오늘날 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
에 충돌하는 서브디렉토리가 있을 때 오탐(false-positive) 임포트 성공을 방지하기 위해 초기 임포트만 성공하지 못하도록 막고 있습니다.
따라서 이러한 작은 변경으로 위 datagen.py
예제는 올바르게 작동할 것입니다. import json
을 할 때, json/
디렉토리의 단순한 존재는 .py
파일을 포함하더라도 임포트 프로세스에 전혀 영향을 미치지 않을 것입니다. json/
디렉토리는 import json.converter
와 같은 임포트가 시도될 때만 검색될 것입니다.
한편, 디렉토리 트리를 탐색하여 패키지와 모듈을 찾는 도구는 기존 pkgutil.walk_modules()
API를 사용하도록 업데이트될 수 있으며, 메모리 내 패키지를 검사해야 하는 도구는 아래 “표준 라이브러리 변경/추가” 섹션에 설명된 다른 API를 사용해야 합니다.
명세 (Specification)
최소한 하나 이상의 .
을 포함하는 이름(즉, 부모 패키지를 가진 모듈)을 임포트할 때 기존 임포트 프로세스에 변경이 적용됩니다.
특히, 부모 패키지가 존재하지 않거나 존재하지만 __path__
속성이 없는 경우, 먼저 부모 패키지에 대한 “가상 경로(virtual path)”를 생성하려는 시도가 이루어집니다 (아래 가상 경로 섹션에 설명된 알고리즘을 따릅니다).
계산된 “가상 경로”가 비어 있으면 오늘날과 마찬가지로 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
에 나타나지 않습니다.
가상 경로 (Virtual Paths)
가상 경로는 sys.path
(최상위 모듈의 경우) 또는 부모 __path__
(서브 모듈의 경우)에서 발견된 각 경로 항목에 대해 PEP 302 “임포터(importer)” 객체를 얻어서 생성됩니다.
(참고: sys.meta_path
임포터는 sys.path
또는 __path__
항목 문자열과 연결되지 않으므로, 이러한 임포터는 이 프로세스에 참여하지 않습니다.)
각 임포터는 get_subpath()
메서드가 있는지 확인하고, 존재하는 경우 경로가 구성되는 모듈/패키지의 전체 이름과 함께 메서드가 호출됩니다. 반환 값은 요청된 패키지에 대한 서브디렉토리를 나타내는 문자열이거나, 그러한 서브디렉토리가 없으면 None
입니다.
임포터가 반환한 문자열은 발견된 순서대로 구축되는 경로 목록에 추가됩니다. (None
값과 get_subpath()
메서드가 없는 경우는 단순히 건너뛰어집니다.)
결과 목록(비어 있든 아니든)은 모듈 이름을 키로 하여 sys.virtual_package_paths
딕셔너리에 저장됩니다.
이 딕셔너리에는 두 가지 목적이 있습니다. 첫째, 가상 패키지의 서브 모듈을 임포트하려는 시도가 두 번 이상 있을 경우 캐시 역할을 합니다.
둘째, 더 중요하게는, 딕셔너리는 런타임에 sys.path
를 확장하는 코드가 임포트된 패키지의 __path__
속성을 그에 따라 업데이트하는 데 사용될 수 있습니다. (자세한 내용은 아래 “표준 라이브러리 변경/추가”를 참조하십시오.)
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
훅을 생성하는 사람들이 재사용할 수 있도록 해야 합니다.
표준 라이브러리 변경/추가 (Standard Library Changes/Additions)
pkgutil
모듈은 이 명세를 적절하게 처리하도록 업데이트되어야 하며, extend_path()
, iter_modules()
등에 필요한 모든 변경 사항을 포함해야 합니다.
특히 pkgutil
에 제안된 변경 및 추가 사항은 다음과 같습니다:
- 새로운
extend_virtual_paths(path_entry)
함수: 새로운sys.path
항목에서 발견된 모든 부분을 포함하도록 기존에 이미 임포트된 가상 패키지의__path__
속성을 확장합니다. 이 함수는 런타임에sys.path
를 확장하는 애플리케이션(예: 플러그인 디렉토리나 egg를 경로에 추가할 때)에서 호출되어야 합니다.- 이 함수의 구현은
sys.virtual_package_paths
를 간단히 상향식(top-down)으로 순회하고,path_entry
가sys.path
에 추가되었음을 감안하여 해당 패키지에 대한 가상 경로에 어떤 경로 항목을 추가해야 하는지 식별하기 위해 필요한get_subpath()
호출을 수행합니다. (또는 서브 패키지의 경우, 부모 패키지의 가상 경로를 기반으로 파생된 서브 경로 항목을 추가합니다.) - (참고: 이 함수는
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
가 yield될 수 있지만 (만약sys.virtual_package_paths
에 나열된 가상 패키지인 경우),zope.foo.bar
는 yield되지 않습니다. (이 함수는extend_virtual_paths()
를 구현하는 데 필요하지만, 임포트된 가상 패키지를 검사해야 하는 다른 코드에도 잠재적으로 유용합니다.) ImpImporter.iter_modules()
는 가상 패키지에서 발견된 모듈의 이름을 감지하고 yield하도록 변경되어야 합니다.- 위의 변경 사항 외에도
zipimport
임포터의iter_modules()
구현도 유사하게 변경되어야 합니다. (참고: 현재 버전의 Python은pkgutil
의 shim을 통해 이를 구현하므로, 기술적으로 이것은pkgutil
의 변경 사항이기도 합니다.) - 마지막으로
imp
모듈 (또는 적절하다면importlib
)은 위 가상 경로 섹션에 설명된 알고리즘을get_virtual_path(modulename, parent_path=None)
함수로 노출하여__import__
대체물 생성자가 이를 사용할 수 있도록 해야 합니다.
구현 참고사항 (Implementation Notes)
가상 패키지 사용자, 개발자 및 배포자를 위한 참고사항
- 가상 패키지는 설정하고 사용하기 쉽지만, 자급자족 패키지를 사용해야 할 시기와 장소는 여전히 존재합니다. 엄밀히 필요하지는 않지만, 자급자족 패키지에
__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()
가 현재 구현된 방식에 문제가 됩니다.
- XXX 이것은 실제 패키지가 아닌 많은 것을 나열할 수 있습니다. 임포트 가능한 내용이 존재하도록 요구해야 할까요? 그렇다면 얼마나 깊이 검색해야 하며, 예를 들어 링크 루프나 다른 파일 시스템으로의 순회를 어떻게 방지할 수 있을까요? 끔찍합니다. 또한, 가상 패키지가 나열되더라도 여전히 임포트할 수 없다는 것은
- “메타” 임포터(즉,
sys.meta_path
에 있는 임포터)는get_subpath()
를 구현할 필요가 없습니다. 왜냐하면 이 메서드는sys.path
항목 및__path__
항목에 해당하는 임포터에 대해서만 호출되기 때문입니다. 메타 임포터가 가상 패키지를 지원하려면 전적으로 자체find_module()
구현 내에서 그렇게 해야 합니다.- 불행히도, 그러한 구현이 다른 메타 임포터나
sys.path
임포터의 패키지 서브 경로와 병합할 수 있을 것 같지는 않으므로, 메타 임포터에 대한 “가상 패키지 지원”의 의미는 현재 정의되지 않았습니다! - (그러나 메타 임포터의 의도된 사용 사례는 특정 모듈 집합에 대해 Python의 일반 임포트 프로세스를 완전히 대체하는 것이며, 현재 구현된 그러한 임포터의 수는 매우 적기 때문에, 이것이 실제로는 큰 문제가 될 것 같지는 않습니다.)
- 불행히도, 그러한 구현이 다른 메타 임포터나
참고 자료 (References)
“namespace” vs “module” packages (mailing list thread)
“Dropping __init__.py
requirement for subpackages”
Namespace Packages resolution
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments