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

Python 개선 제안 한국어 번역

PEP 420 – 암시적 네임스페이스 패키지

Author:
Eric V. Smith <eric at trueblade.com>
Status:
Final
Type:
Standards Track
Created:
19-Apr-2012
Python-Version:
3.3
Post-History:

Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

초록

네임스페이스 패키지는 하나의 Python 패키지를 디스크의 여러 디렉터리에 나누어 두는 메커니즘입니다. 현재 Python 버전에서는 패키지의 __path__를 계산하는 알고리즘을 직접 작성해야 합니다. 여기에서 제안하는 개선을 적용하면 임포트 메커니즘 자체가 패키지를 구성하는 디렉터리 목록을 만들게 됩니다. 이 PEP는 PEP 382와 PEP 402에 문서화된 이전 작업을 바탕으로 합니다. 그 후 이 PEP가 채택되면서 해당 PEP들은 거부되었습니다. 이 PEP의 구현은 [1]에 있습니다.

용어

이 PEP에서는 다음과 같이 용어를 사용합니다.

  • “패키지(package)”는 Python의 import 문이 정의하는 Python 패키지를 가리킵니다.
  • “배포(distribution)”는 Python 패키지 색인에 저장되고 distutils나 setuptools로 설치되는, 따로 설치할 수 있는 Python 모듈 묶음을 가리킵니다.
  • “벤더 패키지(vendor package)”는 운영 체제의 패키징 메커니즘으로 설치되는 파일 묶음을 가리킵니다(예: Linux 시스템에 설치되는 Debian 또는 Redhat 패키지).
  • “일반 패키지(regular package)”는 Python 3.2 이하 버전에서 구현된 방식의 패키지를 가리킵니다.
  • “부분(portion)”은 네임스페이스 패키지를 구성하는, 한 디렉터리 안의 파일 묶음을 가리킵니다(zip 파일 안에 저장되어 있을 수도 있습니다).
  • “레거시 부분(legacy portion)”은 네임스페이스 패키지를 구현하기 위해 __path__ 조작을 사용하는 부분을 가리킵니다.

이 PEP는 “네임스페이스 패키지”라는 새로운 종류의 패키지를 정의합니다.

현재의 네임스페이스 패키지

현재 Python은 패키지를 네임스페이스 패키지로 지정할 수 있도록 pkgutil.extend_path를 제공합니다. 권장하는 사용 방법은 다음 코드를:

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

패키지의 __init__.py에 넣는 것입니다. 패키지의 어느 부분이 먼저 임포트되더라도 extend_path가 호출되도록 모든 배포가 __init__.py에 같은 내용을 넣어야 합니다. 그 결과 패키지의 __init__.py에서는 사실상 어떤 이름도 정의할 수 없습니다. 어느 부분이 먼저 임포트될지는 sys.path에 있는 패키지 조각의 순서에 따라 정해지기 때문입니다. 특별한 기능으로 extend_path는 <packagename>.pkg라는 이름의 파일을 읽으며, 이 파일로 부분을 추가로 선언할 수 있습니다.

setuptools는 pkg_resources.declare_namespace라는 비슷한 함수를 제공하며, 다음과 같은 형식으로 사용합니다.:

import pkg_resources
pkg_resources.declare_namespace(__name__)

declare_namespace가 sys.modules를 통해 패키지의 __path__를 수정하므로, 부분의 __init__.py에서 __path__에 값을 대입할 필요가 없습니다. 특별한 기능으로 declare_namespace는 zip 파일도 지원합니다. 또한 패키지 이름을 내부에 등록해 두므로, 나중에 setuptools가 sys.path에 항목을 추가할 때 각 패키지에 부분을 올바르게 추가할 수 있습니다.

setuptools에서는 배포의 setup.py에서 네임스페이스 패키지를 선언할 수 있습니다. 따라서 배포 개발자가 __init__.py에 마법 같은 __path__ 수정 코드를 직접 넣을 필요가 없습니다.

네임스페이스 패키지가 필요한 다른 이유는 PEP 402의 “The Problem” 절을 참조하십시오. PEP 402는 거부되었지만, 그 동기가 된 사용 사례는 여전히 유효합니다.

근거

네임스페이스 패키지를 다루는 현재의 명령형 접근 방식 때문에 네임스페이스 패키지를 제공하는 메커니즘이 여러 가지 생겼고, 이들은 서로 조금씩 호환되지 않습니다. 예를 들어, pkgutil은 *.pkg 파일을 지원하지만 setuptools는 지원하지 않습니다. 마찬가지로 setuptools는 zip 파일 검사를 지원하고 _namespace_packages 변수에 포션을 추가하는 기능도 지원하지만, pkgutil은 지원하지 않습니다.

네임스페이스 패키지는 여러 디렉터리에 나뉘어 있을 수 있도록(따라서 여러 sys.path 항목을 통해 발견될 수 있도록) 설계되었습니다. 이러한 구성에서는 각 포션이 네임스페이스 패키지를 올바르게 초기화하기만 한다면, 여러 포션이 모두 __init__.py 파일을 제공하더라도 문제가 되지 않습니다. 하지만 Linux 배포판 벤더 등은 분리된 부분들을 합쳐서 모두 같은 파일 시스템 디렉터리에 설치하는 것을 선호합니다. 이렇게 하면 포션들이 대상 시스템에서 같은 파일을 제공하려 하게 되므로 충돌이 일어날 가능성이 생깁니다. 이는 많은 패키지 관리자가 허용하지 않는 일입니다. 암시적 네임스페이스 패키지를 허용하면 __init__.py 파일을 제공해야 한다는 요구 사항을 완전히 없앨 수 있으며, 영향을 받는 포션들을 배포판이 적절하다고 판단하는 대로 공통 디렉터리에 설치하거나 여러 디렉터리에 나누어 설치할 수 있습니다.

네임스페이스 패키지는 네임스페이스 패키지 생성 시점에 부모 경로로부터 계산된 고정된 __path__에 제약받지 않습니다. 표준 라이브러리의 encodings 패키지를 생각해 봅시다:

  1. encodings가 네임스페이스 패키지가 된다고 가정합시다.
  2. 이 패키지는 때때로 표준 io 스트림을 초기화하기 위해 인터프리터 시작 중에 임포트됩니다.
  3. 애플리케이션이 시작 후에 sys.path를 수정하고, 새 경로 항목으로부터 추가 인코딩을 제공하려 합니다.
  4. 3단계에서 추가된 경로 항목에서 발견되는 encodings 포션으로부터 인코딩을 임포트하려고 시도합니다.

임포트 시스템이 encodings 네임스페이스 패키지가 생성된 시점에 존재하던 sys.path 값을 따라서만 포션을 찾도록 제한된다면, 4단계에서 임포트하는 추가 포션을 찾을 때 3단계에서 추가된 경로들은 결코 검색되지 않을 것입니다. 게다가 (어떤 런타임 플래그나 다른 조건 때문에) 2단계를 때때로 건너뛴다면, 3단계에서 추가된 경로 항목은 포션이 처음 임포트될 때 실제로 사용될 것입니다. 따라서 이 PEP는 각 포션이 로드될 때 경로 항목 목록을 동적으로 계산하도록 요구합니다. 임포트 메커니즘은 __path__ 값을 캐시하고 부모 경로가 변경되었음을 감지했을 때만 이를 갱신함으로써 이 작업을 효율적으로 수행할 것으로 예상됩니다. encodings와 같은 최상위 패키지의 경우, 이 부모 경로는 sys.path가 됩니다.

명세

일반 패키지는 계속해서 __init__.py를 가지며 단일 디렉터리에 위치합니다.

네임스페이스 패키지는 __init__.py를 포함할 수 없습니다. 그 결과, 네임스페이스 패키지를 생성하는 용도로는 pkgutil.extend_path와 pkg_resources.declare_namespace가 더 이상 필요하지 않게 됩니다. 네임스페이스 패키지를 지정하기 위한 표시 파일이나 디렉터리는 없습니다.

임포트 처리 중에 임포트 메커니즘은 Python 3.2에서와 마찬가지로 부모 경로의 각 디렉터리를 계속 순회합니다. “foo”라는 이름의 모듈이나 패키지를 찾는 동안, 부모 경로의 각 디렉터리에 대해 다음과 같이 처리합니다:

  • <directory>/foo/__init__.py가 발견되면, 일반 패키지가 임포트되어 반환됩니다.
  • 그렇지 않고 <directory>/foo.{py,pyc,so,pyd}가 발견되면, 모듈이 임포트되어 반환됩니다. 정확한 확장자 목록은 플랫폼과 -O 플래그 지정 여부에 따라 달라집니다. 여기에 나온 목록은 대표적인 예입니다.
  • 그렇지 않고 <directory>/foo가 발견되었으며 그것이 디렉터리라면, 이를 기록해 두고 부모 경로의 다음 디렉터리로 탐색을 계속합니다.
  • 그 밖의 경우에는 부모 경로의 다음 디렉터리로 탐색을 계속합니다.

스캔이 모듈이나 패키지를 반환하지 않고 완료되었으며 적어도 하나의 디렉터리가 기록되었다면, 네임스페이스 패키지가 생성됩니다. 새 네임스페이스 패키지는 다음과 같습니다:

  • 스캔 중에 발견되어 기록된 경로 문자열의 이터러블로 설정된 __path__ 속성을 가집니다.
  • __file__ 속성이 없습니다.

“import foo”가 실행되고 (위 규칙에 따라) “foo”가 네임스페이스 패키지로 발견되면, “foo”는 즉시 패키지로 생성된다는 점에 유의하십시오. 네임스페이스 패키지의 생성은 하위 수준의 임포트가 일어날 때까지 지연되지 않습니다.

네임스페이스 패키지는 일반 패키지와 근본적으로 다르지 않습니다. 단지 패키지를 생성하는 또 다른 방법일 뿐입니다. 네임스페이스 패키지가 일단 생성되면, 그것과 일반 패키지 사이에는 기능상의 차이가 없습니다.

동적 경로 계산

임포트 메커니즘은 각 포션이 로드되기 전에 네임스페이스 패키지의 __path__가 다시 계산되는 것처럼 동작합니다.

성능상의 이유로, 이는 부모 경로가 변경되었음을 감지하는 방식으로 구현될 것으로 예상됩니다. 변경이 일어나지 않았다면 __path__ 재계산은 필요하지 않습니다. 구현은 부모 경로의 내용이 변경되는 것을 감지해야 할 뿐만 아니라, 부모 경로가 새로운 경로 엔트리 리스트 객체로 교체되는 것도 감지해야 합니다.

임포트 파인더와 로더에 미치는 영향

PEP 302는 경로 요소를 검색하기 위해 호출되는 “파인더”를 정의합니다. 이 파인더들의 find_module 메서드는 “로더” 객체나 None 중 하나를 반환합니다.

파인더가 네임스페이스 패키지에 기여하려면 새로운 find_loader(fullname) 메서드를 구현해야 합니다. fullname은 find_module에서와 같은 의미를 가집니다. find_loader는 항상 (loader, <iterable-of-path-entries>) 형태의 2-튜플을 반환합니다. loader는 None일 수 있으며, 이 경우 (비어 있을 수도 있는) <iterable-of-path-entries>가 기록된 경로 엔트리 목록에 추가되고 경로 검색이 계속됩니다. loader가 None이 아니면, 즉시 모듈이나 일반 패키지를 로드하는 데 사용됩니다.

loader가 반환되고 None이 아니더라도, <iterable-of-path-entries>는 여전히 패키지의 경로 엔트리를 포함해야 합니다. 이를 통해 pkgutil.extend_path() 같은 코드가 자신이 로드하지 않는 패키지의 경로 엔트리를 계산할 수 있습니다.

파인더당 여러 개의 경로 엔트리가 허용된다는 점에 유의하십시오. 이는 파인더가 주어진 fullname에 대해 여러 네임스페이스 포션을 발견하는 경우를 지원하기 위한 것입니다. 많은 파인더는 find_loader 호출당 하나의 네임스페이스 패키지 포션만 지원할 것이며, 이 경우 이 이터러블에는 문자열 하나만 포함됩니다.

임포트 메커니즘은 find_loader가 존재하면 그것을 호출하고, 그렇지 않으면 find_module로 대체합니다. find_module은 구현하지만 find_loader는 구현하지 않는 레거시 파인더는 네임스페이스 패키지에 포션을 기여할 수 없습니다.

이 명세는 PEP 302 로더를 확장하여 module_repr()이라는 선택적 메서드를 포함하도록 하며, 이 메서드가 존재하면 모듈 객체의 repr을 생성하는 데 사용됩니다. 자세한 내용은 아래 절을 참조하십시오.

네임스페이스 패키지와 일반 패키지의 차이점

네임스페이스 패키지와 일반 패키지는 매우 비슷합니다. 차이점은 다음과 같습니다:

  • 네임스페이스 패키지의 포션은 모두 같은 디렉터리 구조에서 올 필요가 없으며, 같은 로더에서 올 필요조차 없습니다. 일반 패키지는 자기 완결적입니다. 모든 부분이 같은 디렉터리 계층 안에 있습니다.
  • 네임스페이스 패키지에는 __file__ 속성이 없습니다.
  • 네임스페이스 패키지의 __path__ 속성은 읽기 전용 문자열 이터러블이며, 부모 경로가 수정되면 자동으로 갱신됩니다.
  • 네임스페이스 패키지에는 __init__.py 모듈이 없습니다.
  • 네임스페이스 패키지는 __loader__ 속성에 다른 타입의 객체를 가집니다.

표준 라이브러리의 네임스페이스 패키지

표준 라이브러리의 일부를 네임스페이스 패키지로 구현하는 것은 가능하며, 이 PEP는 이를 명시적으로 허용합니다. 표준 라이브러리 패키지가 네임스페이스 패키지가 될지, 그리고 된다면 언제 될지는 이 PEP의 범위를 벗어납니다.

레거시 네임스페이스 패키지에서 이전하기

앞서 설명했듯이, 이 PEP 이전에는 레거시 포션이 네임스페이스 패키지를 만들기 위해 pkgutil.extend_path()를 사용했습니다. 네임스페이스 패키지의 기존 포션을 모두 한꺼번에 이 PEP로 이전하는 것은 현실적이지 않을 가능성이 높으므로, extend_path()가 PEP 420 네임스페이스 패키지도 인식하도록 수정될 것입니다. 이렇게 하면 네임스페이스의 일부 포션은 레거시 포션으로 남아 있고 다른 포션은 PEP 420으로 이전될 수 있습니다. extend_path()는 과거에 이 기능을 제공한 적이 없으므로, 이러한 혼합 네임스페이스 패키지에는 일반 네임스페이스 패키지가 가진 동적 경로 계산 기능이 없을 것입니다.

패키징에 미치는 영향

네임스페이스 패키지의 여러 포션은 같은 디렉터리에 설치될 수도 있고, 서로 다른 디렉터리에 설치될 수도 있습니다. 이 절에서는 “foo.bar”와 “foo.baz”를 정의하는 두 포션이 있다고 가정합니다. “foo” 자체는 네임스페이스 패키지입니다.

이들이 같은 위치에 설치된다면, sys.path에 있는 디렉터리 안에 “foo”라는 단일 디렉터리가 있게 됩니다. “foo” 안에는 “bar”와 “baz”라는 두 디렉터리가 있게 됩니다. (OS 패키지 관리자 등에 의해) “foo.bar”가 제거된다면, “foo/baz” 디렉터리나 “foo” 디렉터리를 제거하지 않도록 주의해야 합니다. 이 경우 모든 포션이 같은 디렉터리에 있더라도 “foo”는 (__init__.py가 없으므로) 네임스페이스 패키지가 된다는 점에 유의하십시오.

“foo.bar”와 “foo.baz”는 공통된 파일을 전혀 갖지 않으므로 같은 “foo” 디렉터리에 설치될 수 있다는 점에 유의하십시오.

포션들이 서로 다른 위치에 설치된다면, sys.path에 있는 디렉터리들 안에 서로 다른 두 “foo” 디렉터리가 있게 됩니다. “foo/bar”는 이 sys.path 항목 중 하나에, “foo/baz”는 다른 하나에 있게 됩니다. “foo.bar”를 제거할 때는 “foo/bar” 디렉터리와 그에 대응하는 “foo” 디렉터리를 완전히 제거할 수 있습니다. 하지만 “foo/baz”와 그에 대응하는 “foo” 디렉터리는 제거할 수 없습니다.

“foo.bar” 포션은 sys.path에 있는 디렉터리에 설치하고, “foo.baz” 포션은 역시 sys.path에 있는 zip 파일로 제공하는 것도 가능합니다.

예시

중첩된 네임스페이스 패키지

이 예는 다음 디렉터리 구조를 사용합니다:

Lib/test/namespace_pkgs
    project1
        parent
            child
                one.py
    project2
        parent
            child
                two.py

여기서 부모와 자식은 모두 네임스페이스 패키지입니다. 이들의 포션은 서로 다른 디렉터리에 존재하며, __init__.py 파일이 없습니다.

여기서는 부모 디렉터리들을 sys.path에 추가하고, 포션들이 올바르게 발견된다는 것을 보여 줍니다:

>>> import sys
>>> sys.path += ['Lib/test/namespace_pkgs/project1', 'Lib/test/namespace_pkgs/project2']
>>> import parent.child.one
>>> parent.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent', 'Lib/test/namespace_pkgs/project2/parent'])
>>> parent.child.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent/child', 'Lib/test/namespace_pkgs/project2/parent/child'])
>>> import parent.child.two
>>>

동적 경로 계산

이 예제는 비슷한 디렉터리 구조를 사용하지만, 세 번째 부분을 추가합니다:

Lib/test/namespace_pkgs
    project1
        parent
            child
                one.py
    project2
        parent
            child
                two.py
    project3
        parent
            child
                three.py

project1과 project2를 sys.path에 추가한 다음, parent.child.one과 parent.child.two를 임포트합니다. 그다음 project3을 sys.path에 추가하면, parent.child.three가 임포트될 때 project3/parent가 자동으로 parent.__path__에 추가됩니다:

# add the first two parent paths to sys.path
>>> import sys
>>> sys.path += ['Lib/test/namespace_pkgs/project1', 'Lib/test/namespace_pkgs/project2']

# parent.child.one can be imported, because project1 was added to sys.path:
>>> import parent.child.one
>>> parent.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent', 'Lib/test/namespace_pkgs/project2/parent'])

# parent.child.__path__ contains project1/parent/child and project2/parent/child, but not project3/parent/child:
>>> parent.child.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent/child', 'Lib/test/namespace_pkgs/project2/parent/child'])

# parent.child.two can be imported, because project2 was added to sys.path:
>>> import parent.child.two

# we cannot import parent.child.three, because project3 is not in the path:
>>> import parent.child.three
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 1286, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1250, in _find_and_load_unlocked
ImportError: No module named 'parent.child.three'

# now add project3 to sys.path:
>>> sys.path.append('Lib/test/namespace_pkgs/project3')

# and now parent.child.three can be imported:
>>> import parent.child.three

# project3/parent has been added to parent.__path__:
>>> parent.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent', 'Lib/test/namespace_pkgs/project2/parent', 'Lib/test/namespace_pkgs/project3/parent'])

# and project3/parent/child has been added to parent.child.__path__
>>> parent.child.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent/child', 'Lib/test/namespace_pkgs/project2/parent/child', 'Lib/test/namespace_pkgs/project3/parent/child'])
>>>

논의

PyCon 2012에서 네임스페이스 패키지에 관한 논의가 있었으며, 이 자리에서 PEP 382와 PEP 402가 거부되고 이 PEP로 대체되었습니다 [3].

일반 패키지 지원을 제거할 의도는 없습니다. 개발자가 자신의 패키지가 결코 네임스페이스 패키지의 부분이 되지 않으리라는 것을 안다면, (__init__.py가 있는) 일반 패키지로 두는 편이 성능상 유리합니다. 일반 패키지는 경로상에서 발견되는 즉시 생성하고 로드할 수 있습니다. 네임스페이스 패키지의 경우에는 패키지를 생성하기 전에 경로의 모든 항목을 검사해야 합니다.

__init__.py 파일이 없는 디렉터리에 대해 더 이상 ImportWarning이 발생하지 않는다는 점에 유의하십시오. 이전 Python 버전에서는 ImportWarning이 발생했지만, 이제 이러한 디렉터리는 네임스페이스 패키지로 임포트됩니다.

Alyssa (Nick) Coghlan은 이 제안에 대한 자신의 반대 의견 목록을 제시했습니다 [4]. 그 내용은 다음과 같습니다.

  1. 암시적 패키지 디렉터리는 Python의 선(Zen of Python)에 어긋납니다.
  2. 암시적 패키지 디렉터리는 까다로운 하위 호환성 문제를 일으킵니다.
  3. 암시적 패키지 디렉터리는 파일 시스템 레이아웃에 모호함을 초래합니다.
  4. 암시적 패키지 디렉터리는 __main__에 있는 현재의 초보자에게 불친절한 동작을 영구적으로 고착시킬 것입니다.

Alyssa는 나중에 자신의 반대 의견에 대해 상세한 답변을 내놓았으며 [5], 그 내용을 요약하면 다음과 같습니다.

  1. 이 PEP의 실용성이 다른 제안들과 현 상태보다 낫습니다.
  2. 사소한 하위 호환성 문제는 적절히 문서화되기만 한다면 괜찮습니다.
  3. 이 문제는 PEP 395에서 다룰 것입니다.
  4. 이 문제 역시 PEP 395에서 다룰 것입니다.

표준 라이브러리에 네임스페이스 패키지를 포함하게 된 계기를 마련한 사람은 Martin v. Löwis로, 그는 encodings 패키지가 네임스페이스 패키지가 되기를 원했습니다 [6]. 이 PEP는 표준 라이브러리 패키지가 네임스페이스가 되는 것을 허용하지만, encodings에 대한 결정은 미룹니다.

find_module 대 find_loader

이 PEP의 초기 초안은 네임스페이스 패키지를 지원하기 위해 find_module 메서드를 변경하도록 명시했습니다. 네임스페이스 패키지의 부분이 발견된 경우에는 문자열을 반환하도록 수정될 예정이었습니다.

그러나 이는 표준 라이브러리 외부에서 find_module을 호출하는 기존 코드에 문제를 일으켰습니다. 이 코드는 이 PEP가 요구하는 변경 사항에 맞춰 함께 업그레이드되지 않을 것이므로, find_module로부터 예상치 못한 반환값을 받으면 실패하게 됩니다. 이러한 비호환성 때문에, 이 PEP는 이제 네임스페이스 부분을 제공하려는 파인더가 앞에서 설명한 find_loader 메서드를 구현해야 한다고 명시합니다.

find_loader 호출 한 번에 여러 부분을 지원하는 사용 사례는 [7]에 제시되어 있습니다.

동적 경로 계산

Guido는 자동 동적 경로 계산이 불필요한 기능이라는 우려를 제기했습니다 [8]. 그 스레드의 뒷부분에서 PJ Eby와 Alyssa Coghlan은 동적 계산이 Python 사용자의 혼란을 최소화하는 이유에 대한 인자를 제시했습니다. 그 논의의 결론은 이 PEP의 근거(Rationale) 절에 반영되었습니다.

이 PEP의 이전 버전에서는 부모 경로 객체가 제자리에서(in-place) 수정된 경우에만 동적 경로 계산이 효력을 발휘할 수 있도록 요구했습니다. 즉, 다음은 동작합니다:

sys.path.append('new-dir')

하지만 다음은 동작하지 않습니다:

sys.path = sys.path + ['new-dir']

같은 스레드 [8]에서 이 제약이 필요하지 않다는 점이 지적되었습니다. 부모 경로에 대한 참조를 보관하는 대신 이름으로 부모 경로를 조회한다면, 부모 경로를 수정하거나 교체하는 방식에 아무런 제약이 없습니다. 최상위 네임스페이스 패키지의 경우, "sys"라는 이름의 모듈을 찾은 다음 그 속성 "path"를 찾게 됩니다. 패키지 foo 안에 중첩된 네임스페이스 패키지의 경우, "foo"라는 이름의 모듈을 찾은 다음 그 속성 "__path__"를 찾게 됩니다.

모듈 repr

이전에는 모듈 repr이 모듈의 __file__ 속성에 대한 가정을 바탕으로 하드 코딩되어 있었습니다. 이 속성이 존재하고 문자열이면 파일 시스템 경로로 간주되었고, 모듈 객체의 repr은 이를 값에 포함했습니다. 유일한 예외는 PEP 302가 __file__ 속성이 없는 경우를 내장 모듈용으로 예약해 두었다는 것이었으며, CPython에서는 이 가정이 모듈 객체의 구현에 박혀 있었습니다. 이 제약 때문에 일부 모듈은 파일 시스템 경로를 반영하지 않는 억지스러운 __file__ 값을 담고 있었고, 이는 나중에 예기치 않은 문제를 일으킬 수 있었습니다(예: 경로가 아닌 __file__에 os.path.join()을 적용하면 의미 없는 값이 반환됩니다).

이 PEP는 이 제약을 완화하여, __file__의 설정을 모듈을 생성하는 로더의 재량에 맡깁니다. 적절한 파일 시스템 경로가 없다면 로더는 __file__을 설정하지 않은 채로 둘 수 있습니다. 로더는 유용하다면 모듈에 추가적인 예약 속성을 설정할 수도 있습니다. 이는 모듈의 출처를 확정적으로 판별하는 방법이 모듈의 __loader__ 속성을 확인하는 것임을 의미합니다.

예를 들어, 이 PEP에서 설명하는 네임스페이스 패키지는 대응하는 파일이 존재하지 않으므로 __file__ 속성을 갖지 않습니다. 이러한 모듈의 repr에 유연성과 설명력을 제공하기 위해, PEP 302 로더에 새로운 선택적 프로토콜이 추가됩니다. 로더는 모듈 객체 하나를 인자로 받는 module_repr() 메서드를 구현할 수 있습니다. 이 메서드는 모듈의 repr로 그대로 사용될 문자열을 반환해야 합니다. 이제 모듈 repr을 생성하는 규칙은 다음과 같이 표준화됩니다:

  • 모듈에 __loader__가 있고 그 로더에 module_repr() 메서드가 있으면, 모듈 객체 하나를 인자로 하여 이를 호출합니다. 반환된 값이 모듈의 repr로 사용됩니다.
  • module_repr()에서 예외가 발생하면, 그 예외를 잡아서 버리고 module_repr()이 존재하지 않는 것처럼 모듈의 repr 계산을 계속합니다.
  • 모듈에 __file__ 속성이 있으면, 이것이 모듈 repr의 일부로 사용됩니다.
  • 모듈에 __file__은 없지만 __loader__가 있으면, 로더의 repr이 모듈 repr의 일부로 사용됩니다.
  • 그 밖의 경우에는 repr에 모듈의 __name__만 사용합니다.

다음은 네임스페이스 모듈 repr이 로더로부터 어떻게 계산되는지 보여 주는 코드 조각입니다:

class NamespaceLoader:
    @classmethod
    def module_repr(cls, module):
        return "<module '{}' (namespace)>".format(module.__name__)

내장 모듈의 repr도 더 이상 하드코딩할 필요 없이 마찬가지로 해당 로더에서 제공됩니다.:

class BuiltinImporter:
    @classmethod
    def module_repr(cls, module):
        return "<module '{}' (built-in)>".format(module.__name__)

다음은 관련 속성의 조합이 서로 다른 여러 종류의 모듈에 대한 repr 예시입니다:

>>> import email
>>> email
<module 'email' from '/home/barry/projects/python/pep-420/Lib/email/__init__.py'>
>>> m = type(email)('foo')
>>> m
<module 'foo'>
>>> m.__file__ = 'zippy:/de/do/dah'
>>> m
<module 'foo' from 'zippy:/de/do/dah'>
>>> class Loader: pass
...
>>> m.__loader__ = Loader
>>> del m.__file__
>>> m
<module 'foo' (<class '__main__.Loader'>)>
>>> class NewLoader:
...   @classmethod
...   def module_repr(cls, module):
...      return '<mystery module!>'
...
>>> m.__loader__ = NewLoader
>>> m
<mystery module!>
>>>

참고 문헌