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

Python 개선 제안 한국어 번역

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 382PEP 402에 관해 길고 유익한 논의를 진행했습니다. 결국 두 PEP를 모두 거부했지만, PEP의 정신을 이어 갈 새로운 PEP가 작성될 예정입니다. 402. Martin von Löwis가 요약문을 작성했습니다: [3].

초록

이 PEP는 Python의 패키지 가져오기를 다음과 같이 개선할 것을 제안합니다.

  • 다른 언어 사용자들을 덜 놀라게 하기
  • 모듈을 패키지로 변환하기 쉽게 만들기
  • 패키지를 별도로 설치되는 구성 요소로 나눌 수 있도록 지원합니다( PEP 382에 설명된 “네임스페이스 패키지”와 유사합니다).

제안된 개선 사항은 현재 가져올 수 있는 디렉터리 레이아웃의 의미를 변경하지 않지만, 패키지에서 단순화된 디렉터리 레이아웃(현재는 가져올 수 없음)을 사용할 수 있도록 합니다.

그러나 제안된 변경 사항은 기존 모듈이나 패키지를 임포트할 때 어떠한 성능 오버헤드도 추가하지 않으며, 새로운 디렉터리 레이아웃의 성능도 이전의 “네임스페이스 패키지” 솔루션(예: pkgutil.extend_path())과 거의 동일해야 합니다.

문제

“대부분의 패키지는 모듈과 같습니다. 그 내용은 서로 매우 긴밀하게 의존하므로 분리할 수 없습니다. [그러나] 일부 패키지는 별도의 네임스페이스를 제공하기 위해 존재합니다. … 이러한 [네임스페이스 패키지]의 하위 패키지나 하위 모듈을 독립적으로 배포할 수 있어야 합니다.”

—Python 2.3 출시 직전의 Jim Fulton [1]

다른 언어에서 Python으로 넘어온 신규 사용자는 Python의 패키지 가져오기 의미 체계 때문에 종종 혼란을 겪습니다. 예를 들어 Google에서 Guido는 패키지에 __init__ 모듈이 포함되어야 한다는 요구 사항이 “잘못된 기능”이므로 없애야 한다는 “쇠스랑을 든 대규모 군중” [2]의 불만을 받았습니다.

또한 Java나 Perl과 같은 언어에서 온 사용자도 Python의 가져오기 경로 검색 방식의 차이 때문에 때때로 혼란을 겪습니다.

Python의 sys.path와 유사한 경로 메커니즘을 사용하는 대부분의 다른 언어에서는 패키지가 모듈이나 클래스를 포함하는 네임스페이스일 뿐이므로, 해당 언어의 경로에 있는 여러 디렉터리에 걸쳐 분산될 수 있습니다. 예를 들어 Perl에서는 모듈 포함 경로 전체의 Foo/ 하위 디렉터리에서 Foo::Bar 모듈을 검색하며, 처음 발견되는 해당 하위 디렉터리에서만 검색하지는 않습니다.

더 나쁜 점은 이것이 신규 사용자만의 문제가 아니라는 것입니다. 누구든지패키지를 별도로 설치 가능한 구성 요소로 쉽게 분할할 수 없게 만듭니다. 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의 현재 패키지 처리 방식이 다소 미흡하기 때문이라는 사실이 곧 분명해졌습니다.

즉, 패키지 시스템을 갖춘 다른 인기 언어에서는 모든 패키지가 일반적으로 원하는 방식으로 동작하므로 “네임스페이스 패키지”를 설명하기 위한 특별한 용어가 필요하지 않습니다.

(파이썬처럼) 특별한 마커 모듈이 있는 고립된 단일 디렉터리가 아니라, 다른 언어의 패키지는 일반적으로 가져오기 또는 포함 경로 전체에 걸쳐 적절한 이름을 가진 디렉터리들의 합집합입니다.

예를 들어 Perl에서는 Foo 모듈이 항상 Foo.pm 파일에서 발견되고, Foo::Bar 모듈이 항상 Foo/Bar.pm 파일에서 발견됩니다. (다시 말해, 특정 모듈의 위치를 찾는 하나의 명백한 방법이 있습니다.)

이는 Perl이 모듈을 패키지와 다른 것으로 간주하기 때문입니다. 패키지는 다른 모듈이 존재할 수 있는 순수한 네임스페이스이며, 모듈의 이름이기도 한 것은 단지 우연히 그렇습니다.

그러나 현재 버전의 Python에서는 모듈과 패키지가 서로 더 밀접하게 결합되어 있습니다. FooFoo.py에서 발견되든 Foo/__init__.py에서 발견되든 항상 모듈이며, 하위 모듈이 있다면 이들과도 긴밀하게 연결되어 있습니다. 하위 모듈은 __init__.py가 발견된 정확히 동일한 디렉터리에 반드시 있어야 합니다.

긍정적인 측면에서 이러한 설계 선택은 패키지가 상당히 독립적으로 구성된다는 것을 의미하며, 패키지의 루트 디렉터리에 작업을 수행하기만 하면 하나의 단위로 설치하거나 복사하는 등의 작업을 수행할 수 있습니다.

그러나 부정적인 측면에서는 초보자에게 직관적이지 않으며, 모듈을 패키지로 바꾸려면 더 복잡한 단계가 필요합니다. FooFoo.py로 시작했다면 이를 Foo/__init__.py로 이동하고 이름을 변경해야 합니다.

반대로 처음부터 Foo.Bar 모듈을 만들려고 하지만 Foo 자체에 넣을 특별한 모듈 내용이 없다면, Foo.Bar를 임포트할 수 있도록 비어 있고 겉보기에는 무관해 보이는 Foo/__init__.py 파일을 만들어야 합니다.

(그리고 이러한 문제는 언어를 처음 접하는 사람들만 혼란스럽게 하는 것도 아닙니다. 경험 많은 개발자들도 이 문제 때문에 불편을 겪습니다.)

따라서 Import-SIG에서 얼마간 논의한 후, 위의 문제를 “네임스페이스 패키지” 사용 사례만이 아니라 모든 해결하려는 시도로서 이 PEP가 PEP 382의 대안으로 만들어졌습니다.

또한 기분 좋은 부수 효과로, 이 PEP에서 제안하는 해결책은 일반 모듈이나 독립적으로 구성된(즉, __init__ 기반) 패키지의 임포트 성능에 영향을 주지 않습니다.

해결책

과거에는 패키지 디렉터리 배치에 더욱 직관적으로 접근할 수 있도록 하는 다양한 제안이 이루어졌습니다. 그러나 대부분은 명백한 하위 호환성 문제 때문에 실패했습니다.

즉, __init__ 모듈에 대한 요구 사항을 단순히 없애면 sys.path에 가령 string이라는 이름의 디렉터리가 있어 표준 라이브러리 string 모듈의 임포트를 차단할 가능성이 생깁니다.

그러나 역설적으로 이 접근 방식의 실패는 __init__ 요구 사항을 제거했기 때문에 비롯되는 것은 아닙니다!

오히려 이러한 실패는 그 기저 접근 방식이 패키지를 두 가지가 아니라 단지 하나의 것으로 당연히 간주하기 때문에 발생합니다.

실제로 패키지는 서로 분리되어 있지만 관련된 두 개체로 구성됩니다. 하나는 자체적인 선택적 내용을 가진 모듈이고, 다른 하나는 다른 모듈이나 패키지를 찾을 수 있는 네임스페이스입니다.

그러나 현재 Python 버전에서는 모듈 부분(__init__에서 발견됨)과 하위 모듈 임포트를 위한 네임스페이스(__path__속성으로 표현됨)가 패키지를 처음 임포트할 때 동시에 초기화됩니다.

또한 이 두 가지를 초기화하는 방법이 이것이 유일한 방법이라고 가정하면, 기존 디렉터리 배치와 하위 호환성을 유지하면서 __init__ 모듈의 필요성을 없앨 방법은 없습니다.

결국 원하는 이름과 일치하는 디렉터리를 sys.path에서 만나는 순간 패키지를 “찾은” 것이므로 검색을 중단해야 한다는 뜻이지요, 그렇지 않습니까?

글쎄요, 꼭 그렇지는 않습니다.

사고 실험

잠시 타임머신을 타고 1990년대 초로 돌아갔다고 가정해 봅시다. Python 패키지와 __init__.py가 발명되기 직전입니다. 하지만 우리가 Perl과 유사한 패키지 임포트에 익숙하며, Python에서 비슷한 시스템을 구현하려 한다고 상상해 보십시오.

그래도 기반으로 삼을 Python의 모듈 임포트는 있으므로, Foo.pyFoo 패키지의 부모 Foo 모듈로 두는 것은 충분히 생각해 볼 수 있습니다. 그렇다면 하위 모듈과 하위 패키지의 임포트는 어떻게 구현해야 할까요?

아직 __path__ 속성이라는 개념이 없었다면, 아마 sys.path를 검색하여 Foo/Bar.py를 찾았을 것입니다.

하지만 오직 누군가 실제로 Foo.Barimport하려고 했을 때에만 그렇게 했을 것입니다.

Foo를 임포트했을 때가 아닙니다.

그리고 그것으로 2011년 당시 __init__요구 사항을 없애는 하위 호환성 문제를 해결할 수 있습니다.

어떻게 합니까?

즉, import Foo를 실행할 때는 아직 신경 쓸 필요가 없으므로 sys.path에서 Foo/ 디렉터리를 찾아보지도 않습니다. 우리가 신경 쓰는 유일한 시점은 누군가 실제로 Foo의 하위 모듈이나 하위 패키지를 임포트하려고 하는 시점입니다.

따라서 예를 들어 Foo 모듈이 표준 라이브러리 모듈이고, 마침 sys.pathFoo 디렉터리가 있다면(물론 __init__.py는 없는 경우), 아무것도 망가지지 않습니다. Foo 모듈은 여전히 단순한 모듈이며, 여전히 정상적으로 임포트됩니다.

독립형 패키지와 “가상” 패키지

물론 오늘날의 Python에서는 Foo가 단순한 Foo.py 모듈이고(따라서 __path__ 속성이 없으면), import Foo.Bar를 시도할 때 실패합니다.

따라서 이 PEP에서는 __path__가 없는 경우 이를 동적으로 생성할 것을 제안합니다.

즉, import Foo.Bar를 시도하면 제안된 가져오기 메커니즘 변경 사항은 Foo 모듈에 __path__가 없음을 감지하고, 계속 진행하기 전에 이를 구성하려고 시도합니다.

그리고 sys.path에 나열된 디렉터리들에 존재하는 모든 Foo/ 하위 디렉터리의 목록을 만드는 방식으로 이를 수행합니다.

목록이 비어 있으면 오늘날과 마찬가지로 임포트가 ImportError와 함께 실패합니다. 그러나 목록이 비어 있지 않으면 새 Foo.__path__ 속성에 저장되어 해당 모듈이 “가상 패키지”가 됩니다.

즉, 이제 유효한 __path__가 있으므로 하위 모듈이나 하위 패키지를 정상적인 방식으로 임포트할 수 있습니다.

이제 이 변경 사항은 __init__ 모듈을 포함하는 “기존” 독립형 패키지에는 영향을 주지 않는다는 점에 유의하십시오. 이러한 패키지는 이미 __path__ 속성을 가지고 있습니다(임포트 시 초기화됨). 따라서 임포트 메커니즘은 나중에 다른 속성을 만들려고 하지 않습니다.

이는 예를 들어 표준 라이브러리의 email 패키지가 sys.pathemail이라는 서로 무관한 디렉터리를 여러 개 가지고 있더라도 어떤 방식으로든 영향을 받지 않는다는 뜻입니다. (그 디렉터리에 *.py 파일이 들어 있는 경우에도 마찬가지입니다.)

그러나 이는 sys.path의 어딘가에 Foo/ 디렉터리를 추가하고 그 안에 모듈을 추가하기 시작하기만 하면 Foo 모듈을 Foo 패키지로 바꿀 수 있다는 의미입니다.

하지만 “네임스페이스 패키지”만 원한다면 어떻게 합니까? 즉, 다양한 별도 배포 서브모듈과 서브패키지를 위한 유일한 네임스페이스인 패키지입니까?

예를 들어 Zope Corporation이라면 zc 네임스페이스 아래의 패키지에 각각 포함된 zc.buildout 같은 수십 개의 별도 도구를 배포하면서, 제공하는 모든 도구에 빈 zc.py를 만들고 포함해야 하는 상황을 원하지 않을 것입니다. (또한 Linux나 다른 OS 공급업체라면 zc.py 사본 열 개를 같은 위치에 설치하려고 할 때 발생하는 패키지 설치 충돌을 처리하고 싶지 않을 것입니다!)

문제없습니다. 임포트 프로세스를 한 가지 더 사소하게 조정하기만 하면 됩니다. “기존” 임포트 프로세스가 독립형 모듈이나 패키지를 찾지 못하면(예를 들어 import zczc.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도 마찬가지로 실패합니다.

따라서 잠재적인 하위 호환성 문제는 다음과 같습니다.

  1. 패키지 디렉터리에 __init__ 모듈이 있어야 한다고 예상하거나, __init__ 모듈이 없는 디렉터리는 가져올 수 없어야 한다고 예상하거나, __path__속성이 정적이어야 한다고 예상하는 도구는 가상 패키지를 패키지로 인식하지 못합니다.

    (실제로 이는 도구가 가상 패키지를 지원하도록 업데이트되어야 한다는 뜻이며, 예를 들어 하드코딩된 파일 시스템 검색 대신 pkgutil.walk_modules()를 사용해야 합니다.)

  2. 특정 가져오기가 실패할 것으로 예상하는 코드는 이제 예기치 않은 작업을 수행할 수 있습니다. 대부분의 정상적인 비테스트 코드는 존재하지 않을 것으로 예상되는 항목을 가져오지 않으므로, 실제로는 이러한 경우가 상당히 드뭅니다!

위의 경우에서 가장 가능성이 큰 예외는 코드 일부가 패키지를 가져와서 해당 패키지가 설치되어 있는지 확인하려는 경우입니다. 이를 오직 최상위 모듈을 가져오는 것만으로 수행하고(즉, __version__이나 다른 속성을 확인하지 않고), 또한 sys.path의 어딘가에 찾는 패키지와 같은 이름의 디렉터리가 있으며, 또한 해당 패키지가 실제로 설치되어 있지 않다면, 그런 코드는 실제로는 설치되지 않은 패키지가 설치되어 있다고 잘못 믿게 될 수 있습니다.

예를 들어, 누군가 다음 코드를 포함하는 스크립트(datagen.py)를 작성한다고 가정하십시오.:

try:
    import json
except ImportError:
    import simplejson as json

그리고 다음과 같이 구성된 디렉터리에서 이를 실행한다고 가정하십시오.:

datagen.py
json/
    foo.js
    bar.js

import jsonjson/ 하위 디렉터리가 단순히 존재하기 때문에 성공한다면, 코드는 json 모듈을 사용할 수 있다고 잘못 판단하고 오류와 함께 실패하게 됩니다.

그러나 지금까지 제시한 알고리즘을 조금만 변경하면 이러한 특수한 경우가 발생하지 않도록 간단히 방지할 수 있습니다. zc와 같은 “순수 가상” 패키지를 가져오도록 허용하는 대신, 가상 패키지의 내용만 가져오도록 허용합니다.

즉, sys.pathzc.py또는 zc/__init__.py가 없다면 import zc문은 ImportError를 발생시켜야 합니다. 하지만 import zc.buildout을 수행하는 것은 sys.pathzc/buildout.py또는 zc/buildout/__init__.py가 있는 한 여전히 성공해야 합니다.

다시 말해, 순수 가상 패키지를 직접 가져오는 것은 허용하지 않고 모듈과 자체 완결형 패키지만 허용합니다. (이는 허용 가능한 제한입니다. 그러한 패키지를 단독으로 가져오는 데에는 기능적 가치가 없기 때문입니다. 결국, 해당 패키지의 서브패키지나 서브모듈 중 하나 이상을 가져오기 전까지는 모듈 객체에 내용이 전혀 없습니다!)

하지만 zc.buildout을 성공적으로 임포트한 후에는 sys.moduleszc 모듈이 존재하게 되며, 이를 임포트하려고 하면 당연히 성공합니다. 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__ 속성을 추가하여 독립형 모듈을 패키지로 변환하지도 않습니다.

그런데 이 변경 사항은 재귀적으로 적용되어야 합니다. 즉, foofoo.bar가 순수 가상 패키지라면 import foo.bar.bazfoo.bar.baz를 찾을 때까지 기다린 후에 모듈 객체인 foofoo.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에 제안된 변경 사항과 추가 사항은 다음과 같습니다.

  • 이미 임포트된 기존 가상 패키지의 __path__속성을 확장하여 새로운 sys.path 항목에서 발견된 부분을 포함하도록 하는 새로운 extend_virtual_paths(path_entry) 함수입니다. 이 함수는 런타임에 sys.path를 확장하는 애플리케이션에서 호출해야 합니다. 예를 들어 플러그인 디렉터리나 egg를 경로에 추가할 때 호출해야 합니다.

    이 함수의 구현은 sys.virtual_package_paths를 단순히 하향식으로 순회하며, path_entrysys.path에 추가되었을 때 해당 패키지의 가상 경로에 추가해야 하는 경로 항목을 식별하기 위해 필요한 get_subpath() 호출을 수행합니다. (또는 서브패키지의 경우 부모 패키지의 가상 경로를 기반으로 파생된 하위 경로 항목을 추가합니다.)

    (참고: 이 함수는 sys.virtual_package_paths에 있는 경로 값과 sys.modules에 있는 해당 모듈의 __path__속성을 모두 업데이트해야 합니다. 일반적인 경우에는 둘 다 동일한 list 객체이더라도 마찬가지입니다.)

  • sys.virtual_package_paths에서 가상 패키지를 하향식으로 순회할 수 있도록 하는 새로운 iter_virtual_packages(parent='') 함수로, parent의 자식 가상 패키지를 생성합니다. 예를 들어 iter_virtual_packages("zope")를 호출하면 zope.appzope.products를 생성할 수 있지만(이들이 sys.virtual_package_paths에 나열된 가상 패키지인 경우), zope.foo.bar생성하지 않습니다. (이 함수는 extend_virtual_paths()를 구현하는 데 필요하지만, 임포트된 가상 패키지를 검사해야 하는 다른 코드에도 잠재적으로 유용합니다.)
  • ImpImporter.iter_modules()를 변경하여 가상 패키지에서 발견된 모듈의 이름도 감지하고 생성해야 합니다.

위의 변경 사항에 더하여 zipimport 임포터의 iter_modules() 구현도 이와 유사하게 변경해야 합니다. (참고: 현재 Python 버전에서는 이를 pkgutil의 심을 통해 구현하므로, 기술적으로는 이 역시 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__을 사용해야 합니다.

    (참고: 이 주의사항은 오늘날 기존 “네임스페이스 패키지” 사용자에게도 이미 적용됩니다. 즉, 이는 패키지를 분할할 수 있다는 사실에서 필연적으로 발생하는 결과이므로, 원하는 데이터 파일이 어느 분할 영역에 있는지 알아야 합니다. 독립형 패키지에서 가상 패키지로 전환하는 새로운 사용자도 이를 알고 있도록 여기에서 언급합니다.)

  • 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의 일반 임포트 프로세스를 완전히 대체하는 것이고, 현재 구현된 이러한 임포터의 수가 매우 적으므로 실제로 큰 문제가 될 가능성은 낮아 보입니다.)

참고 자료